diff --git a/webnext/index.html b/webnext/index.html
index d8483f5..862e658 100644
--- a/webnext/index.html
+++ b/webnext/index.html
@@ -4,7 +4,9 @@
+
Defguard
+
@@ -23,6 +25,22 @@
+
+
+
diff --git a/webnext/messages/en.json b/webnext/messages/en.json
index 407567a..f288fc0 100644
--- a/webnext/messages/en.json
+++ b/webnext/messages/en.json
@@ -2,6 +2,7 @@
"$schema": "https://inlang.com/schema/inlang-message-format",
"misc_or": "or",
"misc_and": "and",
+ "misc_for": "for",
"controls_back": "Back",
"controls_continue": "Continue",
"controls_cancel": "Cancel",
diff --git a/webnext/package.json b/webnext/package.json
index 51e63c9..9946d77 100644
--- a/webnext/package.json
+++ b/webnext/package.json
@@ -24,6 +24,7 @@
"@tanstack/react-router-devtools": "^1.132.2",
"@uidotdev/usehooks": "^2.4.1",
"axios": "^1.12.2",
+ "change-case": "^5.4.4",
"clsx": "^2.1.1",
"lodash-es": "^4.17.21",
"motion": "^12.23.21",
diff --git a/webnext/pnpm-lock.yaml b/webnext/pnpm-lock.yaml
index 8bc9049..1045dfd 100644
--- a/webnext/pnpm-lock.yaml
+++ b/webnext/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
axios:
specifier: ^1.12.2
version: 1.12.2
+ change-case:
+ specifier: ^5.4.4
+ version: 5.4.4
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -1412,6 +1415,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -3792,6 +3798,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ change-case@5.4.4: {}
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
diff --git a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
index 6d3ea0e..6a10d30 100644
--- a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
+++ b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
@@ -1,5 +1,5 @@
import './style.scss';
-import { useNavigate } from '@tanstack/react-router';
+import { useLoaderData, useNavigate } from '@tanstack/react-router';
import { useMemo, useState } from 'react';
import { m } from '../../paraglide/messages';
import { Page } from '../../shared/components/Page/Page';
@@ -25,34 +25,10 @@ import desktopIcon from './assets/pc-tower.png';
// open link in onClick handler
-const linuxMenu: MenuItemsGroup[] = [
- {
- items: [
- {
- text: 'Deb X86',
- onClick: () => openVirtualLink(externalLink.client.desktop.linux.deb.amd),
- },
- {
- text: 'Deb ARM',
- onClick: () => openVirtualLink(externalLink.client.desktop.linux.deb.arm),
- },
- {
- text: 'RPM X86',
- onClick: () => openVirtualLink(externalLink.client.desktop.linux.rpm.amd),
- },
- {
- text: 'RPM ARM',
- onClick: () => openVirtualLink(externalLink.client.desktop.linux.rpm.arm),
- },
- {
- text: 'ArchLinux',
- onClick: () => openVirtualLink(externalLink.client.desktop.linux.arch),
- },
- ],
- },
-];
-
export const ClientDownloadPage = () => {
+ const pageData = useLoaderData({
+ from: '/download',
+ });
const navigate = useNavigate();
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
@@ -70,16 +46,46 @@ export const ClientDownloadPage = () => {
items: [
{
text: 'Intel',
- onClick: () => openVirtualLink(externalLink.client.desktop.macos.intel),
+ onClick: () => openVirtualLink(pageData?.macos_amd64),
},
{
text: 'ARM',
- onClick: () => openVirtualLink(externalLink.client.desktop.macos.arm),
+ onClick: () => openVirtualLink(pageData?.macos_arm64),
+ },
+ ],
+ },
+ ],
+ [pageData],
+ );
+
+ const linuxMenu: MenuItemsGroup[] = useMemo(
+ () => [
+ {
+ items: [
+ {
+ text: 'Deb X86',
+ onClick: () => openVirtualLink(pageData?.deb_amd64),
+ },
+ {
+ text: 'Deb ARM',
+ onClick: () => openVirtualLink(pageData?.deb_arm64),
+ },
+ {
+ text: 'RPM X86',
+ onClick: () => openVirtualLink(pageData?.rpm_amd64),
+ },
+ {
+ text: 'RPM ARM',
+ onClick: () => openVirtualLink(pageData?.rpm_arm64),
+ },
+ {
+ text: 'Arch Linux',
+ onClick: () => openVirtualLink(externalLink.client.desktop.linux.arch),
},
],
},
],
- [],
+ [pageData],
);
return (
@@ -103,7 +109,7 @@ export const ClientDownloadPage = () => {
})}
buttonText={m.client_download_for({ platform: 'Windows' })}
buttonIconKind="windows"
- directLink={externalLink.client.desktop.windows}
+ directLink={pageData?.windows_amd64}
icon={desktopIcon}
/>
{
const pageData = useLoaderData({
from: '/client-setup',
});
+ const clientLinks = pageData.clientDownload;
+
+ const clientDownloadMenu = useMemo(
+ (): MenuItemsGroup[] => [
+ {
+ items: [
+ {
+ text: 'Windows',
+ icon: 'windows',
+ onClick: () => openVirtualLink(clientLinks?.windows_amd64),
+ },
+ ],
+ },
+ {
+ header: {
+ text: m.client_download_apple_help_header(),
+ },
+ items: [
+ {
+ icon: 'apple',
+ text: 'Intel',
+ onClick: () => openVirtualLink(clientLinks?.macos_amd64),
+ },
+ {
+ icon: 'apple',
+ text: 'ARM',
+ onClick: () => openVirtualLink(clientLinks?.macos_arm64),
+ },
+ ],
+ },
+ {
+ header: {
+ text: `${capitalCase(m.misc_for())} Linux`,
+ },
+ items: [
+ {
+ icon: 'linux',
+ text: 'Deb X86',
+ onClick: () => openVirtualLink(clientLinks?.deb_amd64),
+ },
+ {
+ icon: 'linux',
+ text: 'Deb ARM',
+ onClick: () => openVirtualLink(clientLinks?.deb_arm64),
+ },
+ {
+ icon: 'linux',
+ text: 'RPM X86',
+ onClick: () => openVirtualLink(clientLinks?.rpm_amd64),
+ },
+ {
+ icon: 'linux',
+ text: 'RPM ARM',
+ onClick: () => openVirtualLink(clientLinks?.rpm_arm64),
+ },
+ {
+ icon: 'linux',
+ text: 'Arch Linux',
+ onClick: () => openVirtualLink(externalLink.client.desktop.linux.arch),
+ },
+ ],
+ },
+ ],
+ [clientLinks],
+ );
+
const [manualOpen, setManualOpen] = useState(false);
const deepLink = () =>
@@ -60,6 +130,13 @@ export const ConfigureClientPage = () => {
iconRight="open-in-new-window"
/>
+
diff --git a/webnext/src/routes/client-setup.tsx b/webnext/src/routes/client-setup.tsx
index 787878f..186ad7b 100644
--- a/webnext/src/routes/client-setup.tsx
+++ b/webnext/src/routes/client-setup.tsx
@@ -3,6 +3,7 @@ import z from 'zod';
import { ConfigureClientPage } from '../pages/enrollment/ConfigureClient/ConfigureClientPage';
import { api } from '../shared/api/api';
import type { EnrollmentStartResponse } from '../shared/api/types';
+import { updateServiceApi } from '../shared/api/update-service';
import { isPresent } from '../shared/defguard-ui/utils/isPresent';
import { useEnrollmentStore } from '../shared/hooks/useEnrollmentStore';
@@ -38,6 +39,7 @@ export const Route = createFileRoute('/client-setup')({
};
},
loader: async ({ context: { openid } }) => {
+ const clientDownload = await updateServiceApi.getClientArtifacts().catch(() => null);
if (openid) {
try {
const openIdResponse = await api.openId.enrollmentCallback.callbackFn({
@@ -68,6 +70,7 @@ export const Route = createFileRoute('/client-setup')({
}
const state = useEnrollmentStore.getState();
return {
+ clientDownload,
token: state.token as string,
enrollmentData: state.enrollmentData as EnrollmentStartResponse,
};
diff --git a/webnext/src/routes/download.tsx b/webnext/src/routes/download.tsx
index 3969864..2149342 100644
--- a/webnext/src/routes/download.tsx
+++ b/webnext/src/routes/download.tsx
@@ -1,6 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
import { ClientDownloadPage } from '../pages/ClientDownload/ClientDownloadPage';
+import { updateServiceApi } from '../shared/api/update-service';
export const Route = createFileRoute('/download')({
component: ClientDownloadPage,
+ loader: async () => {
+ const clientVersionData = await updateServiceApi
+ .getClientArtifacts()
+ .catch(() => null);
+ return clientVersionData;
+ },
});
diff --git a/webnext/src/shared/api/update-service.ts b/webnext/src/shared/api/update-service.ts
new file mode 100644
index 0000000..6d70a67
--- /dev/null
+++ b/webnext/src/shared/api/update-service.ts
@@ -0,0 +1,39 @@
+import axios from 'axios';
+import qs from 'qs';
+
+const baseUrl = import.meta.env.VITE_UPDATE_BASE_URL as string | undefined;
+
+const client = axios.create({
+ baseURL: baseUrl ?? 'https://update-service-dev.defguard.net/api',
+ headers: { 'Content-Type': 'application/json' },
+ paramsSerializer: {
+ serialize: (params) =>
+ qs.stringify(params, {
+ arrayFormat: 'repeat',
+ }),
+ },
+});
+
+export type ClientVersionCheck = {
+ windows_amd64?: string;
+ deb_amd64?: string;
+ deb_arm64?: string;
+ rpm_amd64?: string;
+ rpm_arm64?: string;
+ macos_amd64?: string;
+ macos_arm64?: string;
+};
+
+const updateServiceApi = {
+ getClientArtifacts: () =>
+ client
+ .get('/update/artifacts', {
+ params: {
+ product: 'defguard-client',
+ source: 'enrollment',
+ },
+ })
+ .then((response) => response.data),
+} as const;
+
+export { updateServiceApi };
diff --git a/webnext/src/shared/defguard-ui/components/ButtonMenu/MenuButton.tsx b/webnext/src/shared/defguard-ui/components/ButtonMenu/MenuButton.tsx
index 54b8a18..769fbc3 100644
--- a/webnext/src/shared/defguard-ui/components/ButtonMenu/MenuButton.tsx
+++ b/webnext/src/shared/defguard-ui/components/ButtonMenu/MenuButton.tsx
@@ -58,6 +58,9 @@ export const ButtonMenu = ({
itemGroups={menuItems}
ref={refs.setFloating}
style={floatingStyles}
+ onClose={() => {
+ setOpen(false);
+ }}
{...getFloatingProps()}
/>
diff --git a/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx b/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx
index e776153..0e0f754 100644
--- a/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx
+++ b/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx
@@ -7,16 +7,18 @@ import { MenuHeader } from './components/MenuHeader';
import { MenuSpacer } from './components/MenuSpacer';
import type { MenuProps } from './types';
-export const Menu = ({ itemGroups, ref, className, ...props }: MenuProps) => {
+export const Menu = ({ itemGroups, ref, className, onClose, ...props }: MenuProps) => {
return (
{itemGroups.map((group, groupIndex) => (
- {isPresent(group.header) && }
+ {isPresent(group.header) && }
{group.items.map((item) => (
-
+
))}
- {groupIndex !== 0 && groupIndex !== itemGroups.length - 1 && }
+ {groupIndex !== itemGroups.length - 1 && itemGroups.length !== 1 && (
+
+ )}
))}
diff --git a/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx b/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx
index e836810..341b11a 100644
--- a/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx
+++ b/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx
@@ -3,7 +3,7 @@ import { isPresent } from '../../../utils/isPresent';
import { InteractionBox } from '../../InteractionBox/InteractionBox';
import type { MenuHeaderProps } from '../types';
-export const MenuHeader = ({ text, onHelp }: MenuHeaderProps) => {
+export const MenuHeader = ({ text, onHelp, onClose }: MenuHeaderProps) => {
return (
{
className="menu-header-help"
icon="help"
iconSize={20}
- onClick={onHelp}
+ onClick={() => {
+ onClose?.();
+ onHelp();
+ }}
/>
)}
diff --git a/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx b/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx
index 51af2e4..fd626e9 100644
--- a/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx
+++ b/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx
@@ -9,6 +9,7 @@ export const MenuItem = ({
icon,
items,
onClick,
+ onClose,
variant,
}: MenuItemProps) => {
const hasItems = isPresent(items) && items.length > 0;
@@ -27,6 +28,9 @@ export const MenuItem = ({
onClick={() => {
if (!disabled) {
onClick?.();
+ if (!hasItems) {
+ onClose?.();
+ }
}
}}
>
diff --git a/webnext/src/shared/defguard-ui/components/Menu/types.ts b/webnext/src/shared/defguard-ui/components/Menu/types.ts
index 81308ac..93c2c12 100644
--- a/webnext/src/shared/defguard-ui/components/Menu/types.ts
+++ b/webnext/src/shared/defguard-ui/components/Menu/types.ts
@@ -3,6 +3,7 @@ import type { IconKindValue } from '../Icon/icon-types';
export interface MenuProps extends HTMLAttributes {
itemGroups: MenuItemsGroup[];
+ onClose?: () => void;
ref?: Ref;
}
@@ -18,10 +19,12 @@ export interface MenuItemProps {
items?: MenuItemProps[];
disabled?: boolean;
onClick?: () => void;
+ onClose?: () => void;
}
export interface MenuHeaderProps {
text: string;
tooltip?: string;
+ onClose?: () => void;
onHelp?: () => void;
}
diff --git a/webnext/src/shared/utils/openVirtualLink.ts b/webnext/src/shared/utils/openVirtualLink.ts
index 10b6d10..c84faf0 100644
--- a/webnext/src/shared/utils/openVirtualLink.ts
+++ b/webnext/src/shared/utils/openVirtualLink.ts
@@ -1,4 +1,6 @@
-export const openVirtualLink = (value: string): void => {
+export const openVirtualLink = (value?: string): void => {
+ if (!value) return;
+
const anchorElement = document.createElement('a');
anchorElement.style.display = 'none';
anchorElement.href = value;