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;