Merge branch 'SIG-6819' into SIG-6421

This commit is contained in:
SagarRajput-7 2025-05-27 11:48:55 +05:30 committed by GitHub
commit 1b7f13bf1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 3179 additions and 414 deletions

View File

@ -23,6 +23,7 @@ import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@ -358,6 +359,7 @@ function App(): JSX.Element {
<CompatRouter>
<UserpilotRouteTracker />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
@ -386,6 +388,7 @@ function App(): JSX.Element {
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</CompatRouter>
</Router>

View File

@ -0,0 +1,191 @@
import React from 'react';
type ErrorIconProps = React.SVGProps<SVGSVGElement>;
function ErrorIcon({ ...props }: ErrorIconProps): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
<path
fill="#C62828"
d="M1.281 5.78a.922.922 0 0 0-.92.921v4.265a.922.922 0 0 0 .92.92h.617V5.775l-.617.005ZM12.747 5.78c.508 0 .92.413.92.92v4.264a.923.923 0 0 1-.92.922h-.617V5.775l.617.004Z"
/>
<path
fill="#90A4AE"
d="M12.463 5.931 12.45 4.82a.867.867 0 0 0-.87-.861H7.34v-1.49a1.083 1.083 0 1 0-.68 0v1.496H2.42a.867.867 0 0 0-.864.86v7.976c.003.475.389.86.865.862h9.16a.868.868 0 0 0 .869-.862v-.82h.013v-6.05Z"
/>
<path
fill="#C62828"
d="M7 1.885a.444.444 0 1 1 0-.888.444.444 0 0 1 0 .888Z"
/>
<path
fill="url(#a)"
d="M4.795 10.379h4.412c.384 0 .696.312.696.697v.063a.697.697 0 0 1-.696.697H4.795a.697.697 0 0 1-.697-.697v-.063c0-.385.312-.697.697-.697Z"
/>
<path fill="url(#b)" d="M6.115 10.38h-.262v1.455h.262V10.38Z" />
<path fill="url(#c)" d="M7.138 10.38h-.262v1.455h.262V10.38Z" />
<path fill="url(#d)" d="M8.147 10.38h-.262v1.455h.262V10.38Z" />
<path fill="url(#e)" d="M9.22 10.38h-.262v1.455h.262V10.38Z" />
<path fill="url(#f)" d="M5.042 10.379H4.78v1.454h.262V10.38Z" />
<path
fill="#C62828"
d="M7 9.367h-.593a.111.111 0 0 1-.098-.162l.304-.6.288-.532a.11.11 0 0 1 .195 0l.29.556.301.576a.11.11 0 0 1-.098.162H7Z"
/>
<path
fill="url(#g)"
d="M4.627 8.587a1.278 1.278 0 1 0 0-2.556 1.278 1.278 0 0 0 0 2.556Z"
/>
<path
fill="url(#h)"
fillRule="evenodd"
d="M4.627 6.142a1.167 1.167 0 1 0 0 2.333 1.167 1.167 0 0 0 0-2.333ZM3.237 7.31a1.389 1.389 0 1 1 2.778 0 1.389 1.389 0 0 1-2.777 0Z"
clipRule="evenodd"
/>
<path
fill="url(#i)"
d="M9.333 6.028a1.278 1.278 0 1 0 0 2.556 1.278 1.278 0 0 0 0-2.556Z"
/>
<path
fill="url(#j)"
fillRule="evenodd"
d="M7.944 7.306a1.39 1.39 0 0 1 2.778 0 1.389 1.389 0 0 1-2.778 0Zm1.39-1.167a1.167 1.167 0 1 0 0 2.334 1.167 1.167 0 0 0 0-2.334Z"
clipRule="evenodd"
/>
<defs>
<linearGradient
id="a"
x1="7.001"
x2="7.001"
y1="11.836"
y2="10.379"
gradientUnits="userSpaceOnUse"
>
<stop offset=".12" stopColor="#E0E0E0" />
<stop offset=".52" stopColor="#fff" />
<stop offset="1" stopColor="#EAEAEA" />
</linearGradient>
<linearGradient
id="b"
x1="5.984"
x2="5.984"
y1="11.835"
y2="10.381"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="c"
x1="7.007"
x2="7.007"
y1="11.835"
y2="10.381"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="d"
x1="8.016"
x2="8.016"
y1="11.835"
y2="10.381"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="e"
x1="9.089"
x2="9.089"
y1="11.835"
y2="10.381"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="f"
x1="4.911"
x2="4.911"
y1="11.833"
y2="10.379"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="h"
x1="3.238"
x2="6.015"
y1="7.309"
y2="7.309"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<linearGradient
id="j"
x1="7.939"
x2="10.716"
y1="7.306"
y2="7.306"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333" />
<stop offset=".55" stopColor="#666" />
<stop offset="1" stopColor="#333" />
</linearGradient>
<radialGradient
id="g"
cx="0"
cy="0"
r="1"
gradientTransform="matrix(1.27771 0 0 1.2777 4.627 7.309)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".48" stopColor="#fff" />
<stop offset=".77" stopColor="#FDFDFD" />
<stop offset=".88" stopColor="#F6F6F6" />
<stop offset=".96" stopColor="#EBEBEB" />
<stop offset="1" stopColor="#E0E0E0" />
</radialGradient>
<radialGradient
id="i"
cx="0"
cy="0"
r="1"
gradientTransform="matrix(1.27771 0 0 1.2777 9.328 7.306)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".48" stopColor="#fff" />
<stop offset=".77" stopColor="#FDFDFD" />
<stop offset=".88" stopColor="#F6F6F6" />
<stop offset=".96" stopColor="#EBEBEB" />
<stop offset="1" stopColor="#E0E0E0" />
</radialGradient>
</defs>
</svg>
);
}
export default ErrorIcon;

View File

@ -0,0 +1,118 @@
.error-modal {
&__trigger {
width: fit-content;
display: flex;
align-items: center;
gap: 4px;
border-radius: 20px;
background: rgba(229, 72, 77, 0.2);
padding-left: 3px;
padding-right: 8px;
cursor: pointer;
span {
color: var(--bg-cherry-500);
font-size: 10px;
font-weight: 500;
line-height: 20px; /* 200% */
letter-spacing: 0.4px;
text-transform: uppercase;
}
}
&__wrap {
background: linear-gradient(
180deg,
rgba(11, 12, 14, 0.12) 0.07%,
rgba(39, 8, 14, 0.24) 50.04%,
rgba(106, 29, 44, 0.36) 75.02%,
rgba(197, 57, 85, 0.48) 87.51%,
rgba(242, 71, 105, 0.6) 100%
);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
.ant-modal {
bottom: 40px;
top: unset;
position: absolute;
width: 520px;
left: 0px;
right: 0px;
margin: auto;
}
}
&__body {
padding: 0;
background: var(--bg-ink-400);
overflow: hidden;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&__header {
background: none !important;
.ant-modal-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.key-value-label {
padding: 0;
border: none;
border-radius: 4px;
overflow: hidden;
&__key,
&__value {
padding: 4px 8px;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.48px;
}
&__key {
text-transform: uppercase;
&,
&:hover {
color: var(--bg-vanilla-100);
}
}
&__value {
color: var(--bg-vanilla-400);
pointer-events: none;
}
}
.close-button {
padding: 3px 7px;
background: var(--bg-ink-400);
display: inline-flex;
align-items: center;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
box-shadow: none;
}
}
&__footer {
margin: 0 !important;
height: 6px;
background: var(--bg-sakura-500);
}
&__content {
padding: 0 !important;
border-radius: 4px;
overflow: hidden;
background: none !important;
}
}
.lightMode {
.error-modal {
&__body,
&__header .close-button {
background: var(--bg-vanilla-100);
}
&__header .close-button {
svg {
fill: var(--bg-vanilla-100);
}
}
}
}

View File

@ -0,0 +1,195 @@
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
import APIError from 'types/api/error';
import ErrorModal from './ErrorModal';
// Mock the query client to return version data
const mockVersionData = {
payload: {
ee: 'Y',
version: '1.0.0',
},
};
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: (): { getQueryData: () => typeof mockVersionData } => ({
getQueryData: jest.fn(() => mockVersionData),
}),
}));
const mockError: APIError = new APIError({
httpStatusCode: 400,
error: {
// eslint-disable-next-line sonarjs/no-duplicate-string
message: 'Something went wrong while processing your request.',
// eslint-disable-next-line sonarjs/no-duplicate-string
code: 'An error occurred',
// eslint-disable-next-line sonarjs/no-duplicate-string
url: 'https://example.com/docs',
errors: [
{ message: 'First error detail' },
{ message: 'Second error detail' },
{ message: 'Third error detail' },
],
},
});
describe('ErrorModal Component', () => {
it('should render the modal when open is true', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the error message is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument();
expect(
screen.getByText('Something went wrong while processing your request.'),
).toBeInTheDocument();
});
it('should not render the modal when open is false', () => {
render(<ErrorModal error={mockError} open={false} onClose={jest.fn()} />);
// Check that the modal content is not in the document
expect(screen.queryByText('An error occurred')).not.toBeInTheDocument();
});
it('should call onClose when the close button is clicked', async () => {
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} open onClose={onCloseMock} />);
// Click the close button
const closeButton = screen.getByTestId('close-button');
act(() => {
fireEvent.click(closeButton);
});
// Check if onClose was called
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
it('should display version data if available', async () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the version data is displayed
expect(screen.getByText('ENTERPRISE')).toBeInTheDocument();
expect(screen.getByText('1.0.0')).toBeInTheDocument();
});
it('should render the messages count badge when there are multiple errors', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the messages count badge is displayed
expect(screen.getByText('MESSAGES')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
// Check if the individual error messages are displayed
expect(screen.getByText('First error detail')).toBeInTheDocument();
expect(screen.getByText('Second error detail')).toBeInTheDocument();
expect(screen.getByText('Third error detail')).toBeInTheDocument();
});
it('should render the open docs button when URL is provided', async () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the open docs button is displayed
const openDocsButton = screen.getByTestId('error-docs-button');
expect(openDocsButton).toBeInTheDocument();
expect(openDocsButton).toHaveAttribute('href', 'https://example.com/docs');
expect(openDocsButton).toHaveAttribute('target', '_blank');
});
it('should not display scroll for more if there are less than 10 messages', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
expect(screen.queryByText('Scroll for more')).not.toBeInTheDocument();
});
it('should display scroll for more if there are more than 10 messages', async () => {
const longError = new APIError({
httpStatusCode: 400,
error: {
...mockError.error,
code: 'An error occurred',
message: 'Something went wrong while processing your request.',
url: 'https://example.com/docs',
errors: Array.from({ length: 15 }, (_, i) => ({
message: `Error detail ${i + 1}`,
})),
},
});
render(<ErrorModal error={longError} open onClose={jest.fn()} />);
// Check if the scroll hint is displayed
expect(screen.getByText('Scroll for more')).toBeInTheDocument();
});
});
it('should render the trigger component if provided', () => {
const mockTrigger = <button type="button">Open Error Modal</button>;
render(
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={jest.fn()}
/>,
);
// Check if the trigger component is rendered
expect(screen.getByText('Open Error Modal')).toBeInTheDocument();
});
it('should open the modal when the trigger component is clicked', async () => {
const mockTrigger = <button type="button">Open Error Modal</button>;
render(
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={jest.fn()}
/>,
);
// Click the trigger component
const triggerButton = screen.getByText('Open Error Modal');
act(() => {
fireEvent.click(triggerButton);
});
// Check if the modal is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument();
});
it('should render the default trigger tag if no trigger component is provided', () => {
render(<ErrorModal error={mockError} onClose={jest.fn()} />);
// Check if the default trigger tag is rendered
expect(screen.getByText('error')).toBeInTheDocument();
});
it('should close the modal when the onCancel event is triggered', async () => {
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} onClose={onCloseMock} />);
// Click the trigger component
const triggerButton = screen.getByText('error');
act(() => {
fireEvent.click(triggerButton);
});
await waitFor(() => {
expect(screen.getByText('An error occurred')).toBeInTheDocument();
});
// Trigger the onCancel event
act(() => {
fireEvent.click(screen.getByTestId('close-button'));
});
// Check if the modal is closed
expect(onCloseMock).toHaveBeenCalledTimes(1);
await waitFor(() => {
// check if the modal is not visible
const modal = document.getElementsByClassName('ant-modal');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
});
});

