diff --git a/.gitignore b/.gitignore index 014c7c2800bc..c002fbe276c1 100644 --- a/.gitignore +++ b/.gitignore @@ -230,6 +230,6 @@ poetry.toml # LSP config files pyrightconfig.json -# End of https://www.toptal.com/developers/gitignore/api/python -frontend/.cursor/rules/ \ No newline at end of file +# cursor files +frontend/.cursor/ diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 1d9255a329e8..a5e8d86c3fce 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -3,6 +3,7 @@ import type { Config } from '@jest/types'; const USE_SAFE_NAVIGATE_MOCK_PATH = '/__mocks__/useSafeNavigate.ts'; const config: Config.InitialOptions = { + silent: true, clearMocks: true, coverageDirectory: 'coverage', coverageReporters: ['text', 'cobertura', 'html', 'json-summary'], diff --git a/frontend/public/Images/cloud.svg b/frontend/public/Images/cloud.svg new file mode 100644 index 000000000000..c7138d589b2f --- /dev/null +++ b/frontend/public/Images/cloud.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/ErrorBoundaryHOC/README.md b/frontend/src/components/ErrorBoundaryHOC/README.md new file mode 100644 index 000000000000..4022cc96681d --- /dev/null +++ b/frontend/src/components/ErrorBoundaryHOC/README.md @@ -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 + +``` + +## Advanced Usage + +### Custom Fallback Component + +```tsx +const CustomFallback = () => ( +
+

Oops! Something went wrong

+ +
+); + +const SafeComponent = withErrorBoundary(MyComponent, { + fallback: +}); +``` + +### 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

(component, options?)` + +#### Parameters + +- `component: ComponentType

