diff --git a/frontend/package.json b/frontend/package.json index be606378e0..fb1096565e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index f5641d96b6..a457360245 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -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" } diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 75c8d73bcf..bea44d8a18 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -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" } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 9367ed1624..62ed0946be 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -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(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]); diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index ad33f3a83c..b2892e3d38 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -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'), diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index dfda9f8312..b764d609b3 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -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']; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index a66e7e7b4e..80c032a1be 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -39,6 +39,7 @@ const ROUTES = { TRACE_EXPLORER: '/trace-explorer', PIPELINES: '/pipelines', BILLING: '/billing', + SUPPORT: '/support', WORKSPACE_LOCKED: '/workspace-locked', }; diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index 5d813c0ddb..008b64415e 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -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,31 +115,41 @@ function SideNav(): JSX.Element { const isLatestVersion = checkVersionState(currentVersion, latestVersion); - const secondaryMenuItems: MenuItem[] = [ - { - key: SecondaryMenuItemKey.Version, - icon: !isLatestVersion ? ( - - ) : ( - - ), - label: ( - - - {!isCurrentVersionError ? currentVersion : t('n_a')} - - {!isLatestVersion && } - - ), - onClick: onClickVersionHandler, - }, - { - key: SecondaryMenuItemKey.Slack, - icon: , - label: Support, - onClick: onClickSlackHandler, - }, - ]; + if (isCloudUser() || isEECloudUser()) { + secondaryMenuItems = [ + { + key: SecondaryMenuItemKey.Support, + label: 'Support', + icon: , + }, + ]; + } else { + secondaryMenuItems = [ + { + key: SecondaryMenuItemKey.Version, + icon: !isLatestVersion ? ( + + ) : ( + + ), + label: ( + + + {!isCurrentVersionError ? currentVersion : t('n_a')} + + {!isLatestVersion && } + + ), + onClick: onClickVersionHandler, + }, + { + key: SecondaryMenuItemKey.Slack, + icon: , + label: Support, + onClick: onClickSlackHandler, + }, + ]; + } const activeMenuKey = useMemo(() => getActiveMenuKeyFromPath(pathname), [ pathname, @@ -159,6 +172,7 @@ function SideNav(): JSX.Element { mode="vertical" style={styles} items={secondaryMenuItems} + onClick={onClickMenuHandler} /> ); diff --git a/frontend/src/container/SideNav/sideNav.types.ts b/frontend/src/container/SideNav/sideNav.types.ts index d67862e51c..804cad8d18 100644 --- a/frontend/src/container/SideNav/sideNav.types.ts +++ b/frontend/src/container/SideNav/sideNav.types.ts @@ -18,4 +18,5 @@ export interface SidebarItem { export enum SecondaryMenuItemKey { Slack = 'slack', Version = 'version', + Support = 'support', } diff --git a/frontend/src/container/SideNav/styles.ts b/frontend/src/container/SideNav/styles.ts index 35dcc2ed38..5b7989f654 100644 --- a/frontend/src/container/SideNav/styles.ts +++ b/frontend/src/container/SideNav/styles.ts @@ -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; diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 855dd1103d..d749300de8 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -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', }; diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index d8a438bff8..a9fa99f9be 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -85,6 +85,7 @@ export const routesToSkip = [ ROUTES.LIST_ALL_ALERT, ROUTES.PIPELINES, ROUTES.BILLING, + ROUTES.SUPPORT, ROUTES.WORKSPACE_LOCKED, ]; diff --git a/frontend/src/container/TopNav/index.tsx b/frontend/src/container/TopNav/index.tsx index e5f1d6ade3..38d163b3f8 100644 --- a/frontend/src/container/TopNav/index.tsx +++ b/frontend/src/container/TopNav/index.tsx @@ -33,15 +33,19 @@ function TopNav(): JSX.Element | null { [location.pathname], ); + const hideBreadcrumbs = location.pathname === ROUTES.SUPPORT; + if (isSignUpPage || isDisabled) { return null; } return ( - - - + {!hideBreadcrumbs && ( + + + + )} {!isRouteToSkip && ( diff --git a/frontend/src/pages/Support/Support.styles.scss b/frontend/src/pages/Support/Support.styles.scss new file mode 100644 index 0000000000..e298f74d8a --- /dev/null +++ b/frontend/src/pages/Support/Support.styles.scss @@ -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%; + } +} diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx new file mode 100644 index 0000000000..61d6c11c38 --- /dev/null +++ b/frontend/src/pages/Support/Support.tsx @@ -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: , + title: 'Find answers in the documentation.', + url: 'https://signoz.io/docs/', + btnText: 'Visit docs', + }, + { + key: 'github', + name: 'Github', + icon: , + 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: , + title: 'Get support from the SigNoz community on Slack.', + url: 'https://signoz.io/slack', + btnText: 'Join Slack', + }, + { + key: 'chat', + name: 'Chat', + icon: , + title: 'Get quick support directly from the team.', + url: '', + btnText: 'Launch chat', + }, + { + key: 'schedule_call', + name: 'Schedule a call', + icon: , + 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: , + 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 ( +
+
+ 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} +
+ +
+ +
+
+ ), + )} +
+
+ ); +} diff --git a/frontend/src/pages/Support/index.tsx b/frontend/src/pages/Support/index.tsx new file mode 100644 index 0000000000..e16e7fedc9 --- /dev/null +++ b/frontend/src/pages/Support/index.tsx @@ -0,0 +1,3 @@ +import Support from './Support'; + +export default Support; diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index 55312a11dc..0ab9e6fca7 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -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, diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 1ca3064720..b4de0e3110 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -81,5 +81,6 @@ export const routePermission: Record = { GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'], BILLING: ['ADMIN', 'EDITOR', 'VIEWER'], + SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'], SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'], }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b8b7025757..bb32618a25 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"