feat: add support page (#3768)

* feat: add support page

* feat: handle chat, slack connect and book a call functionality
This commit is contained in:
Yunus M 2023-10-19 11:52:58 +05:30 committed by GitHub
parent 6e20fbb174
commit 814431e3a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 314 additions and 35 deletions

View File

@ -71,6 +71,7 @@
"less": "^4.1.2", "less": "^4.1.2",
"less-loader": "^10.2.0", "less-loader": "^10.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "0.288.0",
"mini-css-extract-plugin": "2.4.5", "mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1", "papaparse": "5.4.1",
"react": "18.2.0", "react": "18.2.0",

View File

@ -36,5 +36,6 @@
"PASSWORD_RESET": "SigNoz | Password Reset", "PASSWORD_RESET": "SigNoz | Password Reset",
"LIST_LICENSES": "SigNoz | List of Licenses", "LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked", "WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz" "DEFAULT": "Open source Observability Platform | SigNoz"
} }

View File

@ -36,5 +36,6 @@
"PASSWORD_RESET": "SigNoz | Password Reset", "PASSWORD_RESET": "SigNoz | Password Reset",
"LIST_LICENSES": "SigNoz | List of Licenses", "LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked", "WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz" "DEFAULT": "Open source Observability Platform | SigNoz"
} }

View File

@ -23,16 +23,16 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app'; import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app';
import AppReducer, { User } from 'types/reducer/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 { trackPageView } from 'utils/segmentAnalytics';
import PrivateRoute from './Private'; import PrivateRoute from './Private';
import defaultRoutes from './routes'; import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
function App(): JSX.Element { function App(): JSX.Element {
const themeConfig = useThemeConfig(); const themeConfig = useThemeConfig();
const { data } = useLicense(); const { data } = useLicense();
const [routes, setRoutes] = useState(defaultRoutes); const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector< const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
AppState, AppState,
AppReducer AppReducer
@ -136,6 +136,13 @@ function App(): JSX.Element {
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING); const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
setRoutes(newRoutes); setRoutes(newRoutes);
} }
if (isCloudUserVal || isEECloudUser()) {
const newRoutes = [...routes, SUPPORT_ROUTE];
setRoutes(newRoutes);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoggedInState, isOnBasicPlan, user]); }, [isLoggedInState, isOnBasicPlan, user]);

View File

