diff --git a/web/src/pages/AddLocationPage/AddLocationPage.tsx b/web/src/pages/AddLocationPage/AddLocationPage.tsx index 42695ccd6d..e7d736302f 100644 --- a/web/src/pages/AddLocationPage/AddLocationPage.tsx +++ b/web/src/pages/AddLocationPage/AddLocationPage.tsx @@ -1,6 +1,10 @@ import { useNavigate } from '@tanstack/react-router'; -import { type ReactNode, useMemo } from 'react'; -import type { WizardPageStep } from '../../shared/components/wizard/types'; +import { type ReactNode, useCallback, useMemo } from 'react'; +import type { + WizardPageStep, + WizardWelcomePageConfig, +} from '../../shared/components/wizard/types'; +import { WizardCoverImage } from '../../shared/components/wizard/WizardCoverImage/WizardCoverImage'; import { WizardPage } from '../../shared/components/wizard/WizardPage/WizardPage'; import { AddLocationAccessStep } from './steps/AddLocationAccessStep'; import { AddLocationFirewallStep } from './steps/AddLocationFirewallStep'; @@ -9,6 +13,7 @@ import { AddLocationMfaStep } from './steps/AddLocationMfaStep'; import { AddLocationNetworkStep } from './steps/AddLocationNetworkStep'; import { AddLocationServiceStep } from './steps/AddLocationServiceStep'; import { AddLocationStartStep } from './steps/AddLocationStartStep'; +import { AddLocationWelcomeStep } from './steps/AddLocationWelcomeStep'; import { AddLocationPageStep, type AddLocationPageStepValue } from './types'; import { useAddLocationStore } from './useAddLocationStore'; @@ -16,6 +21,29 @@ export const AddLocationPage = () => { const navigate = useNavigate(); const activeStep = useAddLocationStore((s) => s.activeStep); const locationType = useAddLocationStore((s) => s.locationType); + const isWelcome = useAddLocationStore((s) => s.isWelcome); + + const onClose = useCallback(() => { + navigate({ + to: '/locations', + }).then(() => { + setTimeout(() => { + useAddLocationStore.getState().reset(); + }, 100); + }); + }, [navigate]); + + const welcomeConfig = useMemo( + (): WizardWelcomePageConfig => ({ + title: 'Add new location', + subtitle: `Welcome! Let's set up a new location to organize users, manage access, and connect gateways for activity tracking and monitoring.`, + content: , + displayDocs: false, + media: , + onClose, + }), + [onClose], + ); const stepsConfig = useMemo( (): Record => ({ @@ -88,20 +116,14 @@ export const AddLocationPage = () => { return ( { - navigate({ - to: '/locations', - }).then(() => { - setTimeout(() => { - useAddLocationStore.getState().reset(); - }, 100); - }); - }} + onClose={onClose} subtitle="Welcome! Let's set up a new location to organize users, manage access, and connect gateways for activity tracking and monitoring." title="Create new location" steps={stepsConfig} id="add-location-wizard" + welcomePageConfig={welcomeConfig} > {stepsComponents[activeStep]} diff --git a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx index 2858866cdd..0f17dfd3d0 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx @@ -83,6 +83,13 @@ export const AddLocationStartStep = () => { {(field) => } + { + useAddLocationStore.setState({ isWelcome: true }); + }} + /> { + return ( + <> + + + { + useAddLocationStore.setState({ + isWelcome: false, + }); + }} + /> + + > + ); +}; diff --git a/web/src/pages/AddLocationPage/useAddLocationStore.tsx b/web/src/pages/AddLocationPage/useAddLocationStore.tsx index e680ad1d22..78c4366358 100644 --- a/web/src/pages/AddLocationPage/useAddLocationStore.tsx +++ b/web/src/pages/AddLocationPage/useAddLocationStore.tsx @@ -9,6 +9,7 @@ import { import { AddLocationPageStep, type AddLocationPageStepValue } from './types'; type StoreValues = { + isWelcome: boolean; activeStep: AddLocationPageStepValue; locationType: 'regular' | 'service'; } & EditNetworkLocation; @@ -19,6 +20,7 @@ type StoreMethods = { }; const defaults: StoreValues = { + isWelcome: true, locationType: 'regular', activeStep: AddLocationPageStep.Start, // form values diff --git a/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx index 3da1998023..f064fd9acd 100644 --- a/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx +++ b/web/src/pages/EdgeSetupPage/EdgeSetupPage.tsx @@ -5,13 +5,13 @@ import { m } from '../../paraglide/messages'; import { ActionCard } from '../../shared/components/ActionCard/ActionCard'; import { Controls } from '../../shared/components/Controls/Controls'; import type { WizardPageStep } from '../../shared/components/wizard/types'; +import { WizardCoverImage } from '../../shared/components/wizard/WizardCoverImage/WizardCoverImage'; import { WizardPage } from '../../shared/components/wizard/WizardPage/WizardPage'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; import deployImage from './assets/deploy.svg'; -import welcomeImage from './assets/welcome_image.svg'; import { SetupConfirmationStep } from './steps/SetupConfirmationStep'; import { SetupEdgeAdoptionStep } from './steps/SetupEdgeAdoptionStep'; import { SetupEdgeComponentStep } from './steps/SetupEdgeComponentStep'; @@ -106,7 +106,7 @@ export const EdgeSetupPage = () => { content: , docsLink: 'https://docs.defguard.net/edge-component/deployment', docsText: m.edge_setup_welcome_docs_text(), - media: , + media: , onClose: () => { navigate({ to: '/vpn-overview', replace: true }).then(() => { setTimeout(() => { diff --git a/web/src/pages/EdgeSetupPage/assets/welcome_image.svg b/web/src/pages/EdgeSetupPage/assets/welcome_image.svg deleted file mode 100644 index c265621aaf..0000000000 --- a/web/src/pages/EdgeSetupPage/assets/welcome_image.svg +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx index 6e2b80b728..a0656d21ba 100644 --- a/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx +++ b/web/src/pages/GatewaySetupPage/GatewaySetupPage.tsx @@ -4,11 +4,12 @@ import { type ReactNode, useCallback, useMemo } from 'react'; import { m } from '../../paraglide/messages'; import { Controls } from '../../shared/components/Controls/Controls'; import type { WizardPageStep } from '../../shared/components/wizard/types'; +import { WizardCover } from '../../shared/components/wizard/WizardCoverImage/types'; +import { WizardCoverImage } from '../../shared/components/wizard/WizardCoverImage/WizardCoverImage'; import { WizardPage } from '../../shared/components/wizard/WizardPage/WizardPage'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; -import welcomeImage from './assets/welcome_image.svg'; import { SetupConfirmationStep } from './steps/SetupConfirmationStep'; import { SetupDeployGatewayStep } from './steps/SetupDeployGatewayStep'; import { SetupGatewayAdoptionStep } from './steps/SetupGatewayAdaptationStep'; @@ -104,7 +105,7 @@ export const GatewaySetupPage = () => { content: , docsLink: 'https://docs.defguard.net/edge-component/deployment', docsText: m.gateway_setup_welcome_docs_text(), - media: , + media: , onClose, }} > diff --git a/web/src/pages/GatewaySetupPage/assets/welcome_image.svg b/web/src/pages/GatewaySetupPage/assets/welcome_image.svg deleted file mode 100644 index c265621aaf..0000000000 --- a/web/src/pages/GatewaySetupPage/assets/welcome_image.svg +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx index 93c00f4556..5c84fd3661 100644 --- a/web/src/pages/PlaygroundPage/PlaygroundPage.tsx +++ b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx @@ -29,7 +29,6 @@ import { BadgeVariant, } from '../../shared/defguard-ui/components/Badge/types'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; -import { ButtonsGroup } from '../../shared/defguard-ui/components/ButtonsGroup/ButtonsGroup'; import { Checkbox } from '../../shared/defguard-ui/components/Checkbox/Checkbox'; import { CheckboxIndicator } from '../../shared/defguard-ui/components/CheckboxIndicator/CheckboxIndicator'; import { Chip } from '../../shared/defguard-ui/components/Chip/Chip'; @@ -279,7 +278,13 @@ const TestPlanUpgrade = () => { {`Licensing modals`} - + { @@ -310,7 +315,15 @@ const TestPlanUpgrade = () => { } }} /> - + { + openModal(ModalName.LicenseLimitConflict, { + conflicts: [], + }); + }} + /> + ); }; diff --git a/web/src/shared/components/modals/LicenseModal/LicenseModal.tsx b/web/src/shared/components/modals/LicenseModal/LicenseModal.tsx index 7664c837f6..cd135d6ed9 100644 --- a/web/src/shared/components/modals/LicenseModal/LicenseModal.tsx +++ b/web/src/shared/components/modals/LicenseModal/LicenseModal.tsx @@ -1,30 +1,34 @@ import './style.scss'; -import type { PropsWithChildren } from 'react'; +import type { PropsWithChildren, ReactNode } from 'react'; import { ModalFoundation } from '../../../defguard-ui/components/ModalFoundation/ModalFoundation'; import type { ModalBase } from '../../../defguard-ui/components/ModalFoundation/types'; +import { isPresent } from '../../../defguard-ui/utils/isPresent'; import gridImage from './grid.png?url'; type Props = Omit & { - image?: string; + image?: ReactNode; + lines?: boolean; } & PropsWithChildren; -export const LicenseModal = ({ image, children, ...foundationProps }: Props) => { +export const LicenseModal = ({ + image, + children, + lines = false, + ...foundationProps +}: Props) => { return ( - - + {lines && ( + + )} + {isPresent(image) && image} {children} diff --git a/web/src/shared/components/modals/LicenseModal/style.scss b/web/src/shared/components/modals/LicenseModal/style.scss index c756bc5796..0fe85c4864 100644 --- a/web/src/shared/components/modals/LicenseModal/style.scss +++ b/web/src/shared/components/modals/LicenseModal/style.scss @@ -20,6 +20,10 @@ position: absolute; inset: 0; } + + & > img { + position: absolute; + } } & > .content-track { diff --git a/web/src/shared/components/modals/license/LicenseExpiredModal/LicenseExpiredModal.tsx b/web/src/shared/components/modals/license/LicenseExpiredModal/LicenseExpiredModal.tsx index 54323dfe41..6d11fa0ea8 100644 --- a/web/src/shared/components/modals/license/LicenseExpiredModal/LicenseExpiredModal.tsx +++ b/web/src/shared/components/modals/license/LicenseExpiredModal/LicenseExpiredModal.tsx @@ -12,6 +12,7 @@ import { import { ModalName } from '../../../../hooks/modalControls/modalTypes'; import { LicenseModal } from '../../LicenseModal/LicenseModal'; import { LicenseModalControls } from '../LicenseModalControls'; +import { LicenseModalSideImage } from '../LicenseModalSideImage/LicenseModalSideImage'; const modalNameKey = ModalName.LicenseExpired; @@ -39,6 +40,8 @@ export const LicenseExpiredModal = () => { afterClose={() => { setTier(null); }} + image={} + lines > {isPresent(tier) && } diff --git a/web/src/shared/components/modals/license/LicenseLimitConflictModal/LicenseLimitConflictModal.tsx b/web/src/shared/components/modals/license/LicenseLimitConflictModal/LicenseLimitConflictModal.tsx index 6240115194..c91ed71d7e 100644 --- a/web/src/shared/components/modals/license/LicenseLimitConflictModal/LicenseLimitConflictModal.tsx +++ b/web/src/shared/components/modals/license/LicenseLimitConflictModal/LicenseLimitConflictModal.tsx @@ -12,6 +12,7 @@ import { ModalName } from '../../../../hooks/modalControls/modalTypes'; import type { OpenLicenseLimitConflictModal } from '../../../../hooks/modalControls/types'; import { LicenseModal } from '../../LicenseModal/LicenseModal'; import { LicenseModalControls } from '../LicenseModalControls'; +import { LicenseModalSideImage } from '../LicenseModalSideImage/LicenseModalSideImage'; const modalNameKey = ModalName.LicenseLimitConflict; @@ -39,6 +40,8 @@ export const LicenseLimitConflictModal = () => { afterClose={() => { setModalData(null); }} + image={} + lines > {isPresent(modalData) && } @@ -61,7 +64,7 @@ const ModalContent = ({ conflicts }: OpenLicenseLimitConflictModal) => { {`The license you’re trying to use allows fewer resources that your current setup is using.`} + >{`The license you're trying to use allows fewer resources that your current setup is using.`} {`To apply this license, first reduce your usage so it fits within the license limits.`} @@ -78,15 +81,17 @@ const ModalContent = ({ conflicts }: OpenLicenseLimitConflictModal) => { {`No changes were made to your current configuration.`} - - {conflicts.map((conflict) => ( - {`${conflict.label}: ${conflict.current} used, ${conflict.limit} allowed`} - ))} - + {conflicts.length > 0 && ( + + {conflicts.map((conflict) => ( + {`${conflict.label}: ${conflict.current} used, ${conflict.limit} allowed`} + ))} + + )} > ); diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/LicenseModalSideImage.tsx b/web/src/shared/components/modals/license/LicenseModalSideImage/LicenseModalSideImage.tsx new file mode 100644 index 0000000000..515364c555 --- /dev/null +++ b/web/src/shared/components/modals/license/LicenseModalSideImage/LicenseModalSideImage.tsx @@ -0,0 +1,66 @@ +import businessImage from './assets/business.png'; +import enterpriseImage from './assets/enterprise.png'; +import expiredImage from './assets/expired.png'; +import limitImage from './assets/limit.png'; +import type { LicenseModalSideImageVariantValue } from './types'; + +export const LicenseModalSideImage = ({ + variant, +}: { + variant: LicenseModalSideImageVariantValue; +}) => { + switch (variant) { + case 'limit': + return ( + + ); + case 'business': + return ( + + ); + case 'enterprise': + return ( + + ); + case 'expired': + return ( + + ); + } +}; diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/assets/business.png b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/business.png new file mode 100644 index 0000000000..fc74e80e4b Binary files /dev/null and b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/business.png differ diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/assets/enterprise.png b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/enterprise.png new file mode 100644 index 0000000000..7bd6a318a7 Binary files /dev/null and b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/enterprise.png differ diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/assets/expired.png b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/expired.png new file mode 100644 index 0000000000..7aec25c51f Binary files /dev/null and b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/expired.png differ diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/assets/limit.png b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/limit.png new file mode 100644 index 0000000000..d8579d25b9 Binary files /dev/null and b/web/src/shared/components/modals/license/LicenseModalSideImage/assets/limit.png differ diff --git a/web/src/shared/components/modals/license/LicenseModalSideImage/types.ts b/web/src/shared/components/modals/license/LicenseModalSideImage/types.ts new file mode 100644 index 0000000000..f0116b7e63 --- /dev/null +++ b/web/src/shared/components/modals/license/LicenseModalSideImage/types.ts @@ -0,0 +1,9 @@ +export const LicenseModalSideImageVariant = { + Expired: 'expired', + Limit: 'limit', + Business: 'business', + Enterprise: 'enterprise', +} as const; + +export type LicenseModalSideImageVariantValue = + (typeof LicenseModalSideImageVariant)[keyof typeof LicenseModalSideImageVariant]; diff --git a/web/src/shared/components/modals/license/LimitReachedModal/LimitReachedModal.tsx b/web/src/shared/components/modals/license/LimitReachedModal/LimitReachedModal.tsx index 3f94d14742..9192f40bf2 100644 --- a/web/src/shared/components/modals/license/LimitReachedModal/LimitReachedModal.tsx +++ b/web/src/shared/components/modals/license/LimitReachedModal/LimitReachedModal.tsx @@ -10,6 +10,7 @@ import { import { ModalName } from '../../../../hooks/modalControls/modalTypes'; import { LicenseModal } from '../../LicenseModal/LicenseModal'; import { LicenseModalControls } from '../LicenseModalControls'; +import { LicenseModalSideImage } from '../LicenseModalSideImage/LicenseModalSideImage'; const modalNameKey = ModalName.LimitReached; @@ -33,6 +34,7 @@ export const LimitReachedModal = () => { isOpen={isOpen} onClose={() => setOpen(false)} afterClose={() => {}} + image={} > diff --git a/web/src/shared/components/modals/license/UpgradeBusinessModal/UpgradeBusinessModal.tsx b/web/src/shared/components/modals/license/UpgradeBusinessModal/UpgradeBusinessModal.tsx index e9445444e0..ab24656ee3 100644 --- a/web/src/shared/components/modals/license/UpgradeBusinessModal/UpgradeBusinessModal.tsx +++ b/web/src/shared/components/modals/license/UpgradeBusinessModal/UpgradeBusinessModal.tsx @@ -10,6 +10,7 @@ import { import { ModalName } from '../../../../hooks/modalControls/modalTypes'; import { LicenseModal } from '../../LicenseModal/LicenseModal'; import { LicenseModalControls } from '../LicenseModalControls'; +import { LicenseModalSideImage } from '../LicenseModalSideImage/LicenseModalSideImage'; const modalNameKey = ModalName.UpgradeBusiness; @@ -33,6 +34,8 @@ export const UpgradeBusinessModal = () => { isOpen={isOpen} onClose={() => setOpen(false)} afterClose={() => {}} + image={} + lines > diff --git a/web/src/shared/components/modals/license/UpgradeEnterpriseModal/UpgradeEnterpriseModal.tsx b/web/src/shared/components/modals/license/UpgradeEnterpriseModal/UpgradeEnterpriseModal.tsx index 5cbc7d55db..240ed6e202 100644 --- a/web/src/shared/components/modals/license/UpgradeEnterpriseModal/UpgradeEnterpriseModal.tsx +++ b/web/src/shared/components/modals/license/UpgradeEnterpriseModal/UpgradeEnterpriseModal.tsx @@ -10,6 +10,7 @@ import { import { ModalName } from '../../../../hooks/modalControls/modalTypes'; import { LicenseModal } from '../../LicenseModal/LicenseModal'; import { LicenseModalControls } from '../LicenseModalControls'; +import { LicenseModalSideImage } from '../LicenseModalSideImage/LicenseModalSideImage'; const modalNameKey = ModalName.UpgradeEnterprise; @@ -33,6 +34,8 @@ export const UpgradeEnterpriseModal = () => { isOpen={isOpen} onClose={() => setOpen(false)} afterClose={() => {}} + image={} + lines > diff --git a/web/src/shared/components/wizard/WizardCoverImage/WizardCoverImage.tsx b/web/src/shared/components/wizard/WizardCoverImage/WizardCoverImage.tsx new file mode 100644 index 0000000000..70fe2c6376 --- /dev/null +++ b/web/src/shared/components/wizard/WizardCoverImage/WizardCoverImage.tsx @@ -0,0 +1,65 @@ +import edgeImage from './assets/edge_wizard_cover.png'; +import gatewayImage from './assets/gw_wizard_cover.png'; +import locationImage from './assets/location_wizard_cover.png'; +import migrationImage from './assets/migration_wizard_cover.png'; +import type { WizardCoverValue } from './types'; + +type Props = { + variant: WizardCoverValue; +}; + +export const WizardCoverImage = ({ variant }: Props) => { + switch (variant) { + case 'edge': + return ( + + ); + case 'gateway': + return ( + + ); + case 'location': + return ( + + ); + case 'migration': + return ( + + ); + } +}; diff --git a/web/src/shared/components/wizard/WizardCoverImage/assets/edge_wizard_cover.png b/web/src/shared/components/wizard/WizardCoverImage/assets/edge_wizard_cover.png new file mode 100644 index 0000000000..1208a330d3 Binary files /dev/null and b/web/src/shared/components/wizard/WizardCoverImage/assets/edge_wizard_cover.png differ diff --git a/web/src/shared/components/wizard/WizardCoverImage/assets/gw_wizard_cover.png b/web/src/shared/components/wizard/WizardCoverImage/assets/gw_wizard_cover.png new file mode 100644 index 0000000000..fab3f16159 Binary files /dev/null and b/web/src/shared/components/wizard/WizardCoverImage/assets/gw_wizard_cover.png differ diff --git a/web/src/shared/components/wizard/WizardCoverImage/assets/location_wizard_cover.png b/web/src/shared/components/wizard/WizardCoverImage/assets/location_wizard_cover.png new file mode 100644 index 0000000000..8c49bff4ae Binary files /dev/null and b/web/src/shared/components/wizard/WizardCoverImage/assets/location_wizard_cover.png differ diff --git a/web/src/shared/components/wizard/WizardCoverImage/assets/migration_wizard_cover.png b/web/src/shared/components/wizard/WizardCoverImage/assets/migration_wizard_cover.png new file mode 100644 index 0000000000..5036a8adaf Binary files /dev/null and b/web/src/shared/components/wizard/WizardCoverImage/assets/migration_wizard_cover.png differ diff --git a/web/src/shared/components/wizard/WizardCoverImage/types.ts b/web/src/shared/components/wizard/WizardCoverImage/types.ts new file mode 100644 index 0000000000..bec987147a --- /dev/null +++ b/web/src/shared/components/wizard/WizardCoverImage/types.ts @@ -0,0 +1,8 @@ +export const WizardCover = { + Migration: 'migration', + Location: 'location', + Edge: 'edge', + Gateway: 'gateway', +} as const; + +export type WizardCoverValue = (typeof WizardCover)[keyof typeof WizardCover]; diff --git a/web/src/shared/components/wizard/WizardWelcomePage/style.scss b/web/src/shared/components/wizard/WizardWelcomePage/style.scss index d9c70adeba..f7117dba43 100644 --- a/web/src/shared/components/wizard/WizardWelcomePage/style.scss +++ b/web/src/shared/components/wizard/WizardWelcomePage/style.scss @@ -54,12 +54,14 @@ background-size: cover; img { - width: 100%; - height: 100%; - object-fit: cover; + position: absolute; } #default-globe-media-image { + position: relative; + width: 100%; + height: 100%; + object-fit: cover; object-position: 80px -80px; }