diff --git a/index.html b/index.html index 8b0b56f7..24f30a87 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,8 @@
+
+
diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index fd7b4514..15cee28d 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -35,6 +35,9 @@ const en = { back: 'Back', next: 'Next', submit: 'Submit', + cancel: 'Cancel', + close: 'Close', + reset: 'Reset', }, }, components: { @@ -44,9 +47,68 @@ const en = { }, pages: { client: { - title: 'Device Overview', - locationsList: { - title: 'Available Locations', + title: 'Locations', + sideBar: { + instances: 'Instances', + addInstance: 'Add Instance', + copyright: { + copyright: `Copyright © 2023`, + appVersion: 'Application version: {version:string}', + }, + }, + controls: { + connect: 'Connect', + disconnect: 'Disconnect', + }, + header: { + title: 'Locations', + filters: { + views: { + grid: 'Grid View', + detail: 'Detail View', + }, + }, + }, + connectionLabels: { + lastConnectedFrom: 'Last connected from', + lastConnected: 'Last connected', + assignedIp: 'Assigned IP', + }, + locationNoData: + 'This device was never connected to this location, connect to view statistics and information about connection', + detailView: { + history: { + title: 'Connection history', + headers: { + date: 'Date', + duration: 'Duration', + connectedFrom: 'Connected from', + upload: 'Upload', + download: 'Download', + }, + }, + }, + modals: { + addInstanceModal: { + title: 'Add instance', + messages: { + success: { + add: 'Instance added', + update: 'Instance information updated', + }, + error: 'Fetching information failed.', + }, + form: { + fields: { + token: { + label: 'Token', + }, + url: { + label: 'Proxy URL:', + }, + }, + }, + }, }, }, enrollment: { diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index be2afd61..5e51e645 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -101,6 +101,18 @@ type RootTranslation = { * S​u​b​m​i​t */ submit: string + /** + * C​a​n​c​e​l + */ + cancel: string + /** + * C​l​o​s​e + */ + close: string + /** + * R​e​s​e​t + */ + reset: string } } components: { @@ -114,14 +126,145 @@ type RootTranslation = { pages: { client: { /** - * D​e​v​i​c​e​ ​O​v​e​r​v​i​e​w + * L​o​c​a​t​i​o​n​s */ title: string - locationsList: { + sideBar: { + /** + * I​n​s​t​a​n​c​e​s + */ + instances: string + /** + * A​d​d​ ​I​n​s​t​a​n​c​e + */ + addInstance: string + copyright: { + /** + * C​o​p​y​r​i​g​h​t​ ​©​ ​2​0​2​3 + */ + copyright: string + /** + * A​p​p​l​i​c​a​t​i​o​n​ ​v​e​r​s​i​o​n​:​ ​{​v​e​r​s​i​o​n​} + * @param {string} version + */ + appVersion: RequiredParams<'version'> + } + } + controls: { + /** + * C​o​n​n​e​c​t + */ + connect: string /** - * A​v​a​i​l​a​b​l​e​ ​L​o​c​a​t​i​o​n​s + * D​i​s​c​o​n​n​e​c​t + */ + disconnect: string + } + header: { + /** + * L​o​c​a​t​i​o​n​s */ title: string + filters: { + views: { + /** + * G​r​i​d​ ​V​i​e​w + */ + grid: string + /** + * D​e​t​a​i​l​ ​V​i​e​w + */ + detail: string + } + } + } + connectionLabels: { + /** + * L​a​s​t​ ​c​o​n​n​e​c​t​e​d​ ​f​r​o​m + */ + lastConnectedFrom: string + /** + * L​a​s​t​ ​c​o​n​n​e​c​t​e​d + */ + lastConnected: string + /** + * A​s​s​i​g​n​e​d​ ​I​P + */ + assignedIp: string + } + /** + * T​h​i​s​ ​d​e​v​i​c​e​ ​w​a​s​ ​n​e​v​e​r​ ​c​o​n​n​e​c​t​e​d​ ​t​o​ ​t​h​i​s​ ​l​o​c​a​t​i​o​n​,​ ​c​o​n​n​e​c​t​ ​t​o​ ​v​i​e​w​ ​s​t​a​t​i​s​t​i​c​s​ ​a​n​d​ ​i​n​f​o​r​m​a​t​i​o​n​ ​a​b​o​u​t​ ​c​o​n​n​e​c​t​i​o​n + */ + locationNoData: string + detailView: { + history: { + /** + * C​o​n​n​e​c​t​i​o​n​ ​h​i​s​t​o​r​y + */ + title: string + headers: { + /** + * D​a​t​e + */ + date: string + /** + * D​u​r​a​t​i​o​n + */ + duration: string + /** + * C​o​n​n​e​c​t​e​d​ ​f​r​o​m + */ + connectedFrom: string + /** + * U​p​l​o​a​d + */ + upload: string + /** + * D​o​w​n​l​o​a​d + */ + download: string + } + } + } + modals: { + addInstanceModal: { + /** + * A​d​d​ ​i​n​s​t​a​n​c​e + */ + title: string + messages: { + success: { + /** + * I​n​s​t​a​n​c​e​ ​a​d​d​e​d + */ + add: string + /** + * I​n​s​t​a​n​c​e​ ​i​n​f​o​r​m​a​t​i​o​n​ ​u​p​d​a​t​e​d + */ + update: string + } + /** + * F​e​t​c​h​i​n​g​ ​i​n​f​o​r​m​a​t​i​o​n​ ​f​a​i​l​e​d​. + */ + error: string + } + form: { + fields: { + token: { + /** + * T​o​k​e​n + */ + label: string + } + url: { + /** + * P​r​o​x​y​ ​U​R​L​: + */ + label: string + } + } + } + } } } enrollment: { @@ -551,6 +694,18 @@ export type TranslationFunctions = { * Submit */ submit: () => LocalizedString + /** + * Cancel + */ + cancel: () => LocalizedString + /** + * Close + */ + close: () => LocalizedString + /** + * Reset + */ + reset: () => LocalizedString } } components: { @@ -564,14 +719,144 @@ export type TranslationFunctions = { pages: { client: { /** - * Device Overview + * Locations */ title: () => LocalizedString - locationsList: { + sideBar: { + /** + * Instances + */ + instances: () => LocalizedString + /** + * Add Instance + */ + addInstance: () => LocalizedString + copyright: { + /** + * Copyright © 2023 + */ + copyright: () => LocalizedString + /** + * Application version: {version} + */ + appVersion: (arg: { version: string }) => LocalizedString + } + } + controls: { + /** + * Connect + */ + connect: () => LocalizedString /** - * Available Locations + * Disconnect + */ + disconnect: () => LocalizedString + } + header: { + /** + * Locations */ title: () => LocalizedString + filters: { + views: { + /** + * Grid View + */ + grid: () => LocalizedString + /** + * Detail View + */ + detail: () => LocalizedString + } + } + } + connectionLabels: { + /** + * Last connected from + */ + lastConnectedFrom: () => LocalizedString + /** + * Last connected + */ + lastConnected: () => LocalizedString + /** + * Assigned IP + */ + assignedIp: () => LocalizedString + } + /** + * This device was never connected to this location, connect to view statistics and information about connection + */ + locationNoData: () => LocalizedString + detailView: { + history: { + /** + * Connection history + */ + title: () => LocalizedString + headers: { + /** + * Date + */ + date: () => LocalizedString + /** + * Duration + */ + duration: () => LocalizedString + /** + * Connected from + */ + connectedFrom: () => LocalizedString + /** + * Upload + */ + upload: () => LocalizedString + /** + * Download + */ + download: () => LocalizedString + } + } + } + modals: { + addInstanceModal: { + /** + * Add instance + */ + title: () => LocalizedString + messages: { + success: { + /** + * Instance added + */ + add: () => LocalizedString + /** + * Instance information updated + */ + update: () => LocalizedString + } + /** + * Fetching information failed. + */ + error: () => LocalizedString + } + form: { + fields: { + token: { + /** + * Token + */ + label: () => LocalizedString + } + url: { + /** + * Proxy URL: + */ + label: () => LocalizedString + } + } + } + } } } enrollment: { diff --git a/src/pages/client/ClientPage.tsx b/src/pages/client/ClientPage.tsx index e5932a20..e4af894a 100644 --- a/src/pages/client/ClientPage.tsx +++ b/src/pages/client/ClientPage.tsx @@ -3,6 +3,7 @@ import './style.scss'; import { useI18nContext } from '../../i18n/i18n-react'; import { ClientSideBar } from './components/ClientSideBar/ClientSideBar'; import { LocationsList } from './components/LocationsList/LocationsList'; +import { AddInstanceModal } from './components/modals/AddInstanceModal/AddInstanceModal'; export const ClientPage = () => { const { LL } = useI18nContext(); @@ -16,6 +17,7 @@ export const ClientPage = () => { + ); }; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index 960b6a05..055cd9c9 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -1,16 +1,48 @@ import './style.scss'; +import { useI18nContext } from '../../../../i18n/i18n-react'; import SvgDefguardLogoIcon from '../../../../shared/components/svg/DefguardLogoIcon'; import SvgDefguardLogoText from '../../../../shared/components/svg/DefguardLogoText'; +import SvgIconNavConnections from '../../../../shared/components/svg/IconNavConnections'; +import { IconContainer } from '../../../../shared/defguard-ui/components/Layout/IconContainer/IconContainer'; +import SvgIconPlus from '../../../../shared/defguard-ui/components/svg/IconPlus'; +import { useClientStore } from '../../hooks/useClientStore'; +import { useAddInstanceModal } from '../modals/AddInstanceModal/hooks/useAddInstanceModal'; +import { ClientBarItem } from './components/ClientBarItem/ClientBarItem'; export const ClientSideBar = () => { + const { LL } = useI18nContext(); + const instances = useClientStore((state) => state.instances); + return (
-
+
+
+ +

{LL.pages.client.sideBar.instances()}

+
+ {instances.map((instance) => ( + + ))} + +
+
+ ); +}; + +const AddInstance = () => { + const { LL } = useI18nContext(); + const openAddInstanceModal = useAddInstanceModal((state) => state.open); + return ( +
openAddInstanceModal()}> + + + +

{LL.pages.client.sideBar.addInstance()}

); }; diff --git a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx index 8d639e1d..12124142 100644 --- a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx +++ b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx @@ -1,13 +1,24 @@ import { autoUpdate, useFloating } from '@floating-ui/react'; import classNames from 'classnames'; +import { isUndefined } from 'lodash-es'; +import { useMemo } from 'react'; -import { ActivityIcon } from '../../../../../../shared/defguard-ui/components/icons/ActivityIcon/ActivityIcon'; -import { ActivityIconVariant } from '../../../../../../shared/defguard-ui/components/icons/ActivityIcon/types'; +import SvgIconConnection from '../../../../../../shared/defguard-ui/components/svg/IconConnection'; +import { DefguardInstance } from '../../../../types'; -export const ClientBarItem = () => { - const cn = classNames('client-bar-item'); +type Props = { + instance: DefguardInstance; +}; + +export const ClientBarItem = ({ instance }: Props) => { + const active = useMemo(() => { + if (instance.locations.length === 0) return false; + return !isUndefined(instance.locations.find((l) => l.connected)); + }, [instance.locations]); - const active = true; + const cn = classNames('client-bar-item', 'clickable', { + active, + }); const { refs, floatingStyles } = useFloating({ placement: 'right', @@ -18,10 +29,8 @@ export const ClientBarItem = () => { return ( <>
- -

Placeholder instance name

+ +

{instance.name}

{active && (
svg { + & > svg, + & > .icon-wrapper { grid-column: 1; grid-row: 1; width: 24px; @@ -59,6 +63,7 @@ max-width: 100%; display: block; text-align: left; + user-select: none; @include text-overflow-dots; @include typography(app-side-bar); @@ -66,10 +71,33 @@ color: var(--text-body-tertiary); } + & > .connection-icon { + path { + stroke: var(--surface-important); + } + } + &.active { & > p { color: var(--text-body-primary); } + & > .connection-icon { + path { + stroke: var(--surface-positive-primary); + } + } + } + + &.clickable { + cursor: pointer; + } + + &:not(.active) { + &:hover { + & > p { + color: var(--text-body-primary); + } + } } } } diff --git a/src/pages/client/components/LocationsList/LocationsList.tsx b/src/pages/client/components/LocationsList/LocationsList.tsx index b4c68c8c..3ced48e8 100644 --- a/src/pages/client/components/LocationsList/LocationsList.tsx +++ b/src/pages/client/components/LocationsList/LocationsList.tsx @@ -1,15 +1,24 @@ -import './style.scss'; +import { useMemo } from 'react'; -import { useI18nContext } from '../../../../i18n/i18n-react'; +import { useClientStore } from '../../hooks/useClientStore'; +import { LocationsGridView } from './components/LocationsGridView/LocationsGridView'; export const LocationsList = () => { - const { LL } = useI18nContext(); - const componentLL = LL.pages.client.locationsList; + const instances = useClientStore((state) => state.instances); + const selectedInstance = useClientStore((state) => state.selectedInstance); + const locations = useMemo(() => { + const selected = instances.find((i) => i.id === selectedInstance); + if (selected) { + return selected.locations; + } + return []; + }, [selectedInstance, instances]); + + if (!selectedInstance) return null; + return ( -
-
-

{componentLL.title()}

-
-
+ <> + + ); }; diff --git a/src/pages/client/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx new file mode 100644 index 00000000..c141d286 --- /dev/null +++ b/src/pages/client/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -0,0 +1,39 @@ +import './style.scss'; + +import classNames from 'classnames'; + +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import SvgIconCheckmarkSmall from '../../../../../../shared/components/svg/IconCheckmarkSmall'; +import { Button } from '../../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../../../shared/defguard-ui/components/Layout/Button/types'; +import SvgIconX from '../../../../../../shared/defguard-ui/components/svg/IconX'; +import { DefguardLocation } from '../../../../types'; + +type Props = { + location: DefguardLocation; +}; + +export const LocationCardConnectButton = ({ location }: Props) => { + const { LL } = useI18nContext(); + + const cn = classNames('location-card-connect-button', { + connected: location.connected, + }); + + return ( +
-

- This device was never connected to this location, connect to view statistics and - information about connection -

- - ); -}; diff --git a/src/pages/client/components/LocationsList/components/LocationItem/style.scss b/src/pages/client/components/LocationsList/components/LocationItem/style.scss deleted file mode 100644 index 02506b75..00000000 --- a/src/pages/client/components/LocationsList/components/LocationItem/style.scss +++ /dev/null @@ -1,44 +0,0 @@ -@use '@scssutils' as *; - -.location-item { - width: 445px; - display: flex; - flex-flow: column; - box-sizing: border-box; - padding: 20px 25px; - border-radius: 15px; - background-color: var(--surface-default-modal); - row-gap: 18px; - - &.no-data { - row-gap: 48px; - } - - & > .top { - height: 40px; - width: 100%; - display: flex; - flex-flow: row nowrap; - align-items: flex-start; - justify-content: flex-start; - - & > .btn { - margin-left: auto; - min-width: 110px; - height: 40px; - - svg { - path { - fill: var(--text-button-primary); - } - } - } - } - - & > .no-data { - @include typography(app-button-xl); - color: var(--text-body-primary); - text-align: center; - width: 100%; - } -} diff --git a/src/pages/client/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx b/src/pages/client/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx new file mode 100644 index 00000000..0ef6fa76 --- /dev/null +++ b/src/pages/client/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx @@ -0,0 +1,55 @@ +import './style.scss'; + +import classNames from 'classnames'; + +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { DefguardInstance, DefguardLocation } from '../../../../types'; +import { LocationCardConnectButton } from '../LocationCardConnectButton/LocationCardConnectButton'; +import { LocationCardInfo } from '../LocationCardInfo/LocationCardInfo'; +import { LocationCardTitle } from '../LocationCardTitle/LocationCardTitle'; + +type Props = { + instanceId: DefguardInstance['id']; + locations: DefguardLocation[]; +}; + +export const LocationsGridView = ({ instanceId, locations }: Props) => { + return ( +
+ {locations.map((l) => ( + + ))} +
+ ); +}; + +type GridItemProps = { + location: DefguardLocation; +}; + +const GridItem = ({ location }: GridItemProps) => { + const { LL } = useI18nContext(); + const cn = classNames( + 'grid-item', + { + active: location.connected, + }, + 'no-info', + ); + return ( + +
+ + +
+ {/* +
+ +
+
+ */} +

{LL.pages.client.locationNoData()}

+
+ ); +}; diff --git a/src/pages/client/components/LocationsList/components/LocationsGridView/style.scss b/src/pages/client/components/LocationsList/components/LocationsGridView/style.scss new file mode 100644 index 00000000..8c824492 --- /dev/null +++ b/src/pages/client/components/LocationsList/components/LocationsGridView/style.scss @@ -0,0 +1,47 @@ +@use '@scssutils' as *; + +#locations-grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(445px, 1fr)); + grid-auto-rows: auto; + gap: 50px; + justify-content: start; + align-content: start; + + & > .grid-item { + box-sizing: border-box; + padding: 20px 25px; + min-height: 245px; + + & > .top { + width: 100%; + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + + & > .btn { + margin-left: auto; + height: 40px; + } + } + + & > .info { + margin: 32px 0; + display: flex; + flex-flow: row; + align-items: flex-start; + justify-content: space-between; + } + + & > .no-data { + width: 100%; + display: block; + text-align: center; + margin-top: 42px; + + @include typography(app-button-xl); + + color: var(--text-body-primary); + } + } +} diff --git a/src/pages/client/components/LocationsList/style.scss b/src/pages/client/components/LocationsList/style.scss deleted file mode 100644 index c28fc2e1..00000000 --- a/src/pages/client/components/LocationsList/style.scss +++ /dev/null @@ -1,18 +0,0 @@ -@use '@scssutils' as *; - -#client-locations-list { - width: 100%; - & > header { - width: 100%; - margin-bottom: 36px; - display: flex; - flex-flow: row; - - h3 { - @include typography(app-body-1); - - color: var(--text-body-primary); - user-select: none; - } - } -} diff --git a/src/pages/client/components/modals/AddInstanceModal/AddInstanceModal.tsx b/src/pages/client/components/modals/AddInstanceModal/AddInstanceModal.tsx new file mode 100644 index 00000000..1fe98f81 --- /dev/null +++ b/src/pages/client/components/modals/AddInstanceModal/AddInstanceModal.tsx @@ -0,0 +1,101 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; +import { Button } from '../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../../shared/defguard-ui/components/Layout/Button/types'; +import { ModalWithTitle } from '../../../../../shared/defguard-ui/components/Layout/modals/ModalWithTitle/ModalWithTitle'; +import { useAddInstanceModal } from './hooks/useAddInstanceModal'; + +export const AddInstanceModal = () => { + const { LL } = useI18nContext(); + const [isOpen, reset, close] = useAddInstanceModal((state) => [ + state.isOpen, + state.reset, + state.close, + ]); + + return ( + + + + ); +}; + +type FormFields = { + url: string; + token: string; +}; + +const defaultValues: FormFields = { + url: '', + token: '', +}; + +const ModalContent = () => { + const { LL } = useI18nContext(); + const closeModal = useAddInstanceModal((state) => state.close); + const schema = useMemo( + () => + z.object({ + url: z + .string() + .trim() + .nonempty(LL.form.errors.required()) + .url(LL.form.errors.invalid()), + token: z.string().trim().nonempty(LL.form.errors.required()), + }), + [LL.form.errors], + ); + const { handleSubmit, control } = useForm({ + resolver: zodResolver(schema), + defaultValues, + mode: 'all', + }); + + const handleValidSubmit: SubmitHandler = (values) => { + console.table(values); + }; + + return ( +
+ + +
+
+ + ); +}; diff --git a/src/pages/client/components/modals/AddInstanceModal/hooks/useAddInstanceModal.tsx b/src/pages/client/components/modals/AddInstanceModal/hooks/useAddInstanceModal.tsx new file mode 100644 index 00000000..005672b3 --- /dev/null +++ b/src/pages/client/components/modals/AddInstanceModal/hooks/useAddInstanceModal.tsx @@ -0,0 +1,27 @@ +import { createWithEqualityFn } from 'zustand/traditional'; + +const defaultValues: StoreValues = { + isOpen: false, +}; + +export const useAddInstanceModal = createWithEqualityFn( + (set) => ({ + ...defaultValues, + open: (initial) => set({ ...defaultValues, ...initial, isOpen: true }), + close: () => set({ isOpen: false }), + reset: () => set(defaultValues), + }), + Object.is, +); + +type Store = StoreValues & StoreMethods; + +type StoreValues = { + isOpen: boolean; +}; + +type StoreMethods = { + open: (init?: Partial) => void; + close: () => void; + reset: () => void; +}; diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx new file mode 100644 index 00000000..200241f4 --- /dev/null +++ b/src/pages/client/hooks/useClientStore.tsx @@ -0,0 +1,115 @@ +import { createWithEqualityFn } from 'zustand/traditional'; + +import { DefguardInstance } from '../types'; + +const mockValues: StoreValues = { + instances: [ + { + id: 'instance-1', + name: 'Teonite', + locations: [ + { + id: 'location-teonite-1', + ip: '169.254.0.0', + name: 'Szczecin', + connected: false, + }, + { + id: 'location-teonite-2', + ip: '169.253.0.0', + name: 'Zurich', + connected: false, + }, + { + id: 'location-teonite-3', + ip: '169.252.0.0', + name: 'Monaco', + connected: true, + }, + { + id: 'location-teonite-4', + ip: '169.251.0.0', + name: 'Berlin', + connected: false, + }, + { + id: 'location-teonite-5', + ip: '169.250.0.0', + name: 'Paris', + connected: false, + }, + { + id: 'location-teonite-6', + ip: '169.249.0.0', + name: 'US East', + connected: true, + }, + ], + }, + { + id: 'instance-2', + name: 'WideStreet', + locations: [ + { + id: 'location-widestreet-1', + ip: '169.254.0.0', + name: 'Szczecin', + connected: false, + }, + { + id: 'location-widestreet-2', + ip: '169.253.0.0', + name: 'Zurich', + connected: false, + }, + { + id: 'location-widestreet-3', + ip: '169.252.0.0', + name: 'Monaco', + connected: false, + }, + { + id: 'location-widestreet-4', + ip: '169.251.0.0', + name: 'Berlin', + connected: false, + }, + { + id: 'location-widestreet-5', + ip: '169.250.0.0', + name: 'Paris', + connected: false, + }, + { + id: 'location-widestreet-6', + ip: '169.249.0.0', + name: 'US East', + connected: false, + }, + ], + }, + ], + selectedInstance: 'instance-1', +}; + +// eslint-disable-next-line +const defaultValues: StoreValues = { + instances: [], + selectedInstance: undefined, +}; + +export const useClientStore = createWithEqualityFn( + (set) => ({ ...mockValues, setState: (values) => set({ ...values }) }), + Object.is, +); + +type Store = StoreValues & StoreMethods; + +type StoreValues = { + instances: DefguardInstance[]; + selectedInstance?: DefguardInstance['id']; +}; + +type StoreMethods = { + setState: (values: Partial) => void; +}; diff --git a/src/pages/client/style.scss b/src/pages/client/style.scss index 1456dc75..9a1cf438 100644 --- a/src/pages/client/style.scss +++ b/src/pages/client/style.scss @@ -5,12 +5,13 @@ box-sizing: border-box; padding: 64px 75px 64px calc(75px + 270px); min-height: 100vh; + background-color: var(--surface-frame-bg); & > header { width: 100%; display: flex; flex-flow: row; - margin-bottom: 200px; + margin-bottom: 60px; & > h1 { @include typography(app-title); diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts new file mode 100644 index 00000000..b22a5f71 --- /dev/null +++ b/src/pages/client/types.ts @@ -0,0 +1,12 @@ +export type DefguardInstance = { + id: string; + name: string; + locations: DefguardLocation[]; +}; + +export type DefguardLocation = { + id: string; + ip: string; + name: string; + connected: boolean; +}; diff --git a/src/shared/components/svg/IconNavConnections.tsx b/src/shared/components/svg/IconNavConnections.tsx new file mode 100644 index 00000000..d4023226 --- /dev/null +++ b/src/shared/components/svg/IconNavConnections.tsx @@ -0,0 +1,17 @@ +import type { SVGProps } from 'react'; +const SvgIconNavConnections = (props: SVGProps) => ( + + + +); +export default SvgIconNavConnections; diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index bc43c37b..1b7b6a16 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit bc43c37bc0ee9e77406b82e8f54b6cf0d111578c +Subproject commit 1b7b6a163252038e787b6e8d0e9e72fd980e708a diff --git a/src/shared/images/svg/icon-nav-connections.svg b/src/shared/images/svg/icon-nav-connections.svg new file mode 100644 index 00000000..c09dacff --- /dev/null +++ b/src/shared/images/svg/icon-nav-connections.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/scss/_base.scss b/src/shared/scss/_base.scss index 253c9196..1113349e 100644 --- a/src/shared/scss/_base.scss +++ b/src/shared/scss/_base.scss @@ -42,6 +42,16 @@ h6 { margin: 0; } +h1, +h2, +h3, +h4, +h5, +h6, +label { + user-select: none; +} + // SCROLL ::-webkit-scrollbar {