` - 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; + 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. diff --git a/frontend/src/components/ErrorBoundaryHOC/__tests__/withErrorBoundary.test.tsx b/frontend/src/components/ErrorBoundaryHOC/__tests__/withErrorBoundary.test.tsx new file mode 100644 index 000000000000..3cec7083326a --- /dev/null +++ b/frontend/src/components/ErrorBoundaryHOC/__tests__/withErrorBoundary.test.tsx @@ -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

{fallback}
; + } + return
{children}
; + } + } + + return { + ErrorBoundary: MockErrorBoundary, + SeverityLevel: { + error: 'error', + warning: 'warning', + info: 'info', + }, + }; +}); + +jest.mock( + '../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback', + () => + function MockErrorBoundaryFallback(): JSX.Element { + return ( +
Default Error Fallback
+ ); + }, +); + +// 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
{message}
; +} + +TestComponent.defaultProps = { + shouldThrow: false, + message: 'Test Component', +}; + +// Test component with display name +function NamedComponent(): JSX.Element { + return
Named Component
; +} +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(); + + // 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(); + + // 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 = ( +
Custom Error UI
+ ); + const options: WithErrorBoundaryOptions = { + fallback: customFallback, + }; + const SafeComponent = withErrorBoundary(TestComponent, options); + + // Act + render(); + + // 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(); + + // 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
Anonymous
; + } + + // Act + const SafeAnonymousComponent = withErrorBoundary(AnonymousComponent); + + // Assert + expect(SafeAnonymousComponent.displayName).toBe( + 'withErrorBoundary(AnonymousComponent)', + ); + }); +}); diff --git a/frontend/src/components/ErrorBoundaryHOC/index.ts b/frontend/src/components/ErrorBoundaryHOC/index.ts new file mode 100644 index 000000000000..1e7e5a6ae10c --- /dev/null +++ b/frontend/src/components/ErrorBoundaryHOC/index.ts @@ -0,0 +1,2 @@ +export type { WithErrorBoundaryOptions } from './withErrorBoundary'; +export { default as withErrorBoundary } from './withErrorBoundary'; diff --git a/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.example.tsx b/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.example.tsx new file mode 100644 index 000000000000..ce0c83fa537b --- /dev/null +++ b/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.example.tsx @@ -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 ( +
+

Problematic Component

+

This component can throw errors when the button is clicked.

+ +
+ ); +} + +/** + * Basic usage - wraps component with default error boundary + */ +export const SafeProblematicComponent = withErrorBoundary(ProblematicComponent); + +/** + * Usage with custom fallback component + */ +function CustomErrorFallback(): JSX.Element { + return ( +
+

Custom Error Fallback

+

Something went wrong in this specific component!

+ +
+ ); +} + +export const SafeProblematicComponentWithCustomFallback = withErrorBoundary( + ProblematicComponent, + { + fallback: , + }, +); + +/** + * 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 ( +
+

{title}

+
    + {data.map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key +
  • {item.name}
  • + ))} +
+
+ ); +} + +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 ( +
+

Error Boundary HOC Examples

+ +
+

1. Basic Usage

+ +
+ +
+

2. With Custom Fallback

+ +
+ +
+

3. With Custom Error Handler

+ +
+ +
+

4. Wrapped Existing Component

+ +
+
+ ); +} diff --git a/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.tsx b/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.tsx new file mode 100644 index 000000000000..62c552641506 --- /dev/null +++ b/frontend/src/components/ErrorBoundaryHOC/withErrorBoundary.tsx @@ -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; + 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:
Something went wrong!
+ * }); + * + * @example + * // With custom error handler + * const SafeComponent = withErrorBoundary(MyComponent, { + * onError: (error, errorInfo) => { + * console.error('Component error:', error, errorInfo); + * } + * }); + */ +function withErrorBoundary

>( + WrappedComponent: ComponentType

, + options: WithErrorBoundaryOptions = {}, +): ComponentType

{ + const { + fallback = , + onError, + sentryOptions = {}, + } = options; + + function WithErrorBoundaryComponent(props: P): JSX.Element { + return ( + { + // 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 */} + + + ); + } + + // Set display name for debugging purposes + WithErrorBoundaryComponent.displayName = `withErrorBoundary(${ + WrappedComponent.displayName || WrappedComponent.name || 'Component' + })`; + + return WithErrorBoundaryComponent; +} + +export default withErrorBoundary; +export type { WithErrorBoundaryOptions }; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx index 909a46558106..bc9c839e642b 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx @@ -1,6 +1,7 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Spin, Switch, Table, Tooltip, Typography } from 'antd'; import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer'; +import { withErrorBoundary } from 'components/ErrorBoundaryHOC'; import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V4 } from 'constants/app'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { @@ -248,4 +249,4 @@ function TopErrors({ ); } -export default TopErrors; +export default withErrorBoundary(TopErrors); diff --git a/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx index 48522c686875..0923e405cf47 100644 --- a/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx @@ -1,4 +1,3 @@ -import { render, screen } from '@testing-library/react'; import { MetricType } from 'api/metricsExplorer/getMetricsList'; import ROUTES from 'constants/routes'; import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList'; @@ -7,6 +6,7 @@ import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; import { useSearchParams } from 'react-router-dom-v5-compat'; import store from 'store'; +import { render, screen } from 'tests/test-utils'; import Summary from '../Summary'; import { TreemapViewType } from '../types'; diff --git a/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.styles.scss b/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.styles.scss index 33f0121b974b..768713bb5c78 100644 --- a/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.styles.scss +++ b/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.styles.scss @@ -9,18 +9,39 @@ color: var(--bg-vanilla-100); - .error-icon { - margin-bottom: 16px; - } - - .title, - .actions { + .error-boundary-fallback-content { display: flex; - align-items: center; + flex-direction: column; + max-width: 520px; gap: 8px; - } - .actions { - margin-top: 16px; + .title, + .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; + } } } diff --git a/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.tsx b/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.tsx index 3bf26e0db1f4..9aa1eba5187d 100644 --- a/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.tsx +++ b/frontend/src/pages/ErrorBoundaryFallback/ErrorBoundaryFallback.tsx @@ -1,55 +1,56 @@ import './ErrorBoundaryFallback.styles.scss'; -import { BugOutlined } from '@ant-design/icons'; -import { Button, Typography } from 'antd'; +import { Button } from 'antd'; import ROUTES from 'constants/routes'; -import Slack from 'container/SideNav/Slack'; -import { Home, TriangleAlert } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; +import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; +import { Home, LifeBuoy } from 'lucide-react'; +import { handleContactSupport } from 'pages/Integrations/utils'; +import { useCallback } from 'react'; function ErrorBoundaryFallback(): JSX.Element { - const { t } = useTranslation(['errorDetails']); - - const onClickSlackHandler = (): void => { - window.open('https://signoz.io/slack', '_blank'); - }; - const handleReload = (): void => { // Go to home page window.location.href = ROUTES.HOME; }; + + const { isCloudUser: isCloudUserVal } = useGetTenantLicense(); + + const handleSupport = useCallback(() => { + handleContactSupport(isCloudUserVal); + }, [isCloudUserVal]); + return (

-
- -
-
- - - {t('something_went_wrong')} - -
+
+
+ error-cloud-icon +
+
Something went wrong :/
-

{t('contact_if_issue_exists')}

+
+ Our team is getting on top to resolve this. Please reach out to support if + the issue persists. +
-
- +
+ - + +
); diff --git a/frontend/src/pages/Support/Support.styles.scss b/frontend/src/pages/Support/Support.styles.scss index 4d63414a9089..1c47b29f5108 100644 --- a/frontend/src/pages/Support/Support.styles.scss +++ b/frontend/src/pages/Support/Support.styles.scss @@ -1,10 +1,49 @@ .support-page-container { - color: white; - padding-left: 48px; - padding-right: 48px; + max-height: 100vh; + overflow: hidden; - max-width: 1400px; - margin: 64px auto; + .support-page-header { + 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 { @@ -21,6 +60,16 @@ position: relative; 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 { width: 100%; display: flex; @@ -37,6 +86,21 @@ button { 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 { - 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); + } + } + } } } diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index 0b3dad39fff1..19faad6c0812 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -6,9 +6,11 @@ import updateCreditCardApi from 'api/v1/checkout/create'; import { FeatureKeys } from 'constants/features'; import { useNotifications } from 'hooks/useNotifications'; import { + ArrowUpRight, Book, CreditCard, Github, + LifeBuoy, MessageSquare, Slack, X, @@ -45,34 +47,38 @@ const supportChannels = [ { key: 'documentation', name: 'Documentation', - icon: , + icon: , title: 'Find answers in the documentation.', url: 'https://signoz.io/docs/', btnText: 'Visit docs', + isExternal: true, }, { key: 'github', name: 'Github', - icon: , + icon: , title: 'Create an issue on GitHub to report bugs or request new features.', url: 'https://github.com/SigNoz/signoz/issues', btnText: 'Create issue', + isExternal: true, }, { key: 'slack_community', name: 'Slack Community', - icon: , + icon: , title: 'Get support from the SigNoz community on Slack.', url: 'https://signoz.io/slack', btnText: 'Join Slack', + isExternal: true, }, { key: 'chat', name: 'Chat', - icon: , + icon: , title: 'Get quick support directly from the team.', url: '', btnText: 'Launch chat', + isExternal: false, }, ]; @@ -182,38 +188,45 @@ export default function Support(): JSX.Element { return (
-
- Help & Support - +
+
+ + Support +
+
+ +
+
We are here to help in case of questions or issues. Pick the channel that is most convenient for you. - -
+
-
- {supportChannels.map( - (channel): JSX.Element => ( - -
- - {channel.icon} - {channel.name}{' '} - - {channel.title} -
+
+ {supportChannels.map( + (channel): JSX.Element => ( + +
+ + {channel.icon} + {channel.name}{' '} + + {channel.title} +
-
- -
-
- ), - )} +
+ +
+ + ), + )} +
{/* Add Credit Card Modal */}