View File

@ -0,0 +1,102 @@
import './ErrorModal.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal, Tag } from 'antd';
import { CircleAlert, X } from 'lucide-react';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { useAppContext } from 'providers/App/App';
import React from 'react';
import APIError from 'types/api/error';
import ErrorContent from './components/ErrorContent';
type Props = {
error: APIError;
triggerComponent?: React.ReactElement;
onClose?: () => void;
open?: boolean;
};
const classNames = {
body: 'error-modal__body',
mask: 'error-modal__mask',
header: 'error-modal__header',
footer: 'error-modal__footer',
content: 'error-modal__content',
};
function ErrorModal({
open,
error,
triggerComponent,
onClose,
}: Props): JSX.Element {
const [visible, setVisible] = React.useState(open);
const handleClose = (): void => {
setVisible(false);
onClose?.();
};
const { versionData } = useAppContext();
const versionDataPayload = versionData;
return (
<>
{!triggerComponent ? (
<Tag
className="error-modal__trigger"
icon={<CircleAlert size={14} color={Color.BG_CHERRY_500} />}
color="error"
onClick={(): void => setVisible(true)}
>
error
</Tag>
) : (
React.cloneElement(triggerComponent, {
onClick: () => setVisible(true),
})
)}
<Modal
open={visible}
footer={<div className="error-modal__footer" />}
title={
<>
{versionDataPayload ? (
<KeyValueLabel
badgeKey={versionDataPayload.ee === 'Y' ? 'ENTERPRISE' : 'COMMUNITY'}
badgeValue={versionDataPayload.version}
/>
) : (
<div className="error-modal__version-placeholder" />
)}
<Button
type="default"
className="close-button"
onClick={handleClose}
data-testid="close-button"
>
<X size={16} color={Color.BG_VANILLA_400} />
</Button>
</>
}
onCancel={handleClose}
closeIcon={false}
classNames={classNames}
wrapClassName="error-modal__wrap"
>
<ErrorContent error={error} />
</Modal>
</>
);
}
ErrorModal.defaultProps = {
onClose: undefined,
triggerComponent: null,
open: false,
};
export default ErrorModal;

View File

