mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 06:39:04 +08:00
feat(error): build generic error component (#8038)
* feat: build generic error component * chore: test error component in DataSourceInfo component * feat: get version from API + minor improvements * feat: enhance error notifications with ErrorV2 support and integrate ErrorModal * feat: implement ErrorModalContext + directly display error modal in create channel if request fails * chore: write tests for the generic error modal * chore: add optional chaining + __blank to _blank * test: add trigger component tests for ErrorModal component * test: fix the failing tests by wrapping in ErrorModalProvider * chore: address review comments * test: fix the failing tests --------- Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
This commit is contained in:
parent
3a396602a8
commit
4d484b225f
@ -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,34 +359,36 @@ function App(): JSX.Element {
|
||||
<CompatRouter>
|
||||
<UserpilotRouteTracker />
|
||||
<NotificationProvider>
|
||||
<PrivateRoute>
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<DashboardProvider>
|
||||
<KeyboardHotkeysProvider>
|
||||
<AlertRuleProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</AlertRuleProvider>
|
||||
</KeyboardHotkeysProvider>
|
||||
</DashboardProvider>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
</PrivateRoute>
|
||||
<ErrorModalProvider>
|
||||
<PrivateRoute>
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<DashboardProvider>
|
||||
<KeyboardHotkeysProvider>
|
||||
<AlertRuleProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</AlertRuleProvider>
|
||||
</KeyboardHotkeysProvider>
|
||||
</DashboardProvider>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
</PrivateRoute>
|
||||
</ErrorModalProvider>
|
||||
</NotificationProvider>
|
||||
</CompatRouter>
|
||||
</Router>
|
||||
|
191
frontend/src/assets/Error.tsx
Normal file
191
frontend/src/assets/Error.tsx
Normal 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;
|
118
frontend/src/components/ErrorModal/ErrorModal.styles.scss
Normal file
118
frontend/src/components/ErrorModal/ErrorModal.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
frontend/src/components/ErrorModal/ErrorModal.test.tsx
Normal file
195
frontend/src/components/ErrorModal/ErrorModal.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
102
frontend/src/components/ErrorModal/ErrorModal.tsx
Normal file
102
frontend/src/components/ErrorModal/ErrorModal.tsx
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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',
|
||||
});
|
||||
|
||||
fireEvent.click(testButton);
|
||||
act(() => {
|
||||
fireEvent.click(testButton);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
|
||||
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
<TrimmedText text={badgeKey} maxCharacters={maxCharacters} />
|
||||
{typeof badgeKey === 'string' ? (
|
||||
<TrimmedText text={badgeKey} maxCharacters={maxCharacters} />
|
||||
) : (
|
||||
badgeKey
|
||||
)}
|
||||
</div>
|
||||
{isUrl ? (
|
||||
<a
|
||||
|
@ -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>;
|
||||
|
@ -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
|
||||
|
60
frontend/src/providers/ErrorModalProvider.tsx
Normal file
60
frontend/src/providers/ErrorModalProvider.tsx
Normal 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;
|
||||
};
|
@ -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,16 +255,18 @@ function AllTheProviders({
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ResourceProvider>
|
||||
<Provider store={mockStored(role)}>
|
||||
<AppContext.Provider value={getAppContextMock(role, appContextOverrides)}>
|
||||
<BrowserRouter>
|
||||
{/* Use the mock store with the provided role */}
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>{children}</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</BrowserRouter>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
<ErrorModalProvider>
|
||||
<Provider store={mockStored(role)}>
|
||||
<AppContext.Provider value={getAppContextMock(role, appContextOverrides)}>
|
||||
<BrowserRouter>
|
||||
{/* Use the mock store with the provided role */}
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>{children}</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</BrowserRouter>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</ErrorModalProvider>
|
||||
</ResourceProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user