mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 14:18:58 +08:00
feat: add support page (#3768)
* feat: add support page * feat: handle chat, slack connect and book a call functionality
This commit is contained in:
parent
6e20fbb174
commit
814431e3a8
@ -71,6 +71,7 @@
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "0.288.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"papaparse": "5.4.1",
|
||||
"react": "18.2.0",
|
||||
|
@ -36,5 +36,6 @@
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
|
@ -23,16 +23,16 @@ import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app';
|
||||
import AppReducer, { User } from 'types/reducer/app';
|
||||
import { extractDomain, isCloudUser } from 'utils/app';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
import { trackPageView } from 'utils/segmentAnalytics';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
import defaultRoutes from './routes';
|
||||
import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const themeConfig = useThemeConfig();
|
||||
const { data } = useLicense();
|
||||
const [routes, setRoutes] = useState(defaultRoutes);
|
||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
||||
AppState,
|
||||
AppReducer
|
||||
@ -136,6 +136,13 @@ function App(): JSX.Element {
|
||||
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
|
||||
if (isCloudUserVal || isEECloudUser()) {
|
||||
const newRoutes = [...routes, SUPPORT_ROUTE];
|
||||
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoggedInState, isOnBasicPlan, user]);
|
||||
|
||||
|
@ -158,6 +158,10 @@ export const BillingPage = Loadable(
|
||||
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
|
||||
);
|
||||
|
||||
export const SupportPage = Loadable(
|
||||
() => import(/* webpackChunkName: "SupportPage" */ 'pages/Support'),
|
||||
);
|
||||
|
||||
export const WorkspaceBlocked = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
SignupPage,
|
||||
SomethingWentWrong,
|
||||
StatusPage,
|
||||
SupportPage,
|
||||
TraceDetail,
|
||||
TraceFilter,
|
||||
TracesExplorer,
|
||||
@ -287,7 +288,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'PIPELINES',
|
||||
isPrivate: true,
|
||||
},
|
||||
|
||||
{
|
||||
path: ROUTES.BILLING,
|
||||
exact: true,
|
||||
@ -304,6 +304,14 @@ const routes: AppRoutes[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
path: ROUTES.SUPPORT,
|
||||
exact: true,
|
||||
component: SupportPage,
|
||||
key: 'SUPPORT',
|
||||
isPrivate: true,
|
||||
};
|
||||
|
||||
export interface AppRoutes {
|
||||
component: RouteProps['component'];
|
||||
path: RouteProps['path'];
|
||||
|
@ -39,6 +39,7 @@ const ROUTES = {
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
PIPELINES: '/pipelines',
|
||||
BILLING: '/billing',
|
||||
SUPPORT: '/support',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||
import history from 'lib/history';
|
||||
import { LifeBuoy } from 'lucide-react';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -13,7 +14,7 @@ import { useLocation } from 'react-router-dom';
|
||||
import { sideBarCollapse } from 'store/actions/app';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { checkVersionState, isCloudUser } from 'utils/app';
|
||||
import { checkVersionState, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import { routeConfig, styles } from './config';
|
||||
import { getQueryString } from './helper';
|
||||
@ -45,6 +46,8 @@ function SideNav(): JSX.Element {
|
||||
|
||||
const { data } = useLicense();
|
||||
|
||||
let secondaryMenuItems: MenuItem[] = [];
|
||||
|
||||
const isOnBasicPlan =
|
||||
data?.payload?.licenses?.some(
|
||||
(license) =>
|
||||
@ -112,7 +115,16 @@ function SideNav(): JSX.Element {
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const secondaryMenuItems: MenuItem[] = [
|
||||
if (isCloudUser() || isEECloudUser()) {
|
||||
secondaryMenuItems = [
|
||||
{
|
||||
key: SecondaryMenuItemKey.Support,
|
||||
label: 'Support',
|
||||
icon: <LifeBuoy />,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
secondaryMenuItems = [
|
||||
{
|
||||
key: SecondaryMenuItemKey.Version,
|
||||
icon: !isLatestVersion ? (
|
||||
@ -137,6 +149,7 @@ function SideNav(): JSX.Element {
|
||||
onClick: onClickSlackHandler,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const activeMenuKey = useMemo(() => getActiveMenuKeyFromPath(pathname), [
|
||||
pathname,
|
||||
@ -159,6 +172,7 @@ function SideNav(): JSX.Element {
|
||||
mode="vertical"
|
||||
style={styles}
|
||||
items={secondaryMenuItems}
|
||||
onClick={onClickMenuHandler}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
|
@ -18,4 +18,5 @@ export interface SidebarItem {
|
||||
export enum SecondaryMenuItemKey {
|
||||
Slack = 'slack',
|
||||
Version = 'version',
|
||||
Support = 'support',
|
||||
}
|
||||
|
@ -26,8 +26,6 @@ export const StyledPrimaryMenu = styled(Menu)`
|
||||
export const StyledSecondaryMenu = styled(Menu)`
|
||||
&&& {
|
||||
:not(.ant-menu-inline-collapsed) > .ant-menu-item {
|
||||
padding-inline: 48px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -25,6 +25,7 @@ const breadcrumbNameMap = {
|
||||
[ROUTES.LIVE_LOGS]: 'Live View',
|
||||
[ROUTES.PIPELINES]: 'Pipelines',
|
||||
[ROUTES.BILLING]: 'Billing',
|
||||
[ROUTES.SUPPORT]: 'Support',
|
||||
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
|
||||
};
|
||||
|
||||
|
@ -85,6 +85,7 @@ export const routesToSkip = [
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.PIPELINES,
|
||||
ROUTES.BILLING,
|
||||
ROUTES.SUPPORT,
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
];
|
||||
|
||||
|
@ -33,15 +33,19 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const hideBreadcrumbs = location.pathname === ROUTES.SUPPORT;
|
||||
|
||||
if (isSignUpPage || isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{!hideBreadcrumbs && (
|
||||
<Col span={16}>
|
||||
<ShowBreadcrumbs />
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{!isRouteToSkip && (
|
||||
<Col span={8}>
|
||||
|
53
frontend/src/pages/Support/Support.styles.scss
Normal file
53
frontend/src/pages/Support/Support.styles.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.support-page-container {
|
||||
color: white;
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.support-channels {
|
||||
margin: 48px 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.support-channel {
|
||||
flex: 0 0 calc(33.333% - 32px);
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
|
||||
.support-channel-title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.support-channel-action {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
width: calc(100% - 48px);
|
||||
|
||||
button {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1440px) {
|
||||
.support-channel {
|
||||
min-height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
.support-page-container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
169
frontend/src/pages/Support/Support.tsx
Normal file
169
frontend/src/pages/Support/Support.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import './Support.styles.scss';
|
||||
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import {
|
||||
Book,
|
||||
Cable,
|
||||
Calendar,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Slack,
|
||||
} from 'lucide-react';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface Channel {
|
||||
key: any;
|
||||
name?: string;
|
||||
icon?: JSX.Element;
|
||||
title?: string;
|
||||
url: any;
|
||||
btnText?: string;
|
||||
}
|
||||
|
||||
const channelsMap = {
|
||||
documentation: 'documentation',
|
||||
github: 'github',
|
||||
slack_community: 'slack_community',
|
||||
chat: 'chat',
|
||||
schedule_call: 'schedule_call',
|
||||
slack_connect: 'slack_connect',
|
||||
};
|
||||
|
||||
const supportChannels = [
|
||||
{
|
||||
key: 'documentation',
|
||||
name: 'Documentation',
|
||||
icon: <Book />,
|
||||
title: 'Find answers in the documentation.',
|
||||
url: 'https://signoz.io/docs/',
|
||||
btnText: 'Visit docs',
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
name: 'Github',
|
||||
icon: <Github />,
|
||||
title: 'Create an issue on GitHub to report bugs or request new features.',
|
||||
url: 'https://github.com/SigNoz/signoz/issues',
|
||||
btnText: 'Create issue',
|
||||
},
|
||||
{
|
||||
key: 'slack_community',
|
||||
name: 'Slack Community',
|
||||
icon: <Slack />,
|
||||
title: 'Get support from the SigNoz community on Slack.',
|
||||
url: 'https://signoz.io/slack',
|
||||
btnText: 'Join Slack',
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
name: 'Chat',
|
||||
icon: <MessageSquare />,
|
||||
title: 'Get quick support directly from the team.',
|
||||
url: '',
|
||||
btnText: 'Launch chat',
|
||||
},
|
||||
{
|
||||
key: 'schedule_call',
|
||||
name: 'Schedule a call',
|
||||
icon: <Calendar />,
|
||||
title: 'Schedule a call with the founders.',
|
||||
url: 'https://calendly.com/pranay-signoz/signoz-intro-calls',
|
||||
btnText: 'Schedule call',
|
||||
},
|
||||
{
|
||||
key: 'slack_connect',
|
||||
name: 'Slack Connect',
|
||||
icon: <Cable />,
|
||||
title: 'Get a dedicated support channel for your team.',
|
||||
url: '',
|
||||
btnText: 'Request Slack connect',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Support(): JSX.Element {
|
||||
const handleChannelWithRedirects = (url: string): void => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const handleSlackConnectRequest = (): void => {
|
||||
const recipient = 'support@signoz.io';
|
||||
const subject = 'Slack Connect Request';
|
||||
const body = `I'd like to request a dedicated Slack Connect channel for me and my team. Users (emails) to include besides mine:`;
|
||||
|
||||
// Create the mailto link
|
||||
const mailtoLink = `mailto:${recipient}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
// Open the default email client
|
||||
window.location.href = mailtoLink;
|
||||
};
|
||||
|
||||
const handleChat = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (window.Intercom) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Intercom('show');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChannelClick = (channel: Channel): void => {
|
||||
switch (channel.key) {
|
||||
case channelsMap.documentation:
|
||||
case channelsMap.github:
|
||||
case channelsMap.slack_community:
|
||||
case channelsMap.schedule_call:
|
||||
handleChannelWithRedirects(channel.url);
|
||||
break;
|
||||
case channelsMap.chat:
|
||||
handleChat();
|
||||
break;
|
||||
case channelsMap.slack_connect:
|
||||
handleSlackConnectRequest();
|
||||
break;
|
||||
default:
|
||||
handleChannelWithRedirects('https://signoz.io/slack');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="support-page-container">
|
||||
<div className="support-page-header">
|
||||
<Title level={3}> Support </Title>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
We are here to help in case of questions or issues. Pick the channel that
|
||||
is most convenient for you.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="support-channels">
|
||||
{supportChannels.map(
|
||||
(channel): JSX.Element => (
|
||||
<Card className="support-channel" key={channel.key}>
|
||||
<div className="support-channel-content">
|
||||
<Title ellipsis level={5} className="support-channel-title">
|
||||
{channel.icon}
|
||||
{channel.name}{' '}
|
||||
</Title>
|
||||
<Text> {channel.title} </Text>
|
||||
</div>
|
||||
|
||||
<div className="support-channel-action">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void => handleChannelClick(channel)}
|
||||
>
|
||||
<Text ellipsis>{channel.btnText} </Text>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
3
frontend/src/pages/Support/index.tsx
Normal file
3
frontend/src/pages/Support/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import Support from './Support';
|
||||
|
||||
export default Support;
|
@ -18,6 +18,12 @@ export const isCloudUser = (): boolean => {
|
||||
return hostname?.endsWith('signoz.cloud');
|
||||
};
|
||||
|
||||
export const isEECloudUser = (): boolean => {
|
||||
const { hostname } = window.location;
|
||||
|
||||
return hostname?.endsWith('signoz.io');
|
||||
};
|
||||
|
||||
export const checkVersionState = (
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
|
@ -81,5 +81,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
@ -9882,6 +9882,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lucide-react@0.288.0:
|
||||
version "0.288.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.288.0.tgz#cc9fda209fe4ec6e572efca38f7d3e3cde7422eb"
|
||||
integrity sha512-ikhb/9LOkq9orPoLV9lLC4UYyoXQycBhIgH7H59ahOkk0mkcAqkD52m84RXedE/qVqZHW8rEJquInT4xGmsNqw==
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"
|
||||
|
Loading…
x
Reference in New Issue
Block a user