From ae05e2cbf40abc24c47a17445503427a6c6ddbd2 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:55:50 +0100 Subject: [PATCH 01/12] prototype --- web/messages/en/components.json | 1 + web/messages/en/support.json | 23 ++ web/project.inlang/settings.json | 3 +- web/src/pages/SupportPage/SupportPage.tsx | 202 ++++++++++++++++++ web/src/pages/SupportPage/styles.scss | 122 +++++++++++ web/src/routeTree.gen.ts | 22 ++ .../routes/_authorized/_default/support.tsx | 6 + web/src/shared/api/api.ts | 4 + .../components/Navigation/Navigation.tsx | 6 + web/src/shared/constants.ts | 7 + web/src/shared/defguard-ui | 2 +- 11 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 web/messages/en/support.json create mode 100644 web/src/pages/SupportPage/SupportPage.tsx create mode 100644 web/src/pages/SupportPage/styles.scss create mode 100644 web/src/routes/_authorized/_default/support.tsx diff --git a/web/messages/en/components.json b/web/messages/en/components.json index 5237298d31..5c9ca7845a 100644 --- a/web/messages/en/components.json +++ b/web/messages/en/components.json @@ -27,6 +27,7 @@ "cmp_nav_item_overview": "VPN Overview", "cmp_nav_item_settings": "Settings", "cmp_nav_item_edges": "Edge Components", + "cmp_nav_item_support": "Support", "cmp_nav_item_activity_log": "Activity log", "cmp_webhook_event_user_delete": "User deleted", "cmp_webhook_event_user_add": "New user created", diff --git a/web/messages/en/support.json b/web/messages/en/support.json new file mode 100644 index 0000000000..8090cace5c --- /dev/null +++ b/web/messages/en/support.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "support_page_title": "Support", + "support_page_subtitle": "When you need support or must supply diagnostic information to our team, please use one of the options below.", + "support_page_docs_title": "Have questions? Check our documentation first.", + "support_page_docs_desc": "Before contacting or submitting any issues to GitHub please get familiar with Defguard documentation available.", + "support_page_docs_btn": "Go to documentation", + "support_page_bug_title": "Report a bug", + "support_page_bug_desc": "We aim to respond to all bug reports as quickly as possible and prioritize them based on severity before adding them to our development backlog. To give us more context, you can optionally download the support data and/or log file and attach it to your bug report.", + "support_page_bug_btn_report": "Report on Github", + "support_page_bug_btn_download": "Download", + "support_page_bug_btn_download_support_data": "Download support data", + "support_page_bug_btn_download_logs": "Download logs", + "support_page_feature_title": "Request feature", + "support_page_feature_desc": "We grow with the help of our community. If you have an idea or a missing feature to suggest, please share it \u2014 we'll review it.", + "support_page_feature_btn": "Submit on Github", + "support_page_email_title": "Contact us by email", + "support_page_email_desc": "For any additional requests, reach out to us at", + "support_page_assistance_title": "Need Assistance?", + "support_page_assistance_desc": "Let us know your issue or arrange a meeting with our support team.", + "support_page_assistance_btn_ticket": "Open a support ticket", + "support_page_assistance_btn_call": "Schedule a call with our support team" +} diff --git a/web/project.inlang/settings.json b/web/project.inlang/settings.json index 862e3ed1fb..ded05da087 100644 --- a/web/project.inlang/settings.json +++ b/web/project.inlang/settings.json @@ -29,7 +29,8 @@ "./messages/{locale}/initial_wizard.json", "./messages/{locale}/migration_wizard.json", "./messages/{locale}/api-error.json", - "./messages/{locale}/flow_end.json" + "./messages/{locale}/flow_end.json", + "./messages/{locale}/support.json" ] } } diff --git a/web/src/pages/SupportPage/SupportPage.tsx b/web/src/pages/SupportPage/SupportPage.tsx new file mode 100644 index 0000000000..40c18492ae --- /dev/null +++ b/web/src/pages/SupportPage/SupportPage.tsx @@ -0,0 +1,202 @@ +import './styles.scss'; +import type { ReactNode } from 'react'; +import { m } from '../../paraglide/messages'; +import api from '../../shared/api/api'; +import { Page } from '../../shared/components/Page/Page'; +import { SettingsCard } from '../../shared/components/SettingsCard/SettingsCard'; +import { SettingsHeader } from '../../shared/components/SettingsHeader/SettingsHeader'; +import { SettingsLayout } from '../../shared/components/SettingsLayout/SettingsLayout'; +import { Button } from '../../shared/defguard-ui/components/Button/Button'; +import { ButtonMenu } from '../../shared/defguard-ui/components/ButtonMenu/MenuButton'; +import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; +import { Icon } from '../../shared/defguard-ui/components/Icon'; +import type { IconKindValue } from '../../shared/defguard-ui/components/Icon/icon-types'; +import docIllustration from '../../shared/defguard-ui/components/SectionSelect/assets/manual-user.png'; +import { externalLink } from '../../shared/constants'; +import { downloadFile, downloadText } from '../../shared/utils/download'; + +export const SupportPage = () => { + return ( + + + + + +
+ +
+

{m.support_page_docs_desc()}

+
+
+
+ + } + > +
+
+
+ + +
+
+
+ + + {m.support_page_email_desc()}{' '} + support@defguard.net + + } + /> + + +
+
+
+
+
+
+ ); +}; + +/** + * Renders the bug section description with the "you can optionally download" + * portion in bold, matching the Figma design. + */ +const BugDescription = () => { + const full = m.support_page_bug_desc(); + const boldPhrase = 'you can optionally download'; + const idx = full.indexOf(boldPhrase); + if (idx === -1) return <>{full}; + const before = full.slice(0, idx); + const after = full.slice(idx + boldPhrase.length); + return ( + <> + {before} + {boldPhrase} + {after} + + ); +}; + +interface SupportSectionProps { + icon: IconKindValue; + title: string; + subtitle?: ReactNode; + subtitleDark?: boolean; + children?: ReactNode; +} + +const SupportSection = ({ + icon, + title, + subtitle, + subtitleDark, + children, +}: SupportSectionProps) => { + return ( +
+
+
+ +
+
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+ {children} +
+
+ ); +}; diff --git a/web/src/pages/SupportPage/styles.scss b/web/src/pages/SupportPage/styles.scss new file mode 100644 index 0000000000..aa9abe3c42 --- /dev/null +++ b/web/src/pages/SupportPage/styles.scss @@ -0,0 +1,122 @@ +#support-page-content { + width: 784px; + + .support-section { + display: grid; + grid-template-columns: 32px 1fr; + column-gap: var(--spacing-xl); + align-items: start; + padding: var(--spacing-xl) 0; + + &:first-child { + padding-top: 0; + } + + &:last-child { + padding-bottom: 0; + } + } + + .support-section-icon { + position: relative; + display: grid; + width: 32px; + height: 32px; + place-items: center; + + & > * { + grid-row: 1; + grid-column: 1; + } + + .bg { + width: 100%; + height: 100%; + background-color: var(--bg-action-muted); + border-radius: var(--radius-full); + } + + .icon path { + fill: var(--fg-action); + } + } + + .support-section-content { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); + + .section-header { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + } + + .section-title { + font: var(--t-body-primary-500); + color: var(--fg-default); + margin: 0; + } + + > .section-header > p:not(.section-title) { + font: var(--t-body-sm-400); + color: var(--fg-muted) !important; + margin: 0; + + &.subtitle-dark { + color: var(--fg-default) !important; + } + + a { + color: var(--fg-action); + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + + strong { + font-weight: 500; + } + } + } + + .doc-highlight { + background: var(--bg-action-muted); + border-radius: var(--radius-md); + padding: var(--spacing-xl); + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacing-2xl); + + .doc-highlight-illustration { + width: 80px; + height: auto; + flex-shrink: 0; + } + + .doc-highlight-content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-lg); + flex: 1; + + p { + margin: 0; + font: var(--t-body-sm-400); + color: var(--fg-default) !important; + } + } + } + + .support-controls { + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: stretch; + gap: var(--spacing-md); + } +} diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 443c5dd0e8..aba953680d 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -37,6 +37,7 @@ import { Route as AuthorizedWizardAddLocationRouteImport } from './routes/_autho import { Route as AuthorizedWizardAddExternalOpenidRouteImport } from './routes/_authorized/_wizard/add-external-openid' import { Route as AuthorizedDefaultWebhooksRouteImport } from './routes/_authorized/_default/webhooks' import { Route as AuthorizedDefaultUsersRouteImport } from './routes/_authorized/_default/users' +import { Route as AuthorizedDefaultSupportRouteImport } from './routes/_authorized/_default/support' import { Route as AuthorizedDefaultOpenidRouteImport } from './routes/_authorized/_default/openid' import { Route as AuthorizedDefaultNetworkDevicesRouteImport } from './routes/_authorized/_default/network-devices' import { Route as AuthorizedDefaultGroupsRouteImport } from './routes/_authorized/_default/groups' @@ -211,6 +212,12 @@ const AuthorizedDefaultUsersRoute = AuthorizedDefaultUsersRouteImport.update({ path: '/users', getParentRoute: () => AuthorizedDefaultRoute, } as any) +const AuthorizedDefaultSupportRoute = + AuthorizedDefaultSupportRouteImport.update({ + id: '/support', + path: '/support', + getParentRoute: () => AuthorizedDefaultRoute, + } as any) const AuthorizedDefaultOpenidRoute = AuthorizedDefaultOpenidRouteImport.update({ id: '/openid', path: '/openid', @@ -410,6 +417,7 @@ export interface FileRoutesByFullPath { '/groups': typeof AuthorizedDefaultGroupsRoute '/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/openid': typeof AuthorizedDefaultOpenidRoute + '/support': typeof AuthorizedDefaultSupportRoute '/users': typeof AuthorizedDefaultUsersRoute '/webhooks': typeof AuthorizedDefaultWebhooksRoute '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute @@ -467,6 +475,7 @@ export interface FileRoutesByTo { '/groups': typeof AuthorizedDefaultGroupsRoute '/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/openid': typeof AuthorizedDefaultOpenidRoute + '/support': typeof AuthorizedDefaultSupportRoute '/users': typeof AuthorizedDefaultUsersRoute '/webhooks': typeof AuthorizedDefaultWebhooksRoute '/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute @@ -528,6 +537,7 @@ export interface FileRoutesById { '/_authorized/_default/groups': typeof AuthorizedDefaultGroupsRoute '/_authorized/_default/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/_authorized/_default/openid': typeof AuthorizedDefaultOpenidRoute + '/_authorized/_default/support': typeof AuthorizedDefaultSupportRoute '/_authorized/_default/users': typeof AuthorizedDefaultUsersRoute '/_authorized/_default/webhooks': typeof AuthorizedDefaultWebhooksRoute '/_authorized/_wizard/add-external-openid': typeof AuthorizedWizardAddExternalOpenidRoute @@ -588,6 +598,7 @@ export interface FileRouteTypes { | '/groups' | '/network-devices' | '/openid' + | '/support' | '/users' | '/webhooks' | '/add-external-openid' @@ -645,6 +656,7 @@ export interface FileRouteTypes { | '/groups' | '/network-devices' | '/openid' + | '/support' | '/users' | '/webhooks' | '/add-external-openid' @@ -705,6 +717,7 @@ export interface FileRouteTypes { | '/_authorized/_default/groups' | '/_authorized/_default/network-devices' | '/_authorized/_default/openid' + | '/_authorized/_default/support' | '/_authorized/_default/users' | '/_authorized/_default/webhooks' | '/_authorized/_wizard/add-external-openid' @@ -956,6 +969,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedDefaultUsersRouteImport parentRoute: typeof AuthorizedDefaultRoute } + '/_authorized/_default/support': { + id: '/_authorized/_default/support' + path: '/support' + fullPath: '/support' + preLoaderRoute: typeof AuthorizedDefaultSupportRouteImport + parentRoute: typeof AuthorizedDefaultRoute + } '/_authorized/_default/openid': { id: '/_authorized/_default/openid' path: '/openid' @@ -1175,6 +1195,7 @@ interface AuthorizedDefaultRouteChildren { AuthorizedDefaultGroupsRoute: typeof AuthorizedDefaultGroupsRoute AuthorizedDefaultNetworkDevicesRoute: typeof AuthorizedDefaultNetworkDevicesRoute AuthorizedDefaultOpenidRoute: typeof AuthorizedDefaultOpenidRoute + AuthorizedDefaultSupportRoute: typeof AuthorizedDefaultSupportRoute AuthorizedDefaultUsersRoute: typeof AuthorizedDefaultUsersRoute AuthorizedDefaultWebhooksRoute: typeof AuthorizedDefaultWebhooksRoute AuthorizedDefaultAclAddAliasRoute: typeof AuthorizedDefaultAclAddAliasRoute @@ -1210,6 +1231,7 @@ const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultGroupsRoute: AuthorizedDefaultGroupsRoute, AuthorizedDefaultNetworkDevicesRoute: AuthorizedDefaultNetworkDevicesRoute, AuthorizedDefaultOpenidRoute: AuthorizedDefaultOpenidRoute, + AuthorizedDefaultSupportRoute: AuthorizedDefaultSupportRoute, AuthorizedDefaultUsersRoute: AuthorizedDefaultUsersRoute, AuthorizedDefaultWebhooksRoute: AuthorizedDefaultWebhooksRoute, AuthorizedDefaultAclAddAliasRoute: AuthorizedDefaultAclAddAliasRoute, diff --git a/web/src/routes/_authorized/_default/support.tsx b/web/src/routes/_authorized/_default/support.tsx new file mode 100644 index 0000000000..5589a6e367 --- /dev/null +++ b/web/src/routes/_authorized/_default/support.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { SupportPage } from '../../../pages/SupportPage/SupportPage'; + +export const Route = createFileRoute('/_authorized/_default/support')({ + component: SupportPage, +}); diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 439b964869..8f50498978 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -530,6 +530,10 @@ const api = { fetchPage(`/activity_log`, data), info: () => client.get('/info'), getLicenseInfo: () => client.get(`/enterprise_info`), + support: { + getSupportData: () => client.get('/support/configuration'), + getLogs: () => client.get('/support/logs'), + }, } as const; export default api; diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index f7cc59a092..c148b2fdc0 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -150,6 +150,12 @@ const navigationConfig: NavGroupProps[] = [ label: m.cmp_nav_item_settings(), link: '/settings', }, + { + id: 'support', + icon: 'support', + label: m.cmp_nav_item_support(), + link: '/support', + }, { id: 'edges', icon: 'globe', diff --git a/web/src/shared/constants.ts b/web/src/shared/constants.ts index f6e8afa748..216f597fc9 100644 --- a/web/src/shared/constants.ts +++ b/web/src/shared/constants.ts @@ -6,6 +6,13 @@ export const externalLink = { pricing: 'https://defguard.net/pricing', download: 'https://defguard.net/download', sales: 'mailto:sales@defguard.net', + support: 'https://defguard.net/support/', + scheduleCall: 'https://calendly.com/defguard', + }, + github: { + bugReport: 'https://github.com/DefGuard/defguard/issues/new?template=bug_report.md', + featureRequest: + 'https://github.com/DefGuard/defguard/issues/new?template=feature_request.md', }, client: { desktop: { diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index e77b9152d9..c65255fd36 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit e77b9152d960c7ce25c2ed06504f4e94238dfaaa +Subproject commit c65255fd36f0b82b2136b61f692e587fad644d54 From da1d16399aa83d8b28c715f031574c5b252f087d Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:12:03 +0100 Subject: [PATCH 02/12] format and adjust styles --- web/src/pages/SupportPage/SupportPage.tsx | 42 +++++++++++++++++++---- web/src/pages/SupportPage/styles.scss | 13 +++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/web/src/pages/SupportPage/SupportPage.tsx b/web/src/pages/SupportPage/SupportPage.tsx index 40c18492ae..e73d79980b 100644 --- a/web/src/pages/SupportPage/SupportPage.tsx +++ b/web/src/pages/SupportPage/SupportPage.tsx @@ -6,13 +6,13 @@ import { Page } from '../../shared/components/Page/Page'; import { SettingsCard } from '../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../shared/components/SettingsLayout/SettingsLayout'; +import { externalLink } from '../../shared/constants'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { ButtonMenu } from '../../shared/defguard-ui/components/ButtonMenu/MenuButton'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { Icon } from '../../shared/defguard-ui/components/Icon'; import type { IconKindValue } from '../../shared/defguard-ui/components/Icon/icon-types'; import docIllustration from '../../shared/defguard-ui/components/SectionSelect/assets/manual-user.png'; -import { externalLink } from '../../shared/constants'; import { downloadFile, downloadText } from '../../shared/utils/download'; export const SupportPage = () => { @@ -34,7 +34,13 @@ export const SupportPage = () => { variant="primary" text={m.support_page_docs_btn()} iconRight="open-in-new-window" - onClick={() => window.open(externalLink.defguard.docs, '_blank', 'noopener,noreferrer')} + onClick={() => + window.open( + externalLink.defguard.docs, + '_blank', + 'noopener,noreferrer', + ) + } /> @@ -51,7 +57,11 @@ export const SupportPage = () => { text={m.support_page_bug_btn_report()} iconLeft="github" onClick={() => - window.open(externalLink.github.bugReport, '_blank', 'noopener,noreferrer') + window.open( + externalLink.github.bugReport, + '_blank', + 'noopener,noreferrer', + ) } /> { text={m.support_page_feature_btn()} iconLeft="github" onClick={() => - window.open(externalLink.github.featureRequest, '_blank', 'noopener,noreferrer') + window.open( + externalLink.github.featureRequest, + '_blank', + 'noopener,noreferrer', + ) } /> @@ -128,7 +142,11 @@ export const SupportPage = () => { text={m.support_page_assistance_btn_ticket()} iconRight="open-in-new-window" onClick={() => - window.open(externalLink.defguard.support, '_blank', 'noopener,noreferrer') + window.open( + externalLink.defguard.support, + '_blank', + 'noopener,noreferrer', + ) } />