@ -0,0 +1,208 @@
.error-content {
display: flex;
flex-direction: column;
// === SECTION: Summary (Top)
&__summary-section {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--bg-slate-400);
}
&__summary {
display: flex;
justify-content: space-between;
padding: 16px;
}
&__summary-left {
display: flex;
align-items: baseline;
gap: 8px;
}
&__summary-text {
display: flex;
flex-direction: column;
gap: 6px;
}
&__error-code {
color: var(--bg-vanilla-100);
margin: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
}
&__error-message {
margin: 0;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&__docs-button {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 12.5px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
}
&__message-badge {
display: flex;
align-items: center;
gap: 12px;
padding: 0px 16px 16px;
.key-value-label {
width: fit-content;
border-color: var(--bg-slate-400);
border-radius: 20px;
overflow: hidden;
&__key {
padding-left: 8px;
padding-right: 8px;
}
&__value {
padding-right: 10px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
pointer-events: none;
}
}
&-label {
display: flex;
align-items: center;
gap: 6px;
&-dot {
height: 6px;
width: 6px;
background: var(--bg-sakura-500);
border-radius: 50%;
}
&-text {
color: var(--bg-vanilla-100);
font-size: 10px;
font-weight: 500;
line-height: 18px; /* 180% */
letter-spacing: 0.5px;
}
}
&-line {
flex: 1;
height: 8px;
background-image: radial-gradient(circle, #444c63 1px, transparent 2px);
background-size: 8px 11px;
background-position: top left;
padding: 6px;
}
}
// === SECTION: Message List (Bottom)
&__message-list-container {
position: relative;
}
&__message-list {
margin: 0;
padding: 0;
list-style: none;
max-height: 275px;
}
&__message-item {
position: relative;
margin-bottom: 4px;
color: var(--bg-vanilla-400);
font-family: Geist Mono;
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: var(--bg-vanilla-400);
padding: 3px 12px;
padding-left: 26px;
}
&__message-item::before {
font-family: unset;
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 4px;
border-radius: 50px;
background: var(--bg-slate-400);
}
&__scroll-hint {
position: absolute;
bottom: 10px;
left: 0px;
right: 0px;
margin: auto;
width: fit-content;
display: inline-flex;
padding: 4px 12px 4px 10px;
justify-content: center;
align-items: center;
gap: 3px;
background: var(--bg-slate-200);
border-radius: 20px;
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
0px 66px 18px 0px rgba(0, 0, 0, 0.01), 0px 37px 22px 0px rgba(0, 0, 0, 0.03),
0px 17px 17px 0px rgba(0, 0, 0, 0.04), 0px 4px 9px 0px rgba(0, 0, 0, 0.04);
}
&__scroll-hint-text {
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
.lightMode {
.error-content {
&__error-code {
color: var(--bg-ink-100);
}
&__error-message {
color: var(--bg-ink-400);
}
&__message-item {
color: var(--bg-ink-400);
}
&__message-badge {
&-label-text {
color: var(--bg-ink-400);
}
.key-value-label__value {
color: var(--bg-ink-400);
}
}
&__docs-button {
background: var(--bg-vanilla-100);
color: var(--bg-ink-100);
}
}
}

View File

@ -0,0 +1,98 @@
import './ErrorContent.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import ErrorIcon from 'assets/Error';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { BookOpenText, ChevronsDown } from 'lucide-react';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import APIError from 'types/api/error';
interface ErrorContentProps {
error: APIError;
}
function ErrorContent({ error }: ErrorContentProps): JSX.Element {
const {
url: errorUrl,
errors: errorMessages,
code: errorCode,
message: errorMessage,
} = error.error.error;
return (
<section className="error-content">
{/* Summary Header */}
<section className="error-content__summary-section">
<header className="error-content__summary">
<div className="error-content__summary-left">
<div className="error-content__icon-wrapper">
<ErrorIcon />
</div>
<div className="error-content__summary-text">
<h2 className="error-content__error-code">{errorCode}</h2>
<p className="error-content__error-message">{errorMessage}</p>
</div>
</div>
{errorUrl && (
<div className="error-content__summary-right">
<Button
type="default"
className="error-content__docs-button"
href={errorUrl}
target="_blank"
data-testid="error-docs-button"
>
<BookOpenText size={14} />
Open Docs
</Button>
</div>
)}
</header>
{errorMessages?.length > 0 && (
<div className="error-content__message-badge">
<KeyValueLabel
badgeKey={
<div className="error-content__message-badge-label">
<div className="error-content__message-badge-label-dot" />
<div className="error-content__message-badge-label-text">MESSAGES</div>
</div>
}
badgeValue={errorMessages.length.toString()}
/>
<div className="error-content__message-badge-line" />
</div>
)}
</section>
{/* Detailed Messages */}
<section className="error-content__messages-section">
<div className="error-content__message-list-container">
<OverlayScrollbar>
<ul className="error-content__message-list">
{errorMessages?.map((error) => (
<li className="error-content__message-item" key={error.message}>
{error.message}
</li>
))}
</ul>
</OverlayScrollbar>
{errorMessages?.length > 10 && (
<div className="error-content__scroll-hint">
<ChevronsDown
size={16}
color={Color.BG_VANILLA_100}
className="error-content__scroll-hint-icon"
/>
<span className="error-content__scroll-hint-text">Scroll for more</span>
</div>
)}
</div>
</section>
</section>
);
}
export default ErrorContent;

View File

@ -37,6 +37,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -67,6 +68,7 @@ function HostMetricsDetails({
AppState,
GlobalReducer
>((state) => state.globalTime);
const [searchParams, setSearchParams] = useSearchParams();
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
@ -86,7 +88,9 @@ function HostMetricsDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [selectedView, setSelectedView] = useState<VIEWS>(
(searchParams.get('view') as VIEWS) || VIEWS.METRICS,
);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
@ -149,6 +153,9 @@ function HostMetricsDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
if (host?.hostName) {
setSearchParams({ hostName: host?.hostName, view: e.target.value });
}
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.HostEntity,
view: e.target.value,
@ -313,6 +320,7 @@ function HostMetricsDetails({
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
setSearchParams({});
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);

View File

@ -75,7 +75,6 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}

View File

@ -80,6 +80,7 @@ function Metrics({
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
enableZoom: true,
}),
),
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
@ -115,7 +116,7 @@ function Metrics({
<div className="metrics-header">
<div className="metrics-datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showAutoRefresh
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}

View File

@ -3,6 +3,25 @@
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.log-detail-drawer__title {
display: flex;
justify-content: space-between;
align-items: center;
.log-detail-drawer__title-left {
display: flex;
align-items: center;
gap: 8px;
}
.log-detail-drawer__title-right {
.ant-btn {
display: flex;
align-items: center;
}
}
}
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;

View File

@ -8,6 +8,8 @@ import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import JSONView from 'container/LogDetailedView/JsonView';
@ -22,9 +24,12 @@ import dompurify from 'dompurify';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import {
BarChart2,
Braces,
Compass,
Copy,
Filter,
HardHat,
@ -33,9 +38,12 @@ import {
X,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
@ -77,6 +85,12 @@ function LogDetail({
});
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
@ -119,6 +133,21 @@ function LogDetail({
});
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
urlQuery.set(QueryParams.activeLogId, `"${log?.id}"`);
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
};
// Only show when opened from infra monitoring page
const showOpenInExplorerBtn = useMemo(
() => location.pathname?.includes('/infrastructure-monitoring'),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
if (!log) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
@ -131,10 +160,23 @@ function LogDetail({
width="60%"
maskStyle={{ background: 'none' }}
title={
<>
<div className="log-detail-drawer__title">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</>
</div>
{showOpenInExplorerBtn && (
<div className="log-detail-drawer__title-right">
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
}
placement="right"
// closable

View File

@ -15,7 +15,7 @@ import {
} from 'mocks-server/__mockdata__/alerts';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { testLabelInputAndHelpValue } from './testUtils';
@ -30,6 +30,14 @@ jest.mock('hooks/useNotifications', () => ({
},
})),
}));
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
})),
}));
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
MarkdownRenderer: jest.fn(() => <div>Mocked MarkdownRenderer</div>),
@ -119,7 +127,7 @@ describe('Create Alert Channel', () => {
fireEvent.click(saveButton);
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
});
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
server.use(
@ -151,9 +159,11 @@ describe('Create Alert Channel', () => {
name: 'button_test_channel',
});
act(() => {
fireEvent.click(testButton);
});
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
});
});
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {

View File

@ -16,6 +16,7 @@ import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error';
@ -42,6 +43,7 @@ function CreateAlertChannels({
}: CreateAlertChannelsProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('channels');
const { showErrorModal } = useErrorModal();
const [formInstance] = Form.useForm();
@ -145,15 +147,12 @@ function CreateAlertChannels({
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [prepareSlackRequest, t, notifications]);
}, [prepareSlackRequest, notifications, t, showErrorModal]);
const prepareWebhookRequest = useCallback(() => {
// initial api request without auth params
@ -202,15 +201,12 @@ function CreateAlertChannels({
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [prepareWebhookRequest, t, notifications]);
}, [prepareWebhookRequest, notifications, t, showErrorModal]);
const preparePagerRequest = useCallback(() => {
const validationError = ValidatePagerChannel(selectedConfig as PagerChannel);
@ -254,15 +250,12 @@ function CreateAlertChannels({
}
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [t, notifications, preparePagerRequest]);
}, [preparePagerRequest, t, notifications, showErrorModal]);
const prepareOpsgenieRequest = useCallback(
() => ({
@ -287,15 +280,12 @@ function CreateAlertChannels({
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [prepareOpsgenieRequest, t, notifications]);
}, [prepareOpsgenieRequest, notifications, t, showErrorModal]);
const prepareEmailRequest = useCallback(
() => ({
@ -320,15 +310,12 @@ function CreateAlertChannels({
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [prepareEmailRequest, t, notifications]);
}, [prepareEmailRequest, notifications, t, showErrorModal]);
const prepareMsTeamsRequest = useCallback(
() => ({
@ -353,15 +340,12 @@ function CreateAlertChannels({
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [prepareMsTeamsRequest, t, notifications]);
}, [prepareMsTeamsRequest, notifications, t, showErrorModal]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
@ -459,10 +443,8 @@ function CreateAlertChannels({
status: 'Test success',
});
} catch (error) {
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
showErrorModal(error as APIError);
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig?.send_resolved,

View File

@ -14,6 +14,8 @@ function ExplorerOptionWrapper({
isLoading,
onExport,
sourcepage,
isOneChartPerQuery,
splitedQueries,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
@ -32,6 +34,8 @@ function ExplorerOptionWrapper({
sourcepage={sourcepage}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery}
splitedQueries={splitedQueries}
/>
);
}

View File

@ -8,6 +8,21 @@
display: flex;
gap: 16px;
background-color: transparent;
.multi-alert-button,
.multi-dashboard-button {
min-width: 130px;
.ant-select-selector {
.ant-select-selection-placeholder {
margin-left: 0 !important;
}
.ant-select-arrow {
display: none !important;
}
}
}
}
.hide-update {

View File

@ -90,6 +90,8 @@ function ExplorerOptions({
sourcepage,
isExplorerOptionHidden = false,
setIsExplorerOptionHidden,
isOneChartPerQuery = false,
splitedQueries = [],
}: ExplorerOptionsProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
@ -99,6 +101,8 @@ function ExplorerOptions({
const history = useHistory();
const ref = useRef<RefSelectProps>(null);
const isDarkMode = useIsDarkMode();
const [queryToExport, setQueryToExport] = useState<Query | null>(null);
const isLogsExplorer = sourcepage === DataSource.LOGS;
const isMetricsExplorer = sourcepage === DataSource.METRICS;
@ -149,22 +153,28 @@ function ExplorerOptions({
const { user } = useAppContext();
const handleConditionalQueryModification = useCallback((): string => {
const handleConditionalQueryModification = useCallback(
(defaultQuery: Query | null): string => {
const queryToUse = defaultQuery || query;
if (
query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP
queryToUse?.builder?.queryData?.[0]?.aggregateOperator !==
StringOperators.NOOP
) {
return JSON.stringify(query);
return JSON.stringify(queryToUse);
}
// Modify aggregateOperator to count, as noop is not supported in alerts
const modifiedQuery = cloneDeep(query);
const modifiedQuery = cloneDeep(queryToUse);
modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT;
return JSON.stringify(modifiedQuery);
}, [query]);
},
[query],
);
const onCreateAlertsHandler = useCallback(() => {
const onCreateAlertsHandler = useCallback(
(defaultQuery: Query | null) => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
panelType,
@ -179,21 +189,26 @@ function ExplorerOptions({
});
}
const stringifiedQuery = handleConditionalQueryModification();
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
stringifiedQuery,
)}`,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleConditionalQueryModification, history]);
[handleConditionalQueryModification, history],
);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
if (isOneChartPerQuery) {
setQueryToExport(null);
}
};
const onAddToDashboard = (): void => {
const onAddToDashboard = useCallback((): void => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Add to dashboard clicked', {
panelType,
@ -208,7 +223,7 @@ function ExplorerOptions({
});
}
setIsExport(true);
};
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
const {
data: viewsData,
@ -616,6 +631,120 @@ function ExplorerOptions({
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
}, [isLogsExplorer, isMetricsExplorer]);
const getQueryName = (query: Query): string => {
if (query.builder.queryFormulas.length > 0) {
return `Formula ${query.builder.queryFormulas[0].queryName}`;
}
return `Query ${query.builder.queryData[0].queryName}`;
};
const alertButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
disabled={disabled}
shape="round"
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
);
return (
<Select
disabled={disabled}
className="multi-alert-button"
placeholder={selectLabel}
value={selectLabel}
suffixIcon={null}
onSelect={(e): void => {
const selectedQuery = splitedQueries.find(
(query) => query.id === ((e as unknown) as string),
);
if (selectedQuery) {
onCreateAlertsHandler(selectedQuery);
}
}}
>
{splitedQueries.map((splittedQuery) => (
<Select.Option key={splittedQuery.id} value={splittedQuery.id}>
{getQueryName(splittedQuery)}
</Select.Option>
))}
</Select>
);
}
return (
<Button
disabled={disabled}
shape="round"
onClick={(): void => onCreateAlertsHandler(query)}
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
);
}, [
disabled,
isOneChartPerQuery,
onCreateAlertsHandler,
query,
splitedQueries,
]);
const dashboardButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
);
return (
<Select
disabled={disabled}
className="multi-dashboard-button"
placeholder={selectLabel}
value={selectLabel}
suffixIcon={null}
onSelect={(e): void => {
const selectedQuery = splitedQueries.find(
(query) => query.id === ((e as unknown) as string),
);
if (selectedQuery) {
setQueryToExport(() => {
onAddToDashboard();
return selectedQuery;
});
}
}}
>
{/* eslint-disable-next-line sonarjs/no-identical-functions */}
{splitedQueries.map((splittedQuery) => (
<Select.Option key={splittedQuery.id} value={splittedQuery.id}>
{getQueryName(splittedQuery)}
</Select.Option>
))}
</Select>
);
}
return (
<Button
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
);
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
return (
<div className="explorer-options-container">
{
@ -719,24 +848,8 @@ function ExplorerOptions({
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Button
disabled={disabled}
shape="round"
onClick={onCreateAlertsHandler}
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
<Button
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
{alertButton}
{dashboardButton}
</div>
<div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */}
@ -818,9 +931,15 @@ function ExplorerOptions({
destroyOnClose
>
<ExportPanelContainer
query={query}
query={isOneChartPerQuery ? queryToExport : query}
isLoading={isLoading}
onExport={onExport}
onExport={(dashboard, isNewDashboard): void => {
if (isOneChartPerQuery && queryToExport) {
onExport(dashboard, isNewDashboard, queryToExport);
} else {
onExport(dashboard, isNewDashboard);
}
}}
/>
</Modal>
</div>
@ -829,18 +948,26 @@ function ExplorerOptions({
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
onExport: (
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void;
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
isOneChartPerQuery?: boolean;
splitedQueries?: Query[];
}
ExplorerOptions.defaultProps = {
isLoading: false,
isExplorerOptionHidden: false,
setIsExplorerOptionHidden: undefined,
isOneChartPerQuery: false,
splitedQueries: [],
};
export default ExplorerOptions;

View File

@ -142,6 +142,7 @@ function ChartPreview({
params: {
allowSelectedIntervalForStepGen,
},
originalGraphType: graphType,
},
alertDef?.version || DEFAULT_ENTITY_VERSION,
{

View File

@ -94,6 +94,7 @@ function FullView({
variables: getDashboardVariables(selectedDashboard?.data.variables),
fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
originalGraphType: widget?.panelTypes,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;

View File

@ -208,6 +208,7 @@ function GridCardGraph({
: globalSelectedInterval,
start: customTimeRange?.startTime || start,
end: customTimeRange?.endTime || end,
originalGraphType: widget?.panelTypes,
},
version || DEFAULT_ENTITY_VERSION,
{

View File

@ -0,0 +1,228 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { getStepIntervalPoints, updateStepInterval } from '../utils';
describe('GridCardLayout Utils', () => {
describe('getStepIntervalPoints', () => {
it('should return 60 points for duration <= 1 hour', () => {
// 30 minutes in milliseconds
const start = Date.now();
const end = start + 30 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(60);
});
it('should return 60 points for exactly 1 hour', () => {
// 1 hour in milliseconds
const start = Date.now();
const end = start + 60 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(60);
});
it('should return 120 points for duration <= 3 hours', () => {
// 2 hours in milliseconds
const start = Date.now();
const end = start + 2 * 60 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(120);
});
it('should return 120 points for exactly 3 hours', () => {
// 3 hours in milliseconds
const start = Date.now();
const end = start + 3 * 60 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(120);
});
it('should return 180 points for duration <= 5 hours', () => {
// 4 hours in milliseconds
const start = Date.now();
const end = start + 4 * 60 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(180);
});
it('should return 180 points for exactly 5 hours', () => {
// 5 hours in milliseconds
const start = Date.now();
const end = start + 5 * 60 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(180);
});
it('should calculate dynamic interval for duration > 5 hours', () => {
// 10 hours in milliseconds
const start = Date.now();
const end = start + 10 * 60 * 60 * 1000;
const result = getStepIntervalPoints(start, end);
// For 10 hours (600 minutes), interval should be ceil(600/80) = 8, rounded to 10, then * 60 = 600
expect(result).toBe(600);
});
it('should handle very long durations correctly', () => {
// 7 days in milliseconds
const start = Date.now();
const end = start + 7 * 24 * 60 * 60 * 1000;
const result = getStepIntervalPoints(start, end);
// For 7 days (10080 minutes), interval should be ceil(10080/80) = 126, rounded to 130, then * 60 = 7800
expect(result).toBe(7800);
});
it('should round up to nearest multiple of 5 minutes', () => {
// 12 hours in milliseconds
const start = Date.now();
const end = start + 12 * 60 * 60 * 1000;
const result = getStepIntervalPoints(start, end);
// For 12 hours (720 minutes), interval should be ceil(720/80) = 9, rounded to 10, then * 60 = 600
expect(result).toBe(600);
});
it('should handle edge case with very small duration', () => {
// 1 minute in milliseconds
const start = Date.now();
const end = start + 1 * 60 * 1000;
expect(getStepIntervalPoints(start, end)).toBe(60);
});
it('should handle zero duration', () => {
const start = Date.now();
const end = start;
expect(getStepIntervalPoints(start, end)).toBe(60);
});
});
describe('updateStepInterval', () => {
const mockQuery: Query = {
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [
{
stepInterval: 30,
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
queryName: 'A',
aggregateAttribute: { key: 'cpu_usage', type: 'Gauge' },
timeAggregation: 'avg',
spaceAggregation: 'avg',
functions: [],
filters: { items: [], op: 'AND' },
expression: 'A',
disabled: false,
having: [],
groupBy: [],
orderBy: [],
limit: null,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
legend: '',
},
],
queryFormulas: [],
},
clickhouse_sql: [],
promql: [],
id: 'test-query',
};
it('should update stepInterval based on time range', () => {
// 2 hours duration
const minTime = Date.now();
const maxTime = minTime + 2 * 60 * 60 * 1000;
const result = updateStepInterval(mockQuery, minTime, maxTime);
expect(result.builder.queryData[0].stepInterval).toBe(120);
});
it('should preserve other query properties', () => {
const minTime = Date.now();
const maxTime = minTime + 1 * 60 * 60 * 1000;
const result = updateStepInterval(mockQuery, minTime, maxTime);
expect(result.builder.queryData[0].aggregateOperator).toBe('avg');
expect(result.builder.queryData[0].queryName).toBe('A');
expect(result.builder.queryData[0].dataSource).toBe('metrics');
});
it('should handle multiple queryData items', () => {
const multiQueryMock: Query = {
...mockQuery,
builder: {
queryData: [
...mockQuery.builder.queryData,
{
...mockQuery.builder.queryData[0],
queryName: 'B',
stepInterval: 45,
},
],
queryFormulas: [],
},
};
const minTime = Date.now();
const maxTime = minTime + 4 * 60 * 60 * 1000;
const result = updateStepInterval(multiQueryMock, minTime, maxTime);
expect(result.builder.queryData).toHaveLength(2);
expect(result.builder.queryData[0].stepInterval).toBe(180);
expect(result.builder.queryData[1].stepInterval).toBe(180);
});
it('should use calculated stepInterval when original is undefined', () => {
const queryWithUndefinedStep: Query = {
...mockQuery,
builder: {
queryData: [
{
...mockQuery.builder.queryData[0],
stepInterval: undefined as any,
},
],
queryFormulas: [],
},
};
const minTime = Date.now();
const maxTime = minTime + 1 * 60 * 60 * 1000;
const result = updateStepInterval(queryWithUndefinedStep, minTime, maxTime);
expect(result.builder.queryData[0].stepInterval).toBe(60);
});
it('should fallback to 60 when calculated stepInterval is 0', () => {
const minTime = Date.now();
const maxTime = minTime; // Same time = 0 duration
const result = updateStepInterval(mockQuery, minTime, maxTime);
expect(result.builder.queryData[0].stepInterval).toBe(60);
});
it('should handle very large time ranges', () => {
const minTime = Date.now();
const maxTime = minTime + 30 * 24 * 60 * 60 * 1000; // 30 days
const result = updateStepInterval(mockQuery, minTime, maxTime);
// Should calculate appropriate interval for 30 days
expect(result.builder.queryData[0].stepInterval).toBeGreaterThan(180);
});
});
});

View File

@ -2,6 +2,7 @@ import { FORMULA_REGEXP } from 'constants/regExp';
import { isEmpty, isEqual } from 'lodash-es';
import { Layout } from 'react-grid-layout';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
layout.map((obj) =>
@ -51,3 +52,63 @@ export const hasColumnWidthsChanged = (
return !isEqual(newWidths, existingWidths);
});
};
/**
* Calculates the step interval in uPlot points (1 minute = 60 points)
* based on the time duration between two timestamps in nanoseconds.
*
* Conversion logic:
* - <= 1 hr 1 min (60 points)
* - <= 3 hr 2 min (120 points)
* - <= 5 hr 3 min (180 points)
* - > 5 hr max 80 bars, ceil((end-start)/80), rounded to nearest multiple of 5 min
*
* @param startNano - start time in nanoseconds
* @param endNano - end time in nanoseconds
* @returns stepInterval in uPlot points
*/
export function getStepIntervalPoints(
startNano: number,
endNano: number,
): number {
const startMs = startNano;
const endMs = endNano;
const durationMs = endMs - startMs;
const durationMin = durationMs / (60 * 1000); // convert to minutes
if (durationMin <= 60) {
return 60; // 1 min
}
if (durationMin <= 180) {
return 120; // 2 min
}
if (durationMin <= 300) {
return 180; // 3 min
}
const totalPoints = Math.ceil(durationMs / (1000 * 60)); // total minutes
const interval = Math.ceil(totalPoints / 80); // at most 80 bars
const roundedInterval = Math.ceil(interval / 5) * 5; // round up to nearest 5
return roundedInterval * 60; // convert min to points
}
export function updateStepInterval(
query: Query,
minTime: number,
maxTime: number,
): Query {
const stepIntervalPoints = getStepIntervalPoints(minTime, maxTime);
return {
...query,
builder: {
...query.builder,
queryData: [
...query.builder.queryData.map((queryData) => ({
...queryData,
stepInterval: stepIntervalPoints || queryData.stepInterval || 60,
})),
],
},
};
}

View File

@ -8,6 +8,11 @@ import HostMetricDetail from 'components/HostMetricsDetail';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import {
getFiltersFromParams,
getOrderByFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@ -15,6 +20,7 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { Filter } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
@ -27,20 +33,51 @@ function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
);
if (!filters) {
return {
items: [],
op: 'and',
};
}
return filters;
});
const [showFilters, setShowFilters] = useState<boolean>(true);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams));
const [selectedHostName, setSelectedHostName] = useState<string | null>(null);
const handleOrderByChange = (
orderBy: {
columnName: string;
order: 'asc' | 'desc';
} | null,
): void => {
setOrderBy(orderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(orderBy),
});
};
const [selectedHostName, setSelectedHostName] = useState<string | null>(() => {
const hostName = searchParams.get('hostName');
return hostName || null;
});
const handleHostClick = (hostName: string): void => {
setSelectedHostName(hostName);
setSearchParams({ ...searchParams, hostName });
};
const { pageSize, setPageSize } = usePageSize('hosts');
@ -82,6 +119,10 @@ function HostsList(): JSX.Element {
const isNewFilterAdded = value.items.length !== filters.items.length;
setFilters(value);
handleChangeQueryData('filters', value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
});
if (isNewFilterAdded) {
setCurrentPage(1);
@ -161,7 +202,10 @@ function HostsList(): JSX.Element {
</Button>
</div>
)}
<HostsListControls handleFiltersChange={handleFiltersChange} />
<HostsListControls
filters={filters}
handleFiltersChange={handleFiltersChange}
/>
</div>
<HostsListTable
isLoading={isLoading}
@ -172,10 +216,10 @@ function HostsList(): JSX.Element {
filters={filters}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
setSelectedHostName={setSelectedHostName}
onHostClick={handleHostClick}
pageSize={pageSize}
setPageSize={setPageSize}
setOrderBy={setOrderBy}
setOrderBy={handleOrderByChange}
/>
</div>
</div>

View File

@ -10,8 +10,10 @@ import { DataSource } from 'types/common/queryBuilder';
function HostsListControls({
handleFiltersChange,
filters,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
}): JSX.Element {
const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo(
@ -26,11 +28,12 @@ function HostsListControls({
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters,
},
],
},
}),
[currentQuery],
[currentQuery, filters],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;

View File

@ -27,7 +27,7 @@ export default function HostsListTable({
tableData: data,
hostMetricsData,
filters,
setSelectedHostName,
onHostClick,
currentPage,
setCurrentPage,
pageSize,
@ -77,7 +77,7 @@ export default function HostsListTable({
);
const handleRowClick = (record: HostRowData): void => {
setSelectedHostName(record.hostName);
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
page: InfraMonitoringEvents.ListPage,

View File

@ -41,16 +41,13 @@ export interface HostsListTableProps {
| undefined;
hostMetricsData: HostData[];
filters: TagFilter;
setSelectedHostName: Dispatch<SetStateAction<string | null>>;
onHostClick: (hostName: string) => void;
currentPage: number;
setCurrentPage: Dispatch<SetStateAction<number>>;
pageSize: number;
setOrderBy: Dispatch<
SetStateAction<{
columnName: string;
order: 'asc' | 'desc';
} | null>
>;
setOrderBy: (
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
) => void;
setPageSize: (pageSize: number) => void;
}

View File

@ -14,8 +14,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
@ -34,6 +40,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -82,11 +89,27 @@ function ClusterDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -103,12 +126,18 @@ function ClusterDetails({
value: cluster?.meta.k8s_cluster_name || '',
},
],
}),
[cluster?.meta.k8s_cluster_name],
);
};
}, [cluster?.meta.k8s_cluster_name, searchParams]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -138,9 +167,8 @@ function ClusterDetails({
value: cluster?.meta.k8s_cluster_name || '',
},
],
}),
[cluster?.meta.k8s_cluster_name],
);
};
}, [cluster?.meta.k8s_cluster_name, searchParams]);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -181,6 +209,13 @@ function ClusterDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -220,7 +255,7 @@ function ClusterDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
@ -240,7 +275,7 @@ function ClusterDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -250,6 +285,16 @@ function ClusterDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -257,7 +302,7 @@ function ClusterDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
@ -272,7 +317,7 @@ function ClusterDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -283,6 +328,16 @@ function ClusterDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -290,7 +345,7 @@ function ClusterDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const clusterKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -308,7 +363,7 @@ function ClusterDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -322,6 +377,16 @@ function ClusterDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -23,11 +23,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -59,19 +62,36 @@ function K8sClustersList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>({ columnName: 'cpu', order: 'desc' });
} | null>(() => getOrderByFromParams(searchParams, false));
const [selectedClusterName, setselectedClusterName] = useState<string | null>(
null,
() => {
const clusterName = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
);
if (clusterName) {
return clusterName;
}
return null;
},
);
const { pageSize, setPageSize } = usePageSize(K8sCategory.CLUSTERS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -258,15 +278,26 @@ function K8sClustersList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -322,6 +353,10 @@ function K8sClustersList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME]: record.clusterUID,
});
} else {
handleGroupByRowClick(record);
}
@ -348,6 +383,11 @@ function K8sClustersList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -372,7 +412,9 @@ function K8sClustersList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedClusterName(record.clusterUID),
onClick: (): void => {
setselectedClusterName(record.clusterUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -436,6 +478,20 @@ function K8sClustersList({
const handleCloseClusterDetail = (): void => {
setselectedClusterName(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -457,6 +513,10 @@ function K8sClustersList({
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -464,7 +524,7 @@ function K8sClustersList({
category: InfraMonitoringEvents.Cluster,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -13,7 +13,11 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import {
CustomTimeType,
Time,
@ -31,6 +35,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -83,11 +88,27 @@ function DaemonSetDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -117,12 +138,22 @@ function DaemonSetDetails({
value: daemonSet?.meta.k8s_namespace_name || '',
},
],
}),
[daemonSet?.meta.k8s_daemonset_name, daemonSet?.meta.k8s_namespace_name],
);
};
}, [
daemonSet?.meta.k8s_daemonset_name,
daemonSet?.meta.k8s_namespace_name,
searchParams,
]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -152,9 +183,8 @@ function DaemonSetDetails({
value: daemonSet?.meta.k8s_daemonset_name || '',
},
],
}),
[daemonSet?.meta.k8s_daemonset_name],
);
};
}, [daemonSet?.meta.k8s_daemonset_name, searchParams]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -195,6 +225,13 @@ function DaemonSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -234,7 +271,7 @@ function DaemonSetDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -257,7 +294,7 @@ function DaemonSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -265,6 +302,15 @@ function DaemonSetDetails({
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -272,7 +318,7 @@ function DaemonSetDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -289,7 +335,7 @@ function DaemonSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -298,6 +344,16 @@ function DaemonSetDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -305,7 +361,7 @@ function DaemonSetDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const daemonSetKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -323,7 +379,7 @@ function DaemonSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
daemonSetKindFilter,
@ -335,6 +391,16 @@ function DaemonSetDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -59,21 +62,38 @@ function K8sDaemonSetsList({
);
const [currentPage, setCurrentPage] = useState(1);
const [searchParams, setSearchParams] = useSearchParams();
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedDaemonSetUID, setselectedDaemonSetUID] = useState<
const [selectedDaemonSetUID, setSelectedDaemonSetUID] = useState<
string | null
>(null);
>(() => {
const daemonSetUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
);
if (daemonSetUID) {
return daemonSetUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.DAEMONSETS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -262,15 +282,26 @@ function K8sDaemonSetsList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -329,7 +360,11 @@ function K8sDaemonSetsList({
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDaemonSetUID(record.daemonsetUID);
setSelectedDaemonSetUID(record.daemonsetUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID]: record.daemonsetUID,
});
} else {
handleGroupByRowClick(record);
}
@ -356,6 +391,11 @@ function K8sDaemonSetsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -380,7 +420,9 @@ function K8sDaemonSetsList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedDaemonSetUID(record.daemonsetUID),
onClick: (): void => {
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -443,7 +485,21 @@ function K8sDaemonSetsList({
};
const handleCloseDaemonSetDetail = (): void => {
setselectedDaemonSetUID(null);
setSelectedDaemonSetUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -464,6 +520,10 @@ function K8sDaemonSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@ -472,7 +532,7 @@ function K8sDaemonSetsList({
category: InfraMonitoringEvents.DaemonSet,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -14,8 +14,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
@ -34,6 +40,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -85,11 +92,27 @@ function DeploymentDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -119,12 +142,22 @@ function DeploymentDetails({
value: deployment?.meta.k8s_namespace_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_name, deployment?.meta.k8s_namespace_name],
);
};
}, [
deployment?.meta.k8s_deployment_name,
deployment?.meta.k8s_namespace_name,
searchParams,
]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -154,9 +187,8 @@ function DeploymentDetails({
value: deployment?.meta.k8s_deployment_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_name],
);
};
}, [deployment?.meta.k8s_deployment_name, searchParams]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -197,6 +229,13 @@ function DeploymentDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -236,7 +275,7 @@ function DeploymentDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -259,7 +298,7 @@ function DeploymentDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -269,6 +308,16 @@ function DeploymentDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -276,7 +325,7 @@ function DeploymentDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -293,7 +342,7 @@ function DeploymentDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -304,6 +353,16 @@ function DeploymentDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -311,7 +370,7 @@ function DeploymentDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const deploymentKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -329,7 +388,7 @@ function DeploymentDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -343,6 +402,16 @@ function DeploymentDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -61,19 +64,36 @@ function K8sDeploymentsList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedDeploymentUID, setselectedDeploymentUID] = useState<
string | null
>(null);
>(() => {
const deploymentUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
);
if (deploymentUID) {
return deploymentUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.DEPLOYMENTS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -264,15 +284,26 @@ function K8sDeploymentsList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -333,6 +364,10 @@ function K8sDeploymentsList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID]: record.deploymentUID,
});
} else {
handleGroupByRowClick(record);
}
@ -359,6 +394,11 @@ function K8sDeploymentsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -383,7 +423,9 @@ function K8sDeploymentsList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedDeploymentUID(record.deploymentUID),
onClick: (): void => {
setselectedDeploymentUID(record.deploymentUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -447,6 +489,20 @@ function K8sDeploymentsList({
const handleCloseDeploymentDetail = (): void => {
setselectedDeploymentUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -468,6 +524,10 @@ function K8sDeploymentsList({
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@ -476,7 +536,7 @@ function K8sDeploymentsList({
category: InfraMonitoringEvents.Deployment,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -3,6 +3,7 @@ import './entityEvents.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Table, TableColumnsType } from 'antd';
import { VIEWS } from 'components/HostMetricsDetail/constants';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { EventContents } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
@ -28,6 +29,7 @@ import { DataSource } from 'types/common/queryBuilder';
import {
EntityDetailsEmptyContainer,
getEntityEventsOrLogsQueryPayload,
QUERY_KEYS,
} from '../utils';
interface EventDataType {
@ -55,7 +57,10 @@ interface IEntityEventsProps {
startTime: number;
endTime: number;
};
handleChangeEventFilters: (filters: IBuilderQuery['filters']) => void;
handleChangeEventFilters: (
filters: IBuilderQuery['filters'],
view: VIEWS,
) => void;
filters: IBuilderQuery['filters'];
isModalTimeSelection: boolean;
handleTimeChange: (
@ -103,14 +108,18 @@ export default function Events({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
items: filters.items.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
op: 'AND',
},
},
],
},
}),
[currentQuery],
[currentQuery, filters],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@ -243,7 +252,7 @@ export default function Events({
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeEventFilters}
onChange={(value): void => handleChangeEventFilters(value, VIEWS.EVENTS)}
disableNavigationShortcuts
/>
)}

View File

@ -65,7 +65,6 @@ function EntityLogs({
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}

View File

@ -1,5 +1,6 @@
import './entityLogs.styles.scss';
import { VIEWS } from 'components/HostMetricsDetail/constants';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
@ -25,7 +26,7 @@ interface Props {
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters'], view: VIEWS) => void;
logFilters: IBuilderQuery['filters'];
selectedInterval: Time;
queryKey: string;
@ -78,7 +79,7 @@ function EntityLogsDetailedView({
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeLogFilters}
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
disableNavigationShortcuts
/>
)}

View File

@ -110,6 +110,7 @@ function EntityMetrics<T>({
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
enableZoom: true,
});
}),
[
@ -162,7 +163,7 @@ function EntityMetrics<T>({
<div className="metrics-header">
<div className="metrics-datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showAutoRefresh
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}

View File

@ -1,6 +1,7 @@
import './entityTraces.styles.scss';
import logEvent from 'api/common/logEvent';
import { VIEWS } from 'components/HostMetricsDetail/constants';
import { getListColumns } from 'components/HostMetricsDetail/HostMetricTraces/utils';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@ -43,7 +44,10 @@ interface Props {
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
handleChangeTracesFilters: (
value: IBuilderQuery['filters'],
view: VIEWS,
) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
queryKey: string;
@ -164,7 +168,9 @@ function EntityTraces({
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeTracesFilters}
onChange={(value): void =>
handleChangeTracesFilters(value, VIEWS.TRACES)
}
disableNavigationShortcuts
/>
)}

View File

@ -23,6 +23,7 @@ import {
} from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import K8sClustersList from './Clusters/K8sClustersList';
@ -30,6 +31,7 @@ import {
ClustersQuickFiltersConfig,
DaemonSetsQuickFiltersConfig,
DeploymentsQuickFiltersConfig,
INFRA_MONITORING_K8S_PARAMS_KEYS,
JobsQuickFiltersConfig,
K8sCategories,
NamespaceQuickFiltersConfig,
@ -50,7 +52,14 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [selectedCategory, setSelectedCategory] = useState(K8sCategories.PODS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedCategory, setSelectedCategory] = useState(() => {
const category = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY);
if (category) {
return category as keyof typeof K8sCategories;
}
return K8sCategories.PODS;
});
const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1);
const { currentQuery } = useQueryBuilder();
@ -70,6 +79,12 @@ export default function InfraMonitoringK8s(): JSX.Element {
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
handleChangeQueryData('filters', query.builder.queryData[0].filters);
setQuickFiltersLastUpdated(Date.now());
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(
query.builder.queryData[0].filters,
),
});
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
@ -295,6 +310,9 @@ export default function InfraMonitoringK8s(): JSX.Element {
const handleCategoryChange = (key: string | string[]): void => {
if (Array.isArray(key) && key.length > 0) {
setSelectedCategory(key[0] as string);
setSearchParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY]: key[0] as string,
});
// Reset filters
handleChangeQueryData('filters', { items: [], op: 'and' });
}

View File

@ -13,7 +13,11 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import {
CustomTimeType,
Time,
@ -31,6 +35,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -80,11 +85,27 @@ function JobDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -114,12 +135,18 @@ function JobDetails({
value: job?.meta.k8s_namespace_name || '',
},
],
}),
[job?.meta.k8s_job_name, job?.meta.k8s_namespace_name],
);
};
}, [job?.meta.k8s_job_name, job?.meta.k8s_namespace_name, searchParams]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -149,9 +176,8 @@ function JobDetails({
value: job?.meta.k8s_job_name || '',
},
],
}),
[job?.meta.k8s_job_name],
);
};
}, [job?.meta.k8s_job_name, searchParams]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -192,6 +218,13 @@ function JobDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -231,7 +264,7 @@ function JobDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -253,7 +286,7 @@ function JobDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -261,6 +294,16 @@ function JobDetails({
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -268,7 +311,7 @@ function JobDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -285,7 +328,7 @@ function JobDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -294,6 +337,16 @@ function JobDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -301,7 +354,7 @@ function JobDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const jobKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -319,7 +372,7 @@ function JobDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
jobKindFilter,
@ -331,6 +384,16 @@ function JobDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -61,17 +64,32 @@ function K8sJobsList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedJobUID, setselectedJobUID] = useState<string | null>(null);
const [selectedJobUID, setselectedJobUID] = useState<string | null>(() => {
const jobUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID);
if (jobUID) {
return jobUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.JOBS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [selectedRowData, setSelectedRowData] = useState<K8sJobsRowData | null>(
null,
@ -251,15 +269,26 @@ function K8sJobsList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -306,6 +335,10 @@ function K8sJobsList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID]: record.jobUID,
});
} else {
handleGroupByRowClick(record);
}
@ -332,6 +365,11 @@ function K8sJobsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -356,7 +394,9 @@ function K8sJobsList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedJobUID(record.jobUID),
onClick: (): void => {
setselectedJobUID(record.jobUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -420,6 +460,20 @@ function K8sJobsList({
const handleCloseJobDetail = (): void => {
setselectedJobUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -441,6 +495,10 @@ function K8sJobsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -448,7 +506,7 @@ function K8sJobsList({
category: InfraMonitoringEvents.Job,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -7,11 +7,12 @@ import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearc
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Filter, SlidersHorizontal } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { K8sCategory } from './constants';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
import { IEntityColumn } from './utils';
@ -47,11 +48,19 @@ function K8sHeader({
entity,
}: K8sHeaderProps): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo(
() => ({
const updatedCurrentQuery = useMemo(() => {
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
const decoded = decodeURIComponent(urlFilters);
const parsed = JSON.parse(decoded);
filters = parsed;
}
return {
...currentQuery,
builder: {
...currentQuery.builder,
@ -62,20 +71,24 @@ function K8sHeader({
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters,
},
],
},
}),
[currentQuery],
);
};
}, [currentQuery, searchParams]);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleFiltersChange(value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
});
},
[handleFiltersChange],
[handleFiltersChange, searchParams, setSearchParams],
);
return (

View File

@ -23,11 +23,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -60,19 +63,36 @@ function K8sNamespacesList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedNamespaceUID, setselectedNamespaceUID] = useState<
string | null
>(null);
>(() => {
const namespaceUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
);
if (namespaceUID) {
return namespaceUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.NAMESPACES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -261,15 +281,26 @@ function K8sNamespacesList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -330,6 +361,10 @@ function K8sNamespacesList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID]: record.namespaceUID,
});
} else {
handleGroupByRowClick(record);
}
@ -356,6 +391,11 @@ function K8sNamespacesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -380,7 +420,9 @@ function K8sNamespacesList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedNamespaceUID(record.namespaceUID),
onClick: (): void => {
setselectedNamespaceUID(record.namespaceUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -444,6 +486,20 @@ function K8sNamespacesList({
const handleCloseNamespaceDetail = (): void => {
setselectedNamespaceUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -466,6 +522,10 @@ function K8sNamespacesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -473,7 +533,7 @@ function K8sNamespacesList({
category: InfraMonitoringEvents.Namespace,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -14,7 +14,11 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
@ -33,6 +37,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -84,11 +89,27 @@ function NamespaceDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -105,12 +126,18 @@ function NamespaceDetails({
value: namespace?.namespaceName || '',
},
],
}),
[namespace?.namespaceName],
);
};
}, [namespace?.namespaceName, searchParams]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -140,9 +167,8 @@ function NamespaceDetails({
value: namespace?.namespaceName || '',
},
],
}),
[namespace?.namespaceName],
);
};
}, [namespace?.namespaceName, searchParams]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -183,6 +209,13 @@ function NamespaceDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -222,7 +255,7 @@ function NamespaceDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
@ -244,7 +277,7 @@ function NamespaceDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -252,6 +285,17 @@ function NamespaceDetails({
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -259,7 +303,7 @@ function NamespaceDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
@ -276,7 +320,7 @@ function NamespaceDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -285,6 +329,16 @@ function NamespaceDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -292,7 +346,7 @@ function NamespaceDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const namespaceKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -310,7 +364,7 @@ function NamespaceDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
namespaceKindFilter,
@ -322,6 +376,16 @@ function NamespaceDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -23,11 +23,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -60,17 +63,32 @@ function K8sNodesList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>({ columnName: 'cpu', order: 'desc' });
} | null>(() => getOrderByFromParams(searchParams, false));
const [selectedNodeUID, setselectedNodeUID] = useState<string | null>(null);
const [selectedNodeUID, setSelectedNodeUID] = useState<string | null>(() => {
const nodeUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID);
if (nodeUID) {
return nodeUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.NODES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [selectedRowData, setSelectedRowData] = useState<K8sNodesRowData | null>(
null,
@ -250,15 +268,26 @@ function K8sNodesList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -307,7 +336,11 @@ function K8sNodesList({
const handleRowClick = (record: K8sNodesRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNodeUID(record.nodeUID);
setSelectedNodeUID(record.nodeUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID]: record.nodeUID,
});
} else {
handleGroupByRowClick(record);
}
@ -334,6 +367,11 @@ function K8sNodesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -359,7 +397,9 @@ function K8sNodesList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedNodeUID(record.nodeUID),
onClick: (): void => {
setSelectedNodeUID(record.nodeUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -422,7 +462,21 @@ function K8sNodesList({
};
const handleCloseNodeDetail = (): void => {
setselectedNodeUID(null);
setSelectedNodeUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -444,6 +498,10 @@ function K8sNodesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -451,7 +509,7 @@ function K8sNodesList({
category: InfraMonitoringEvents.Node,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -14,8 +14,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
import {
CustomTimeType,
@ -34,6 +40,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -82,11 +89,27 @@ function NodeDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -103,12 +126,18 @@ function NodeDetails({
value: node?.meta.k8s_node_name || '',
},
],
}),
[node?.meta.k8s_node_name],
);
};
}, [node?.meta.k8s_node_name, searchParams]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -138,9 +167,8 @@ function NodeDetails({
value: node?.meta.k8s_node_name || '',
},
],
}),
[node?.meta.k8s_node_name],
);
};
}, [node?.meta.k8s_node_name, searchParams]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -181,6 +209,13 @@ function NodeDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -220,7 +255,7 @@ function NodeDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
@ -242,7 +277,7 @@ function NodeDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -252,6 +287,16 @@ function NodeDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -259,7 +304,7 @@ function NodeDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
@ -276,7 +321,7 @@ function NodeDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -287,6 +332,16 @@ function NodeDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -294,7 +349,7 @@ function NodeDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const nodeKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -312,7 +367,7 @@ function NodeDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
nodeKindFilter,
@ -324,6 +379,16 @@ function NodeDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -59,6 +62,7 @@ function K8sPodsList({
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(1);
@ -68,7 +72,15 @@ function K8sPodsList({
defaultAvailableColumns,
);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [selectedRowData, setSelectedRowData] = useState<K8sPodsRowData | null>(
null,
@ -134,9 +146,15 @@ function K8sPodsList({
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>({ columnName: 'cpu', order: 'desc' });
} | null>(() => getOrderByFromParams(searchParams, false));
const [selectedPodUID, setSelectedPodUID] = useState<string | null>(null);
const [selectedPodUID, setSelectedPodUID] = useState<string | null>(() => {
const podUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID);
if (podUID) {
return podUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.PODS);
@ -265,15 +283,26 @@ function K8sPodsList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -318,6 +347,10 @@ function K8sPodsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -325,7 +358,7 @@ function K8sPodsList({
category: InfraMonitoringEvents.Pod,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {
@ -366,6 +399,10 @@ function K8sPodsList({
const handleRowClick = (record: K8sPodsRowData): void => {
if (groupBy.length === 0) {
setSelectedPodUID(record.podUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID]: record.podUID,
});
setSelectedRowData(null);
} else {
handleGroupByRowClick(record);
@ -380,6 +417,20 @@ function K8sPodsList({
const handleClosePodDetail = (): void => {
setSelectedPodUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleAddColumn = useCallback(
@ -435,6 +486,11 @@ function K8sPodsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -459,7 +515,9 @@ function K8sPodsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setSelectedPodUID(record.podUID),
onClick: (): void => {
setSelectedPodUID(record.podUID);
},
className: 'expanded-clickable-row',
})}
/>

View File

@ -15,8 +15,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
@ -35,6 +41,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -86,11 +93,27 @@ function PodDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -120,12 +143,18 @@ function PodDetails({
value: pod?.meta.k8s_namespace_name || '',
},
],
}),
[pod?.meta.k8s_namespace_name, pod?.meta.k8s_pod_name],
);
};
}, [pod?.meta.k8s_namespace_name, pod?.meta.k8s_pod_name, searchParams]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -155,9 +184,8 @@ function PodDetails({
value: pod?.meta.k8s_pod_name || '',
},
],
}),
[pod?.meta.k8s_pod_name],
);
};
}, [pod?.meta.k8s_pod_name, searchParams]);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -198,6 +226,13 @@ function PodDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -237,7 +272,7 @@ function PodDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[
@ -261,7 +296,7 @@ function PodDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -271,6 +306,16 @@ function PodDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -278,7 +323,7 @@ function PodDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[
@ -297,7 +342,7 @@ function PodDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
@ -308,6 +353,16 @@ function PodDetails({
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -315,7 +370,7 @@ function PodDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const podKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -333,7 +388,7 @@ function PodDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
podKindFilter,
@ -345,6 +400,16 @@ function PodDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -60,19 +63,36 @@ function K8sStatefulSetsList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedStatefulSetUID, setselectedStatefulSetUID] = useState<
string | null
>(null);
>(() => {
const statefulSetUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
);
if (statefulSetUID) {
return statefulSetUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.STATEFULSETS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -263,15 +283,26 @@ function K8sStatefulSetsList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -330,6 +361,10 @@ function K8sStatefulSetsList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID]: record.statefulsetUID,
});
} else {
handleGroupByRowClick(record);
}
@ -356,6 +391,11 @@ function K8sStatefulSetsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -380,7 +420,9 @@ function K8sStatefulSetsList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedStatefulSetUID(record.statefulsetUID),
onClick: (): void => {
setselectedStatefulSetUID(record.statefulsetUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -444,6 +486,20 @@ function K8sStatefulSetsList({
const handleCloseStatefulSetDetail = (): void => {
setselectedStatefulSetUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
@ -465,6 +521,10 @@ function K8sStatefulSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@ -472,7 +532,7 @@ function K8sStatefulSetsList({
category: InfraMonitoringEvents.StatefulSet,
});
},
[groupByFiltersData],
[groupByFiltersData, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -13,7 +13,11 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import EntityEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
import EntityLogs from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs';
import EntityMetrics from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityMetrics';
@ -36,6 +40,7 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@ -83,11 +88,27 @@ function StatefulSetDetails({
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -117,15 +138,22 @@ function StatefulSetDetails({
value: statefulSet?.meta.k8s_namespace_name || '',
},
],
}),
[
};
}, [
searchParams,
statefulSet?.meta.k8s_statefulset_name,
statefulSet?.meta.k8s_namespace_name,
],
);
]);
const initialEventsFilters = useMemo(
() => ({
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@ -155,9 +183,8 @@ function StatefulSetDetails({
value: statefulSet?.meta.k8s_statefulset_name || '',
},
],
}),
[statefulSet?.meta.k8s_statefulset_name],
);
};
}, [searchParams, statefulSet?.meta.k8s_statefulset_name]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@ -198,6 +225,13 @@ function StatefulSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@ -237,7 +271,7 @@ function StatefulSetDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -260,7 +294,7 @@ function StatefulSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -268,6 +302,16 @@ function StatefulSetDetails({
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -275,7 +319,7 @@ function StatefulSetDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
@ -292,7 +336,7 @@ function StatefulSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
@ -301,6 +345,16 @@ function StatefulSetDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -308,7 +362,7 @@ function StatefulSetDetails({
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const statefulSetKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
@ -326,7 +380,7 @@ function StatefulSetDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
statefulSetKindFilter,
@ -338,6 +392,16 @@ function StatefulSetDetails({
),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -24,11 +24,14 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getOrderByFromParams } from '../commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
@ -60,19 +63,36 @@ function K8sVolumesList({
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedVolumeUID, setselectedVolumeUID] = useState<string | null>(
null,
() => {
const volumeUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
);
if (volumeUID) {
return volumeUID;
}
return null;
},
);
const { pageSize, setPageSize } = usePageSize(K8sCategory.VOLUMES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
@ -253,15 +273,26 @@ function K8sVolumesList({
}
if ('field' in sorter && sorter.order) {
setOrderBy({
const currentOrderBy = {
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[],
[searchParams, setSearchParams],
);
const { handleChangeQueryData } = useQueryOperations({
@ -315,6 +346,10 @@ function K8sVolumesList({
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID]: record.volumeUID,
});
} else {
handleGroupByRowClick(record);
}
@ -341,6 +376,11 @@ function K8sVolumesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@ -365,7 +405,9 @@ function K8sVolumesList({
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => setselectedVolumeUID(record.volumeUID),
onClick: (): void => {
setselectedVolumeUID(record.volumeUID);
},
className: 'expanded-clickable-row',
})}
/>
@ -429,6 +471,13 @@ function K8sVolumesList({
const handleCloseVolumeDetail = (): void => {
setselectedVolumeUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) => key !== INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
),
),
});
};
const handleGroupByChange = useCallback(
@ -449,6 +498,10 @@ function K8sVolumesList({
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@ -457,7 +510,7 @@ function K8sVolumesList({
category: InfraMonitoringEvents.Volumes,
});
},
[groupByFiltersData],
[groupByFiltersData?.payload?.attributeKeys, searchParams, setSearchParams],
);
useEffect(() => {

View File

@ -12,9 +12,16 @@ import { ResizeTable } from 'components/ResizeTable';
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
import { DataType } from 'container/LogDetailedView/TableView';
import { useMemo } from 'react';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { getInvalidValueTooltipText, K8sCategory } from './constants';
import {
getInvalidValueTooltipText,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from './constants';
/**
* Converts size in bytes to a human-readable string with appropriate units
@ -250,3 +257,37 @@ export const filterDuplicateFilters = (
return uniqueFilters;
};
export const getOrderByFromParams = (
searchParams: URLSearchParams,
returnNullAsDefault = false,
): {
columnName: string;
order: 'asc' | 'desc';
} | null => {
const orderByFromParams = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
);
if (orderByFromParams) {
const decoded = decodeURIComponent(orderByFromParams);
const parsed = JSON.parse(decoded);
return parsed as { columnName: string; order: 'asc' | 'desc' };
}
if (returnNullAsDefault) {
return null;
}
return { columnName: 'cpu', order: 'desc' };
};
export const getFiltersFromParams = (
searchParams: URLSearchParams,
queryKey: string,
): IBuilderQuery['filters'] | null => {
const filtersFromParams = searchParams.get(queryKey);
if (filtersFromParams) {
const decoded = decodeURIComponent(filtersFromParams);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['filters'];
}
return null;
};

View File

@ -518,3 +518,24 @@ export const getInvalidValueTooltipText = (
entity: K8sCategory,
attribute: string,
): string => `Some ${entity} do not have ${attribute}s.`;
export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
CATEGORY: 'category',
VIEW: 'view',
CLUSTER_NAME: 'clusterName',
DAEMONSET_UID: 'daemonSetUID',
DEPLOYMENT_UID: 'deploymentUID',
JOB_UID: 'jobUID',
NAMESPACE_UID: 'namespaceUID',
NODE_UID: 'nodeUID',
POD_UID: 'podUID',
STATEFULSET_UID: 'statefulsetUID',
VOLUME_UID: 'volumeUID',
FILTERS: 'filters',
GROUP_BY: 'groupBy',
ORDER_BY: 'orderBy',
LOG_FILTERS: 'logFilters',
TRACES_FILTERS: 'tracesFilters',
EVENTS_FILTERS: 'eventsFilters',
HOSTS_FILTERS: 'hostsFilters',
};

View File

@ -19,6 +19,7 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 as uuid } from 'uuid';
@ -26,6 +27,7 @@ import { v4 as uuid } from 'uuid';
import QuerySection from './QuerySection';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
const ONE_CHART_PER_QUERY_ENABLED_KEY = 'isOneChartPerQueryEnabled';
@ -75,14 +77,18 @@ function Explorer(): JSX.Element {
useShareBuilderUrl(exportDefaultQuery);
const handleExport = useCallback(
(dashboard: Dashboard | null): void => {
(
dashboard: Dashboard | null,
_isNewDashboard?: boolean,
queryToExport?: Query,
): void => {
if (!dashboard) return;
const widgetId = uuid();
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
exportDefaultQuery,
queryToExport || exportDefaultQuery,
widgetId,
PANEL_TYPES.TIME_SERIES,
options.selectColumns,
@ -114,7 +120,7 @@ function Explorer(): JSX.Element {
return;
}
const dashboardEditView = generateExportToDashboardLink({
query: exportDefaultQuery,
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: data.payload?.uuid || '',
widgetId,
@ -135,6 +141,14 @@ function Explorer(): JSX.Element {
[exportDefaultQuery, notifications, updateDashboard],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(
stagedQuery || initialQueriesMap[DataSource.METRICS],
),
[stagedQuery],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">
@ -190,6 +204,8 @@ function Explorer(): JSX.Element {
isLoading={isLoading}
sourcepage={DataSource.METRICS}
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
/>
</Sentry.ErrorBoundary>
);

View File

@ -75,7 +75,7 @@ function MetricDetails({
hour."
placement="top"
>
<span>{`${timeSeriesTotal} ${timeSeriesActive} active`}</span>
<span>{`${timeSeriesTotal} total ${timeSeriesActive} active`}</span>
</Tooltip>
);
}, [metric]);

View File

@ -154,10 +154,14 @@ function MetricsTreemap({
<foreignObject
width={nodeWidth}
height={nodeHeight}
style={getTreemapTileStyle(node.data)}
onClick={(): void => openMetricDetails(node.data.id)}
>
<div style={getTreemapTileTextStyle()}>
<div
style={{
...getTreemapTileStyle(node.data),
...getTreemapTileTextStyle(),
}}
>
{`${node.data.displayValue}%`}
</div>
</foreignObject>

View File

@ -385,6 +385,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
PANEL_TYPES.TABLE,
variables: getDashboardVariables(selectedDashboard?.data.variables),
originalGraphType: selectedGraph || selectedWidget?.panelTypes,
};
}

View File

@ -1,9 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { updateStepInterval } from 'container/GridCardLayout/utils';
import {
GetMetricQueryRange,
GetQueryResultsProps,
} from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
@ -75,9 +77,32 @@ export const useGetQueryRange: UseGetQueryRange = (
return [REACT_QUERY_KEY.GET_QUERY_RANGE, newRequestData];
}, [options?.queryKey, newRequestData]);
const modifiedRequestData = useMemo(() => {
const graphType = requestData.originalGraphType || requestData.graphType;
if (graphType === PANEL_TYPES.BAR) {
const { start, end } = getStartEndRangeTime({
type: requestData.selectedTime,
interval: requestData.globalSelectedInterval,
});
const updatedQuery = updateStepInterval(
requestData.query,
requestData.start ? requestData.start * 1e3 : parseInt(start, 10) * 1e3,
requestData.end ? requestData.end * 1e3 : parseInt(end, 10) * 1e3,
);
return {
...requestData,
query: updatedQuery,
};
}
return requestData;
}, [requestData]);
return useQuery<SuccessResponse<MetricRangePayloadProps>, Error>({
queryFn: async ({ signal }) =>
GetMetricQueryRange(requestData, version, signal, headers),
GetMetricQueryRange(modifiedRequestData, version, signal, headers),
...options,
queryKey,
});

View File

@ -103,4 +103,5 @@ export interface GetQueryResultsProps {
start?: number;
end?: number;
step?: number;
originalGraphType?: PANEL_TYPES;
}

View File

@ -69,6 +69,7 @@ export interface GetUPlotChartOptions {
colorMapping?: Record<string, string>;
enhancedLegend?: boolean;
legendPosition?: LegendPosition;
enableZoom?: boolean;
}
/** the function converts series A , series B , series C to
@ -179,6 +180,7 @@ export const getUPlotChartOptions = ({
colorMapping,
enhancedLegend = true,
legendPosition = LegendPosition.BOTTOM,
enableZoom,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@ -248,7 +250,25 @@ export const getUPlotChartOptions = ({
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
fill: (): string => '#fff',
},
...(enableZoom
? {
drag: {
x: true,
y: true,
},
focus: {
prox: 30,
},
}
: {}),
},
...(enableZoom
? {
select: {
show: true,
},
}
: {}),
tzDate,
padding: [16, 16, 8, 8],
bands,

View File

@ -6,7 +6,7 @@ import { useMemo } from 'react';
import TrimmedText from '../TrimmedText/TrimmedText';
type KeyValueLabelProps = {
badgeKey: string;
badgeKey: string | React.ReactNode;
badgeValue: string;
maxCharacters?: number;
};
@ -25,7 +25,11 @@ export default function KeyValueLabel({
return (
<div className="key-value-label">
<div className="key-value-label__key">
{typeof badgeKey === 'string' ? (
<TrimmedText text={badgeKey} maxCharacters={maxCharacters} />
) : (
badgeKey
)}
</div>
{isUrl ? (
<a

View File

@ -1,6 +1,7 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import { Logout } from 'api/utils';
import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
@ -151,6 +152,12 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
enabled: !!isLoggedIn && !!user.email && user.role === USER_ROLES.ADMIN,
});
const { data: versionData } = useQuery({
queryFn: getUserVersion,
queryKey: ['getUserVersion', user?.accessJwt],
enabled: isLoggedIn,
});
useEffect(() => {
if (
!isFetchingOrgPreferences &&
@ -246,6 +253,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
updateUser,
updateOrgPreferences,
updateOrg,
versionData: versionData?.payload || null,
}),
[
trialInfo,
@ -265,6 +273,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
updateOrg,
user,
userFetchError,
versionData,
],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;

View File

@ -3,6 +3,7 @@ import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeatures
import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse as User } from 'types/api/user/getUser';
import { PayloadProps } from 'types/api/user/getVersion';
import { OrgPreference } from 'types/reducer/app';
export interface IAppContext {
@ -25,6 +26,7 @@ export interface IAppContext {
updateUser: (user: IUser) => void;
updateOrgPreferences: (orgPreferences: OrgPreference[]) => void;
updateOrg(orgId: string, updatedOrgName: string): void;
versionData: PayloadProps | null;
}
// User

View File

@ -0,0 +1,60 @@
import ErrorModal from 'components/ErrorModal/ErrorModal';
import {
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import APIError from 'types/api/error';
interface ErrorModalContextType {
showErrorModal: (error: APIError) => void;
hideErrorModal: () => void;
}
const ErrorModalContext = createContext<ErrorModalContextType | undefined>(
undefined,
);
export function ErrorModalProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const [error, setError] = useState<APIError | null>(null);
const [isVisible, setIsVisible] = useState(false);
const showErrorModal = useCallback((error: APIError): void => {
setError(error);
setIsVisible(true);
}, []);
const hideErrorModal = useCallback((): void => {
setError(null);
setIsVisible(false);
}, []);
const value = useMemo(() => ({ showErrorModal, hideErrorModal }), [
showErrorModal,
hideErrorModal,
]);
return (
<ErrorModalContext.Provider value={value}>
{children}
{isVisible && error && (
<ErrorModal error={error} onClose={hideErrorModal} open={isVisible} />
)}
</ErrorModalContext.Provider>
);
}
export const useErrorModal = (): ErrorModalContextType => {
const context = useContext(ErrorModalContext);
if (!context) {
throw new Error('useErrorModal must be used within an ErrorModalProvider');
}
return context;
};

View File

@ -5,6 +5,7 @@ import ROUTES from 'constants/routes';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import TimezoneProvider from 'providers/Timezone';
import React, { ReactElement } from 'react';
@ -234,6 +235,11 @@ export function getAppContextMock(
updateOrg: jest.fn(),
updateOrgPreferences: jest.fn(),
activeLicenseRefetch: jest.fn(),
versionData: {
version: '1.0.0',
ee: 'Y',
setupCompleted: true,
},
...appContextOverrides,
};
}
@ -249,6 +255,7 @@ function AllTheProviders({
return (
<QueryClientProvider client={queryClient}>
<ResourceProvider>
<ErrorModalProvider>
<Provider store={mockStored(role)}>
<AppContext.Provider value={getAppContextMock(role, appContextOverrides)}>
<BrowserRouter>
@ -259,6 +266,7 @@ function AllTheProviders({
</BrowserRouter>
</AppContext.Provider>
</Provider>
</ErrorModalProvider>
</ResourceProvider>
</QueryClientProvider>
);