mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
chore: create a HOC to wrap components with ErrorBoundary (#9096)
* chore: create a HOC to wrap components with ErrorBoundary * feat: move svg to public, use render from test-utils
This commit is contained in:
parent
d595dcc222
commit
8b485de584
4
.gitignore
vendored
4
.gitignore
vendored
@ -230,6 +230,6 @@ poetry.toml
|
|||||||
# LSP config files
|
# LSP config files
|
||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
|
||||||
|
|
||||||
frontend/.cursor/rules/
|
# cursor files
|
||||||
|
frontend/.cursor/
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { Config } from '@jest/types';
|
|||||||
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
|
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
|
||||||
|
|
||||||
const config: Config.InitialOptions = {
|
const config: Config.InitialOptions = {
|
||||||
|
silent: true,
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
|
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
|
||||||
|
|||||||
81
frontend/public/Images/cloud.svg
Normal file
81
frontend/public/Images/cloud.svg
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.11 16.8483L14.0369 17.7304C14.0369 17.7304 12.3481 22.0324 12.2437 22.3235C12.1392 22.6146 12.1437 23.0746 12.6037 23.0746C13.1881 23.0746 16.1546 23.0591 16.1546 23.0591C16.1546 23.0591 15.4346 26.5322 15.3924 26.8433C15.3502 27.1544 15.6835 27.4277 15.9768 27.1144C16.2701 26.8011 20.3121 21.6058 20.4877 21.3525C20.801 20.8992 20.4988 20.4814 20.1433 20.4614C19.7877 20.4414 17.4056 20.4925 17.4056 20.4925L19.11 16.8483Z"
|
||||||
|
fill="#FECA18"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M17.7589 17.4527C17.7589 17.4527 16.6279 19.9548 16.5856 20.097C16.4612 20.5192 17.0078 20.6903 17.1634 20.3481C17.3189 20.0037 18.6655 17.2194 18.6655 17.2194L17.7589 17.4527Z"
|
||||||
|
fill="#FDB900"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.8859 22.2592C13.1836 22.2503 15.7968 22.2436 16.0146 22.2281C16.4213 22.1969 16.4835 22.7591 16.0146 22.7591C15.528 22.7591 12.9637 22.768 12.8081 22.7747C12.4481 22.7925 12.3992 22.2747 12.8859 22.2592Z"
|
||||||
|
fill="#FDB900"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.9813 17.1127C14.9813 17.1127 13.6481 20.4592 13.5725 20.7103C13.2592 21.7591 14.3858 21.728 14.6836 21.1325C14.8302 20.837 16.2635 17.8016 16.3257 17.2527C16.3879 16.7061 14.9813 17.1127 14.9813 17.1127Z"
|
||||||
|
fill="#FFE36A"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.3347 21.0148C15.1436 20.8837 14.9125 20.9992 14.7725 21.2192C14.6325 21.4392 14.428 21.797 14.7414 21.9858C15.0236 22.1547 15.2724 21.8147 15.3835 21.657C15.4924 21.4992 15.6324 21.217 15.3347 21.0148Z"
|
||||||
|
fill="#FFE36A"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M17.6301 21.7326C17.1212 21.6237 16.9568 22.0459 16.8479 22.5459C16.739 23.0459 16.3302 24.6636 16.2546 25.0635C16.1457 25.6257 16.6612 25.7057 16.8812 25.188C17.0457 24.8013 17.759 23.057 17.8501 22.7792C17.9901 22.3592 18.1456 21.8437 17.6301 21.7326Z"
|
||||||
|
fill="#FFE36A"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M25.7585 12.0573C25.7585 12.0573 26.3363 4.28441 19.69 3.2978C13.7147 2.41118 12.5415 8.08421 12.5415 8.08421C12.5415 8.08421 10.2838 7.55757 8.66166 9.00639C7.05064 10.4463 7.17508 12.1507 7.17508 12.1507C7.17508 12.1507 3.20195 11.524 2.79531 14.935C2.41533 18.1215 7.11286 17.3282 7.11286 17.3282L29.4183 15.686C29.4183 15.686 29.9472 14.0106 28.2606 12.7462C27.2585 11.9929 25.7585 12.0573 25.7585 12.0573Z"
|
||||||
|
fill="#E4EAEE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.7347 13.8196C13.9213 13.7574 14.9857 14.6662 18.0522 14.6951C22.2653 14.7373 25.4563 12.2552 25.4563 12.2552C25.4563 12.2552 25.5629 13.0108 25.2274 13.5485C24.8096 14.2151 24.163 14.3418 24.163 14.3418C24.163 14.3418 25.3318 15.1551 26.9362 15.0307C28.3828 14.9173 29.4294 14.4262 29.4294 14.4262C29.4294 14.4262 29.5694 15.1395 29.4916 15.8351C29.3361 17.2217 28.5228 17.7861 27.6251 17.8883C26.9651 17.9638 21.3276 17.9905 19.0122 18.0127C16.9256 18.0327 6.29285 18.2994 5.20624 18.1794C4.07963 18.0549 3.22412 17.3772 2.91303 16.4061C2.67526 15.6706 2.78859 15.1973 2.78859 15.1973C2.78859 15.1973 4.85959 15.7462 6.02175 15.6151C7.48167 15.4484 8.46162 14.5307 8.46162 14.5307C8.46162 14.5307 9.33713 15.0307 11.277 14.864C12.9458 14.7173 13.7347 13.8196 13.7347 13.8196Z"
|
||||||
|
fill="url(#paint0_radial_811_5475)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M24.8653 18.2661C24.6386 18.1394 23.832 18.8927 23.3787 19.346C23.1009 19.6238 22.2165 20.3504 22.0965 21.0504C21.7988 22.7703 23.7698 23.1614 24.552 22.3637C25.143 21.7615 25.0364 20.586 25.0364 20.1904C25.0386 19.6416 25.1475 18.4216 24.8653 18.2661Z"
|
||||||
|
fill="#52C0EE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8.67058 19.1904C8.45504 19.0659 7.68174 19.7948 7.24621 20.237C6.97956 20.5058 6.13294 21.2103 6.01294 21.8947C5.71962 23.5723 7.5973 23.9657 8.34837 23.1924C8.91501 22.608 8.82168 21.4591 8.82391 21.0725C8.82613 20.537 8.93723 19.3459 8.67058 19.1904Z"
|
||||||
|
fill="#52C0EE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.2548 24.1634C12.0126 24.0723 11.1971 24.8145 10.7438 25.2678C10.466 25.5456 9.64386 26.2566 9.52386 26.9566C9.2261 28.6765 11.1349 29.0832 11.9171 28.2854C12.5081 27.6832 12.4104 26.5077 12.4015 26.1122C12.3793 25.0812 12.4215 24.2256 12.2548 24.1634Z"
|
||||||
|
fill="#52C0EE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M23.5054 20.4926C23.1943 20.3304 22.8165 20.3993 22.5765 20.8992C22.3365 21.3992 22.5343 21.797 22.7854 21.9103C23.0743 22.0436 23.432 21.9214 23.672 21.5147C23.912 21.1081 23.7654 20.6281 23.5054 20.4926Z"
|
||||||
|
fill="#B2E6FE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.6682 26.4279C10.3482 26.3168 9.99715 26.4345 9.83716 26.9478C9.67717 27.4611 9.92382 27.8122 10.1794 27.8878C10.4749 27.9744 10.7993 27.8078 10.9727 27.3834C11.1438 26.9589 10.9349 26.5212 10.6682 26.4279Z"
|
||||||
|
fill="#B2E6FE"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.12613 21.3258C6.78837 21.2325 6.43283 21.3769 6.30395 21.9169C6.17507 22.4569 6.45061 22.8035 6.71948 22.8613C7.03058 22.9302 7.35278 22.7369 7.50389 22.288C7.65277 21.8436 7.41056 21.4036 7.12613 21.3258Z"
|
||||||
|
fill="#B2E6FE"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_811_5475"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(0.18837 -6.53799 9.79456 0.282554 16.401 18.5051)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.1934" stopColor="#FFE366" />
|
||||||
|
<stop offset="0.3305" stopColor="#EDDD82" />
|
||||||
|
<stop offset="0.5709" stopColor="#D0D4AD" />
|
||||||
|
<stop offset="0.7589" stopColor="#BFCFC7" />
|
||||||
|
<stop offset="0.8699" stopColor="#B8CDD1" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
117
frontend/src/components/ErrorBoundaryHOC/README.md
Normal file
117
frontend/src/components/ErrorBoundaryHOC/README.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# withErrorBoundary HOC
|
||||||
|
|
||||||
|
A Higher-Order Component (HOC) that wraps React components with ErrorBoundary to provide error handling and recovery.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Error Catching**: Catches JavaScript errors in any component tree
|
||||||
|
- **Integration**: Automatically reports errors with context
|
||||||
|
- **Custom Fallback UI**: Supports custom error fallback components
|
||||||
|
- **Error Logging**: Optional custom error handlers for additional logging
|
||||||
|
- **TypeScript Support**: Fully typed with proper generics
|
||||||
|
- **Component Context**: Automatically adds component name to tags
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { withErrorBoundary } from 'components/HOC';
|
||||||
|
|
||||||
|
// Wrap any component
|
||||||
|
const SafeComponent = withErrorBoundary(MyComponent);
|
||||||
|
|
||||||
|
// Use it like any other component
|
||||||
|
<SafeComponent prop1="value1" prop2="value2" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Fallback Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const CustomFallback = () => (
|
||||||
|
<div>
|
||||||
|
<h3>Oops! Something went wrong</h3>
|
||||||
|
<button onClick={() => window.location.reload()}>Reload</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SafeComponent = withErrorBoundary(MyComponent, {
|
||||||
|
fallback: <CustomFallback />
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Error Handler
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const SafeComponent = withErrorBoundary(MyComponent, {
|
||||||
|
onError: (error, componentStack, eventId) => {
|
||||||
|
console.error('Component error:', error);
|
||||||
|
// Send to analytics, logging service, etc.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sentry Configuration
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const SafeComponent = withErrorBoundary(MyComponent, {
|
||||||
|
sentryOptions: {
|
||||||
|
tags: {
|
||||||
|
section: 'dashboard',
|
||||||
|
priority: 'high',
|
||||||
|
feature: 'metrics'
|
||||||
|
},
|
||||||
|
level: 'error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `withErrorBoundary<P>(component, options?)`
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `component: ComponentType<P>` - The React component to wrap
|
||||||
|
- `options?: WithErrorBoundaryOptions` - Configuration options
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface WithErrorBoundaryOptions {
|
||||||
|
/** Custom fallback component to render when an error occurs */
|
||||||
|
fallback?: ReactElement;
|
||||||
|
|
||||||
|
/** Custom error handler function */
|
||||||
|
onError?: (
|
||||||
|
error: unknown,
|
||||||
|
componentStack: string | undefined,
|
||||||
|
eventId: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/** Additional props to pass to the Sentry ErrorBoundary */
|
||||||
|
sentryOptions?: {
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
level?: Sentry.SeverityLevel;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- **Critical Components**: Wrap important UI components that shouldn't crash the entire app
|
||||||
|
- **Third-party Integrations**: Wrap components that use external libraries
|
||||||
|
- **Data-heavy Components**: Wrap components that process complex data
|
||||||
|
- **Route Components**: Wrap page-level components to prevent navigation issues
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Sparingly**: Don't wrap every component - focus on critical ones
|
||||||
|
2. **Meaningful Fallbacks**: Provide helpful fallback UI that guides users
|
||||||
|
3. **Log Errors**: Always implement error logging for debugging
|
||||||
|
4. **Component Names**: Ensure components have proper `displayName` for debugging
|
||||||
|
5. **Test Error Scenarios**: Test that your error boundaries work as expected
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See `withErrorBoundary.example.tsx` for complete usage examples.
|
||||||
@ -0,0 +1,211 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import withErrorBoundary, {
|
||||||
|
WithErrorBoundaryOptions,
|
||||||
|
} from '../withErrorBoundary';
|
||||||
|
|
||||||
|
// Mock dependencies before imports
|
||||||
|
jest.mock('@sentry/react', () => {
|
||||||
|
const ReactMock = jest.requireActual('react');
|
||||||
|
|
||||||
|
class MockErrorBoundary extends ReactMock.Component<
|
||||||
|
{
|
||||||
|
children: React.ReactNode;
|
||||||
|
fallback: React.ReactElement;
|
||||||
|
onError?: (error: Error, componentStack: string, eventId: string) => void;
|
||||||
|
beforeCapture?: (scope: {
|
||||||
|
setTag: (key: string, value: string) => void;
|
||||||
|
setLevel: (level: string) => void;
|
||||||
|
}) => void;
|
||||||
|
},
|
||||||
|
{ hasError: boolean }
|
||||||
|
> {
|
||||||
|
constructor(props: MockErrorBoundary['props']) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): { hasError: boolean } {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: { componentStack: string }): void {
|
||||||
|
const { beforeCapture, onError } = this.props;
|
||||||
|
if (beforeCapture) {
|
||||||
|
const mockScope = {
|
||||||
|
setTag: jest.fn(),
|
||||||
|
setLevel: jest.fn(),
|
||||||
|
};
|
||||||
|
beforeCapture(mockScope);
|
||||||
|
}
|
||||||
|
if (onError) {
|
||||||
|
onError(error, errorInfo.componentStack, 'mock-event-id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactNode {
|
||||||
|
const { hasError } = this.state;
|
||||||
|
const { fallback, children } = this.props;
|
||||||
|
if (hasError) {
|
||||||
|
return <div data-testid="error-boundary-fallback">{fallback}</div>;
|
||||||
|
}
|
||||||
|
return <div data-testid="app-error-boundary">{children}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ErrorBoundary: MockErrorBoundary,
|
||||||
|
SeverityLevel: {
|
||||||
|
error: 'error',
|
||||||
|
warning: 'warning',
|
||||||
|
info: 'info',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback',
|
||||||
|
() =>
|
||||||
|
function MockErrorBoundaryFallback(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div data-testid="default-error-fallback">Default Error Fallback</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test component that can throw errors
|
||||||
|
interface TestComponentProps {
|
||||||
|
shouldThrow?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent({
|
||||||
|
shouldThrow = false,
|
||||||
|
message = 'Test Component',
|
||||||
|
}: TestComponentProps): JSX.Element {
|
||||||
|
if (shouldThrow) {
|
||||||
|
throw new Error('Test error');
|
||||||
|
}
|
||||||
|
return <div data-testid="test-component">{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestComponent.defaultProps = {
|
||||||
|
shouldThrow: false,
|
||||||
|
message: 'Test Component',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test component with display name
|
||||||
|
function NamedComponent(): JSX.Element {
|
||||||
|
return <div data-testid="named-component">Named Component</div>;
|
||||||
|
}
|
||||||
|
NamedComponent.displayName = 'NamedComponent';
|
||||||
|
|
||||||
|
describe('withErrorBoundary', () => {
|
||||||
|
// Suppress console errors for cleaner test output
|
||||||
|
const originalError = console.error;
|
||||||
|
beforeAll(() => {
|
||||||
|
console.error = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap component with ErrorBoundary and render successfully', () => {
|
||||||
|
// Arrange
|
||||||
|
const SafeComponent = withErrorBoundary(TestComponent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<SafeComponent message="Hello World" />);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId('app-error-boundary')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render fallback UI when component throws error', () => {
|
||||||
|
// Arrange
|
||||||
|
const SafeComponent = withErrorBoundary(TestComponent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<SafeComponent shouldThrow />);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('default-error-fallback')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom fallback component when provided', () => {
|
||||||
|
// Arrange
|
||||||
|
const customFallback = (
|
||||||
|
<div data-testid="custom-fallback">Custom Error UI</div>
|
||||||
|
);
|
||||||
|
const options: WithErrorBoundaryOptions = {
|
||||||
|
fallback: customFallback,
|
||||||
|
};
|
||||||
|
const SafeComponent = withErrorBoundary(TestComponent, options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<SafeComponent shouldThrow />);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Custom Error UI')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call custom error handler when error occurs', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockErrorHandler = jest.fn();
|
||||||
|
const options: WithErrorBoundaryOptions = {
|
||||||
|
onError: mockErrorHandler,
|
||||||
|
};
|
||||||
|
const SafeComponent = withErrorBoundary(TestComponent, options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<SafeComponent shouldThrow />);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockErrorHandler).toHaveBeenCalledWith(
|
||||||
|
expect.any(Error),
|
||||||
|
expect.any(String),
|
||||||
|
'mock-event-id',
|
||||||
|
);
|
||||||
|
expect(mockErrorHandler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set correct display name for debugging', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const SafeTestComponent = withErrorBoundary(TestComponent);
|
||||||
|
const SafeNamedComponent = withErrorBoundary(NamedComponent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(SafeTestComponent.displayName).toBe(
|
||||||
|
'withErrorBoundary(TestComponent)',
|
||||||
|
);
|
||||||
|
expect(SafeNamedComponent.displayName).toBe(
|
||||||
|
'withErrorBoundary(NamedComponent)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle component without display name', () => {
|
||||||
|
// Arrange
|
||||||
|
function AnonymousComponent(): JSX.Element {
|
||||||
|
return <div>Anonymous</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const SafeAnonymousComponent = withErrorBoundary(AnonymousComponent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(SafeAnonymousComponent.displayName).toBe(
|
||||||
|
'withErrorBoundary(AnonymousComponent)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
frontend/src/components/ErrorBoundaryHOC/index.ts
Normal file
2
frontend/src/components/ErrorBoundaryHOC/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export type { WithErrorBoundaryOptions } from './withErrorBoundary';
|
||||||
|
export { default as withErrorBoundary } from './withErrorBoundary';
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { withErrorBoundary } from './index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example component that can throw errors
|
||||||
|
*/
|
||||||
|
function ProblematicComponent(): JSX.Element {
|
||||||
|
const [shouldThrow, setShouldThrow] = useState(false);
|
||||||
|
|
||||||
|
if (shouldThrow) {
|
||||||
|
throw new Error('This is a test error from ProblematicComponent!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<h3>Problematic Component</h3>
|
||||||
|
<p>This component can throw errors when the button is clicked.</p>
|
||||||
|
<Button type="primary" onClick={(): void => setShouldThrow(true)} danger>
|
||||||
|
Trigger Error
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic usage - wraps component with default error boundary
|
||||||
|
*/
|
||||||
|
export const SafeProblematicComponent = withErrorBoundary(ProblematicComponent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage with custom fallback component
|
||||||
|
*/
|
||||||
|
function CustomErrorFallback(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}
|
||||||
|
>
|
||||||
|
<h4 style={{ color: 'red' }}>Custom Error Fallback</h4>
|
||||||
|
<p>Something went wrong in this specific component!</p>
|
||||||
|
<Button onClick={(): void => window.location.reload()}>Reload Page</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SafeProblematicComponentWithCustomFallback = withErrorBoundary(
|
||||||
|
ProblematicComponent,
|
||||||
|
{
|
||||||
|
fallback: <CustomErrorFallback />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage with custom error handler
|
||||||
|
*/
|
||||||
|
export const SafeProblematicComponentWithErrorHandler = withErrorBoundary(
|
||||||
|
ProblematicComponent,
|
||||||
|
{
|
||||||
|
onError: (error, errorInfo) => {
|
||||||
|
console.error('Custom error handler:', error);
|
||||||
|
console.error('Error info:', errorInfo);
|
||||||
|
// You could also send to analytics, logging service, etc.
|
||||||
|
},
|
||||||
|
sentryOptions: {
|
||||||
|
tags: {
|
||||||
|
section: 'dashboard',
|
||||||
|
priority: 'high',
|
||||||
|
},
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of wrapping an existing component from the codebase
|
||||||
|
*/
|
||||||
|
function ExistingComponent({
|
||||||
|
title,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
data: any[];
|
||||||
|
}): JSX.Element {
|
||||||
|
// This could be any existing component that might throw errors
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<ul>
|
||||||
|
{data.map((item, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<li key={index}>{item.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SafeExistingComponent = withErrorBoundary(ExistingComponent, {
|
||||||
|
sentryOptions: {
|
||||||
|
tags: {
|
||||||
|
component: 'ExistingComponent',
|
||||||
|
feature: 'data-display',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage examples in a container component
|
||||||
|
*/
|
||||||
|
export function ErrorBoundaryExamples(): JSX.Element {
|
||||||
|
const sampleData = [
|
||||||
|
{ name: 'Item 1' },
|
||||||
|
{ name: 'Item 2' },
|
||||||
|
{ name: 'Item 3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<h2>Error Boundary HOC Examples</h2>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3>1. Basic Usage</h3>
|
||||||
|
<SafeProblematicComponent />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3>2. With Custom Fallback</h3>
|
||||||
|
<SafeProblematicComponentWithCustomFallback />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3>3. With Custom Error Handler</h3>
|
||||||
|
<SafeProblematicComponentWithErrorHandler />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3>4. Wrapped Existing Component</h3>
|
||||||
|
<SafeExistingComponent title="Sample Data" data={sampleData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { ComponentType, ReactElement } from 'react';
|
||||||
|
|
||||||
|
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the ErrorBoundary HOC
|
||||||
|
*/
|
||||||
|
interface WithErrorBoundaryOptions {
|
||||||
|
/** Custom fallback component to render when an error occurs */
|
||||||
|
fallback?: ReactElement;
|
||||||
|
/** Custom error handler function */
|
||||||
|
onError?: (
|
||||||
|
error: unknown,
|
||||||
|
componentStack: string | undefined,
|
||||||
|
eventId: string,
|
||||||
|
) => void;
|
||||||
|
/** Additional props to pass to the ErrorBoundary */
|
||||||
|
sentryOptions?: {
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
level?: Sentry.SeverityLevel;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Higher-Order Component that wraps a component with ErrorBoundary
|
||||||
|
*
|
||||||
|
* @param WrappedComponent - The component to wrap with error boundary
|
||||||
|
* @param options - Configuration options for the error boundary
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic usage
|
||||||
|
* const SafeComponent = withErrorBoundary(MyComponent);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // With custom fallback
|
||||||
|
* const SafeComponent = withErrorBoundary(MyComponent, {
|
||||||
|
* fallback: <div>Something went wrong!</div>
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // With custom error handler
|
||||||
|
* const SafeComponent = withErrorBoundary(MyComponent, {
|
||||||
|
* onError: (error, errorInfo) => {
|
||||||
|
* console.error('Component error:', error, errorInfo);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
function withErrorBoundary<P extends Record<string, unknown>>(
|
||||||
|
WrappedComponent: ComponentType<P>,
|
||||||
|
options: WithErrorBoundaryOptions = {},
|
||||||
|
): ComponentType<P> {
|
||||||
|
const {
|
||||||
|
fallback = <ErrorBoundaryFallback />,
|
||||||
|
onError,
|
||||||
|
sentryOptions = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
function WithErrorBoundaryComponent(props: P): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Sentry.ErrorBoundary
|
||||||
|
fallback={fallback}
|
||||||
|
beforeCapture={(scope): void => {
|
||||||
|
// Add component name to context
|
||||||
|
scope.setTag(
|
||||||
|
'component',
|
||||||
|
WrappedComponent.displayName || WrappedComponent.name || 'Unknown',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add any custom tags
|
||||||
|
if (sentryOptions.tags) {
|
||||||
|
Object.entries(sentryOptions.tags).forEach(([key, value]) => {
|
||||||
|
scope.setTag(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set severity level if provided
|
||||||
|
if (sentryOptions.level) {
|
||||||
|
scope.setLevel(sentryOptions.level);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={onError}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<WrappedComponent {...props} />
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set display name for debugging purposes
|
||||||
|
WithErrorBoundaryComponent.displayName = `withErrorBoundary(${
|
||||||
|
WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
||||||
|
})`;
|
||||||
|
|
||||||
|
return WithErrorBoundaryComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary;
|
||||||
|
export type { WithErrorBoundaryOptions };
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { Spin, Switch, Table, Tooltip, Typography } from 'antd';
|
import { Spin, Switch, Table, Tooltip, Typography } from 'antd';
|
||||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||||
|
import { withErrorBoundary } from 'components/ErrorBoundaryHOC';
|
||||||
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V4 } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import {
|
import {
|
||||||
@ -248,4 +249,4 @@ function TopErrors({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TopErrors;
|
export default withErrorBoundary(TopErrors);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList';
|
import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
@ -7,6 +6,7 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import { render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
import Summary from '../Summary';
|
import Summary from '../Summary';
|
||||||
import { TreemapViewType } from '../types';
|
import { TreemapViewType } from '../types';
|
||||||
|
|||||||
@ -9,18 +9,39 @@
|
|||||||
|
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
.error-icon {
|
.error-boundary-fallback-content {
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title,
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
max-width: 520px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.title,
|
||||||
margin-top: 16px;
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +1,56 @@
|
|||||||
import './ErrorBoundaryFallback.styles.scss';
|
import './ErrorBoundaryFallback.styles.scss';
|
||||||
|
|
||||||
import { BugOutlined } from '@ant-design/icons';
|
import { Button } from 'antd';
|
||||||
import { Button, Typography } from 'antd';
|
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import Slack from 'container/SideNav/Slack';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { Home, TriangleAlert } from 'lucide-react';
|
import { Home, LifeBuoy } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
function ErrorBoundaryFallback(): JSX.Element {
|
function ErrorBoundaryFallback(): JSX.Element {
|
||||||
const { t } = useTranslation(['errorDetails']);
|
|
||||||
|
|
||||||
const onClickSlackHandler = (): void => {
|
|
||||||
window.open('https://signoz.io/slack', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReload = (): void => {
|
const handleReload = (): void => {
|
||||||
// Go to home page
|
// Go to home page
|
||||||
window.location.href = ROUTES.HOME;
|
window.location.href = ROUTES.HOME;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||||
|
|
||||||
|
const handleSupport = useCallback(() => {
|
||||||
|
handleContactSupport(isCloudUserVal);
|
||||||
|
}, [isCloudUserVal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="error-boundary-fallback-container">
|
<div className="error-boundary-fallback-container">
|
||||||
<div className="error-icon">
|
<div className="error-boundary-fallback-content">
|
||||||
<TriangleAlert size={48} />
|
<div className="error-icon">
|
||||||
</div>
|
<img src="/Images/cloud.svg" alt="error-cloud-icon" />
|
||||||
<div className="title">
|
</div>
|
||||||
<BugOutlined />
|
<div className="title">Something went wrong :/</div>
|
||||||
<Typography.Title type="danger" level={4} style={{ margin: 0 }}>
|
|
||||||
{t('something_went_wrong')}
|
|
||||||
</Typography.Title>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{t('contact_if_issue_exists')}</p>
|
<div className="description">
|
||||||
|
Our team is getting on top to resolve this. Please reach out to support if
|
||||||
|
the issue persists.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleReload}
|
onClick={handleReload}
|
||||||
icon={<Home size={16} />}
|
icon={<Home size={16} />}
|
||||||
className="periscope-btn primary"
|
className="periscope-btn primary"
|
||||||
>
|
>
|
||||||
Go Home
|
Go to Home
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn secondary"
|
className="periscope-btn secondary"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={onClickSlackHandler}
|
onClick={handleSupport}
|
||||||
icon={<Slack />}
|
icon={<LifeBuoy size={16} />}
|
||||||
>
|
>
|
||||||
Slack Support
|
Contact Support
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,49 @@
|
|||||||
.support-page-container {
|
.support-page-container {
|
||||||
color: white;
|
max-height: 100vh;
|
||||||
padding-left: 48px;
|
overflow: hidden;
|
||||||
padding-right: 48px;
|
|
||||||
|
|
||||||
max-width: 1400px;
|
.support-page-header {
|
||||||
margin: 64px auto;
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
background: rgba(11, 12, 14, 0.7);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.support-page-header-title {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
text-align: center;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 14px;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-page-content {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.support-page-content-description {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
text-align: left;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channels {
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-channels {
|
.support-channels {
|
||||||
@ -21,6 +60,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
.support-channel-title {
|
.support-channel-title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -37,6 +86,21 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
padding: 4px 16px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channel-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,8 +111,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1440px) {
|
.lightMode {
|
||||||
.support-page-container {
|
.support-page-container {
|
||||||
width: 80%;
|
.support-page-header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
.support-page-header-title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-page-content {
|
||||||
|
.support-page-content-description {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channels {
|
||||||
|
.support-channel {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(255, 255, 255, 0.8) 0%,
|
||||||
|
rgba(255, 255, 255, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channel-title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channel-action {
|
||||||
|
button {
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-channel-btn {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import updateCreditCardApi from 'api/v1/checkout/create';
|
|||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import {
|
import {
|
||||||
|
ArrowUpRight,
|
||||||
Book,
|
Book,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Github,
|
Github,
|
||||||
|
LifeBuoy,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Slack,
|
Slack,
|
||||||
X,
|
X,
|
||||||
@ -45,34 +47,38 @@ const supportChannels = [
|
|||||||
{
|
{
|
||||||
key: 'documentation',
|
key: 'documentation',
|
||||||
name: 'Documentation',
|
name: 'Documentation',
|
||||||
icon: <Book />,
|
icon: <Book size={16} />,
|
||||||
title: 'Find answers in the documentation.',
|
title: 'Find answers in the documentation.',
|
||||||
url: 'https://signoz.io/docs/',
|
url: 'https://signoz.io/docs/',
|
||||||
btnText: 'Visit docs',
|
btnText: 'Visit docs',
|
||||||
|
isExternal: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'github',
|
key: 'github',
|
||||||
name: 'Github',
|
name: 'Github',
|
||||||
icon: <Github />,
|
icon: <Github size={16} />,
|
||||||
title: 'Create an issue on GitHub to report bugs or request new features.',
|
title: 'Create an issue on GitHub to report bugs or request new features.',
|
||||||
url: 'https://github.com/SigNoz/signoz/issues',
|
url: 'https://github.com/SigNoz/signoz/issues',
|
||||||
btnText: 'Create issue',
|
btnText: 'Create issue',
|
||||||
|
isExternal: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'slack_community',
|
key: 'slack_community',
|
||||||
name: 'Slack Community',
|
name: 'Slack Community',
|
||||||
icon: <Slack />,
|
icon: <Slack size={16} />,
|
||||||
title: 'Get support from the SigNoz community on Slack.',
|
title: 'Get support from the SigNoz community on Slack.',
|
||||||
url: 'https://signoz.io/slack',
|
url: 'https://signoz.io/slack',
|
||||||
btnText: 'Join Slack',
|
btnText: 'Join Slack',
|
||||||
|
isExternal: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'chat',
|
key: 'chat',
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
icon: <MessageSquare />,
|
icon: <MessageSquare size={16} />,
|
||||||
title: 'Get quick support directly from the team.',
|
title: 'Get quick support directly from the team.',
|
||||||
url: '',
|
url: '',
|
||||||
btnText: 'Launch chat',
|
btnText: 'Launch chat',
|
||||||
|
isExternal: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -182,38 +188,45 @@ export default function Support(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="support-page-container">
|
<div className="support-page-container">
|
||||||
<div className="support-page-header">
|
<header className="support-page-header">
|
||||||
<Title level={3}> Help & Support </Title>
|
<div className="support-page-header-title" data-testid="support-page-title">
|
||||||
<Text style={{ fontSize: 14 }}>
|
<LifeBuoy size={16} />
|
||||||
|
Support
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="support-page-content">
|
||||||
|
<div className="support-page-content-description">
|
||||||
We are here to help in case of questions or issues. Pick the channel that
|
We are here to help in case of questions or issues. Pick the channel that
|
||||||
is most convenient for you.
|
is most convenient for you.
|
||||||
</Text>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="support-channels">
|
<div className="support-channels">
|
||||||
{supportChannels.map(
|
{supportChannels.map(
|
||||||
(channel): JSX.Element => (
|
(channel): JSX.Element => (
|
||||||
<Card className="support-channel" key={channel.key}>
|
<Card className="support-channel" key={channel.key}>
|
||||||
<div className="support-channel-content">
|
<div className="support-channel-content">
|
||||||
<Title ellipsis level={5} className="support-channel-title">
|
<Title ellipsis level={5} className="support-channel-title">
|
||||||
{channel.icon}
|
{channel.icon}
|
||||||
{channel.name}{' '}
|
{channel.name}{' '}
|
||||||
</Title>
|
</Title>
|
||||||
<Text> {channel.title} </Text>
|
<Text> {channel.title} </Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="support-channel-action">
|
<div className="support-channel-action">
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
className="periscope-btn secondary support-channel-btn"
|
||||||
className="periscope-btn secondary"
|
type="default"
|
||||||
onClick={(): void => handleChannelClick(channel)}
|
onClick={(): void => handleChannelClick(channel)}
|
||||||
>
|
>
|
||||||
<Text ellipsis>{channel.btnText} </Text>
|
<Text ellipsis>{channel.btnText} </Text>
|
||||||
</Button>
|
{channel.isExternal && <ArrowUpRight size={14} />}
|
||||||
</div>
|
</Button>
|
||||||
</Card>
|
</div>
|
||||||
),
|
</Card>
|
||||||
)}
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Credit Card Modal */}
|
{/* Add Credit Card Modal */}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user