@ -158,6 +158,10 @@ export const BillingPage = Loadable(
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'), () => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
); );
export const SupportPage = Loadable(
() => import(/* webpackChunkName: "SupportPage" */ 'pages/Support'),
);
export const WorkspaceBlocked = Loadable( export const WorkspaceBlocked = Loadable(
() => () =>
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'), import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),

View File

@ -34,6 +34,7 @@ import {
SignupPage, SignupPage,
SomethingWentWrong, SomethingWentWrong,
StatusPage, StatusPage,
SupportPage,
TraceDetail, TraceDetail,
TraceFilter, TraceFilter,
TracesExplorer, TracesExplorer,
@ -287,7 +288,6 @@ const routes: AppRoutes[] = [
key: 'PIPELINES', key: 'PIPELINES',
isPrivate: true, isPrivate: true,
}, },
{ {
path: ROUTES.BILLING, path: ROUTES.BILLING,
exact: true, 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 { export interface AppRoutes {
component: RouteProps['component']; component: RouteProps['component'];
path: RouteProps['path']; path: RouteProps['path'];

View File

@ -39,6 +39,7 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer', TRACE_EXPLORER: '/trace-explorer',
PIPELINES: '/pipelines', PIPELINES: '/pipelines',
BILLING: '/billing', BILLING: '/billing',
SUPPORT: '/support',
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
}; };

View File

@ -6,6 +6,7 @@ import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense'; import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import history from 'lib/history'; import history from 'lib/history';
import { LifeBuoy } from 'lucide-react';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -13,7 +14,7 @@ import { useLocation } from 'react-router-dom';
import { sideBarCollapse } from 'store/actions/app'; import { sideBarCollapse } from 'store/actions/app';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app'; 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 { routeConfig, styles } from './config';
import { getQueryString } from './helper'; import { getQueryString } from './helper';
@ -45,6 +46,8 @@ function SideNav(): JSX.Element {
const { data } = useLicense(); const { data } = useLicense();
let secondaryMenuItems: MenuItem[] = [];
const isOnBasicPlan = const isOnBasicPlan =
data?.payload?.licenses?.some( data?.payload?.licenses?.some(
(license) => (license) =>
@ -112,31 +115,41 @@ function SideNav(): JSX.Element {
const isLatestVersion = checkVersionState(currentVersion, latestVersion); const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const secondaryMenuItems: MenuItem[] = [ if (isCloudUser() || isEECloudUser()) {
{ secondaryMenuItems = [
key: SecondaryMenuItemKey.Version, {
icon: !isLatestVersion ? ( key: SecondaryMenuItemKey.Support,
<WarningOutlined style={{ color: '#E87040' }} /> label: 'Support',
) : ( icon: <LifeBuoy />,
<CheckCircleTwoTone twoToneColor={['#D5F2BB', '#1f1f1f']} /> },
), ];
label: ( } else {
<MenuLabelContainer> secondaryMenuItems = [
<StyledText ellipsis> {
{!isCurrentVersionError ? currentVersion : t('n_a')} key: SecondaryMenuItemKey.Version,
</StyledText> icon: !isLatestVersion ? (
{!isLatestVersion && <RedDot />} <WarningOutlined style={{ color: '#E87040' }} />
</MenuLabelContainer> ) : (
), <CheckCircleTwoTone twoToneColor={['#D5F2BB', '#1f1f1f']} />
onClick: onClickVersionHandler, ),
}, label: (
{ <MenuLabelContainer>
key: SecondaryMenuItemKey.Slack, <StyledText ellipsis>
icon: <Slack />, {!isCurrentVersionError ? currentVersion : t('n_a')}
label: <StyledText>Support</StyledText>, </StyledText>
onClick: onClickSlackHandler, {!isLatestVersion && <RedDot />}
}, </MenuLabelContainer>
]; ),
onClick: onClickVersionHandler,
},
{
key: SecondaryMenuItemKey.Slack,
icon: <Slack />,
label: <StyledText>Support</StyledText>,
onClick: onClickSlackHandler,
},
];
}
const activeMenuKey = useMemo(() => getActiveMenuKeyFromPath(pathname), [ const activeMenuKey = useMemo(() => getActiveMenuKeyFromPath(pathname), [
pathname, pathname,
@ -159,6 +172,7 @@ function SideNav(): JSX.Element {
mode="vertical" mode="vertical"
style={styles} style={styles}
items={secondaryMenuItems} items={secondaryMenuItems}
onClick={onClickMenuHandler}
/> />
</Sider> </Sider>
); );

View File

@ -18,4 +18,5 @@ export interface SidebarItem {
export enum SecondaryMenuItemKey { export enum SecondaryMenuItemKey {
Slack = 'slack', Slack = 'slack',
Version = 'version', Version = 'version',
Support = 'support',
} }

View File

@ -26,8 +26,6 @@ export const StyledPrimaryMenu = styled(Menu)`
export const StyledSecondaryMenu = styled(Menu)` export const StyledSecondaryMenu = styled(Menu)`
&&& { &&& {
:not(.ant-menu-inline-collapsed) > .ant-menu-item { :not(.ant-menu-inline-collapsed) > .ant-menu-item {
padding-inline: 48px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -25,6 +25,7 @@ const breadcrumbNameMap = {
[ROUTES.LIVE_LOGS]: 'Live View', [ROUTES.LIVE_LOGS]: 'Live View',
[ROUTES.PIPELINES]: 'Pipelines', [ROUTES.PIPELINES]: 'Pipelines',
[ROUTES.BILLING]: 'Billing', [ROUTES.BILLING]: 'Billing',
[ROUTES.SUPPORT]: 'Support',
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked', [ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
}; };

View File

@ -85,6 +85,7 @@ export const routesToSkip = [
ROUTES.LIST_ALL_ALERT, ROUTES.LIST_ALL_ALERT,
ROUTES.PIPELINES, ROUTES.PIPELINES,
ROUTES.BILLING, ROUTES.BILLING,
ROUTES.SUPPORT,
ROUTES.WORKSPACE_LOCKED, ROUTES.WORKSPACE_LOCKED,
]; ];

View File

@ -33,15 +33,19 @@ function TopNav(): JSX.Element | null {
[location.pathname], [location.pathname],
); );
const hideBreadcrumbs = location.pathname === ROUTES.SUPPORT;
if (isSignUpPage || isDisabled) { if (isSignUpPage || isDisabled) {
return null; return null;
} }
return ( return (
<Container> <Container>
<Col span={16}> {!hideBreadcrumbs && (
<ShowBreadcrumbs /> <Col span={16}>
</Col> <ShowBreadcrumbs />
</Col>
)}
{!isRouteToSkip && ( {!isRouteToSkip && (
<Col span={8}> <Col span={8}>

View 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%;
}
}

View 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>
);
}

View File

@ -0,0 +1,3 @@
import Support from './Support';
export default Support;

View File

@ -18,6 +18,12 @@ export const isCloudUser = (): boolean => {
return hostname?.endsWith('signoz.cloud'); return hostname?.endsWith('signoz.cloud');
}; };
export const isEECloudUser = (): boolean => {
const { hostname } = window.location;
return hostname?.endsWith('signoz.io');
};
export const checkVersionState = ( export const checkVersionState = (
currentVersion: string, currentVersion: string,
latestVersion: string, latestVersion: string,

View File

@ -81,5 +81,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'], WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'], BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'], SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };

View File

@ -9882,6 +9882,11 @@ lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" 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: lz-string@^1.4.4:
version "1.5.0" version "1.5.0"
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"