diff --git a/webnext/src/pages/PasswordForm/PasswordFormPage.tsx b/webnext/src/pages/PasswordForm/PasswordFormPage.tsx
index 44c9b884..b9be1e44 100644
--- a/webnext/src/pages/PasswordForm/PasswordFormPage.tsx
+++ b/webnext/src/pages/PasswordForm/PasswordFormPage.tsx
@@ -167,6 +167,7 @@ export const PasswordFormPage = () => {
)}
@@ -178,7 +179,11 @@ export const PasswordFormPage = () => {
}}
>
{(field) => (
-
+
)}
diff --git a/webnext/src/routeTree.gen.ts b/webnext/src/routeTree.gen.ts
index d5a618ff..2c91b64d 100644
--- a/webnext/src/routeTree.gen.ts
+++ b/webnext/src/routeTree.gen.ts
@@ -18,6 +18,7 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as PasswordIndexRouteImport } from './routes/password/index'
import { Route as PasswordSentRouteImport } from './routes/password/sent'
import { Route as PasswordFinishRouteImport } from './routes/password/finish'
+import { Route as OpenidCallbackRouteImport } from './routes/openid/callback'
const TestRoute = TestRouteImport.update({
id: '/test',
@@ -64,6 +65,11 @@ const PasswordFinishRoute = PasswordFinishRouteImport.update({
path: '/password/finish',
getParentRoute: () => rootRouteImport,
} as any)
+const OpenidCallbackRoute = OpenidCallbackRouteImport.update({
+ id: '/openid/callback',
+ path: '/openid/callback',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -72,6 +78,7 @@ export interface FileRoutesByFullPath {
'/enrollment-start': typeof EnrollmentStartRoute
'/password-reset': typeof PasswordResetRoute
'/test': typeof TestRoute
+ '/openid/callback': typeof OpenidCallbackRoute
'/password/finish': typeof PasswordFinishRoute
'/password/sent': typeof PasswordSentRoute
'/password': typeof PasswordIndexRoute
@@ -83,6 +90,7 @@ export interface FileRoutesByTo {
'/enrollment-start': typeof EnrollmentStartRoute
'/password-reset': typeof PasswordResetRoute
'/test': typeof TestRoute
+ '/openid/callback': typeof OpenidCallbackRoute
'/password/finish': typeof PasswordFinishRoute
'/password/sent': typeof PasswordSentRoute
'/password': typeof PasswordIndexRoute
@@ -95,6 +103,7 @@ export interface FileRoutesById {
'/enrollment-start': typeof EnrollmentStartRoute
'/password-reset': typeof PasswordResetRoute
'/test': typeof TestRoute
+ '/openid/callback': typeof OpenidCallbackRoute
'/password/finish': typeof PasswordFinishRoute
'/password/sent': typeof PasswordSentRoute
'/password/': typeof PasswordIndexRoute
@@ -108,6 +117,7 @@ export interface FileRouteTypes {
| '/enrollment-start'
| '/password-reset'
| '/test'
+ | '/openid/callback'
| '/password/finish'
| '/password/sent'
| '/password'
@@ -119,6 +129,7 @@ export interface FileRouteTypes {
| '/enrollment-start'
| '/password-reset'
| '/test'
+ | '/openid/callback'
| '/password/finish'
| '/password/sent'
| '/password'
@@ -130,6 +141,7 @@ export interface FileRouteTypes {
| '/enrollment-start'
| '/password-reset'
| '/test'
+ | '/openid/callback'
| '/password/finish'
| '/password/sent'
| '/password/'
@@ -142,6 +154,7 @@ export interface RootRouteChildren {
EnrollmentStartRoute: typeof EnrollmentStartRoute
PasswordResetRoute: typeof PasswordResetRoute
TestRoute: typeof TestRoute
+ OpenidCallbackRoute: typeof OpenidCallbackRoute
PasswordFinishRoute: typeof PasswordFinishRoute
PasswordSentRoute: typeof PasswordSentRoute
PasswordIndexRoute: typeof PasswordIndexRoute
@@ -212,6 +225,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PasswordFinishRouteImport
parentRoute: typeof rootRouteImport
}
+ '/openid/callback': {
+ id: '/openid/callback'
+ path: '/openid/callback'
+ fullPath: '/openid/callback'
+ preLoaderRoute: typeof OpenidCallbackRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
@@ -222,6 +242,7 @@ const rootRouteChildren: RootRouteChildren = {
EnrollmentStartRoute: EnrollmentStartRoute,
PasswordResetRoute: PasswordResetRoute,
TestRoute: TestRoute,
+ OpenidCallbackRoute: OpenidCallbackRoute,
PasswordFinishRoute: PasswordFinishRoute,
PasswordSentRoute: PasswordSentRoute,
PasswordIndexRoute: PasswordIndexRoute,
diff --git a/webnext/src/routes/client-setup.tsx b/webnext/src/routes/client-setup.tsx
index 635d58cb..787878fb 100644
--- a/webnext/src/routes/client-setup.tsx
+++ b/webnext/src/routes/client-setup.tsx
@@ -1,17 +1,31 @@
import { createFileRoute, redirect } from '@tanstack/react-router';
+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 { isPresent } from '../shared/defguard-ui/utils/isPresent';
import { useEnrollmentStore } from '../shared/hooks/useEnrollmentStore';
-const _fetchDesktop = async () => {
- const _resp = await fetch('https://api.git.com/repos/defguard/client/releases/latest', {
- method: 'GET',
- });
-};
+const schema = z.object({
+ code: z.string().trim().optional(),
+ state: z.string().trim().optional(),
+});
export const Route = createFileRoute('/client-setup')({
component: ConfigureClientPage,
- beforeLoad: () => {
+ validateSearch: schema,
+ loaderDeps: ({ search }) => ({ search }),
+ beforeLoad: ({ search }) => {
+ // if openId flow just pass the search to the context
+ if (search && isPresent(search.code) && isPresent(search.state)) {
+ return {
+ openid: {
+ code: search.code,
+ state: search.state,
+ },
+ };
+ }
+ // if not openId then expect state to be in session
const state = useEnrollmentStore.getState();
if (state.token === undefined || state.enrollmentData === undefined) {
throw redirect({
@@ -19,8 +33,39 @@ export const Route = createFileRoute('/client-setup')({
replace: true,
});
}
+ return {
+ openid: undefined,
+ };
},
- loader: () => {
+ loader: async ({ context: { openid } }) => {
+ if (openid) {
+ try {
+ const openIdResponse = await api.openId.enrollmentCallback.callbackFn({
+ data: {
+ code: openid.code,
+ state: openid.state,
+ type: 'enrollment',
+ },
+ });
+
+ const enrollResponse = await api.enrollment.start.callbackFn({
+ data: {
+ token: openIdResponse.data.token,
+ },
+ });
+
+ return {
+ token: openIdResponse.data.token,
+ enrollmentData: enrollResponse.data,
+ };
+ } catch (e) {
+ console.error(e);
+ throw redirect({
+ to: '/',
+ replace: true,
+ });
+ }
+ }
const state = useEnrollmentStore.getState();
return {
token: state.token as string,
diff --git a/webnext/src/routes/enrollment-start.tsx b/webnext/src/routes/enrollment-start.tsx
index 68785a8b..b8c546f8 100644
--- a/webnext/src/routes/enrollment-start.tsx
+++ b/webnext/src/routes/enrollment-start.tsx
@@ -1,4 +1,4 @@
-import { createFileRoute, redirect } from '@tanstack/react-router';
+import { createFileRoute } from '@tanstack/react-router';
import type { AxiosError } from 'axios';
import { EnrollmentStartPage } from '../pages/enrollment/EnrollmentStart/EnrollmentStartPage';
import { api } from '../shared/api/api';
@@ -6,6 +6,7 @@ import { api } from '../shared/api/api';
export const Route = createFileRoute('/enrollment-start')({
component: EnrollmentStartPage,
loader: async () => {
+ // workaround cuz this endpoint throws 404 when openid is not configured
const resp = await api.openId.authInfo
.callbackFn({
data: {
@@ -13,15 +14,8 @@ export const Route = createFileRoute('/enrollment-start')({
},
})
.catch((e: AxiosError) => {
- //FIXME: should redirect
- if (e.status !== 500) {
- throw redirect({
- to: '/',
- replace: true,
- });
- }
+ console.error(e);
});
- //FIXME
return resp?.data;
},
});
diff --git a/webnext/src/routes/openid/callback.tsx b/webnext/src/routes/openid/callback.tsx
new file mode 100644
index 00000000..47173022
--- /dev/null
+++ b/webnext/src/routes/openid/callback.tsx
@@ -0,0 +1,24 @@
+import { createFileRoute, redirect } from '@tanstack/react-router';
+import z from 'zod';
+
+const schema = z.object({
+ state: z.string().trim().min(1),
+ code: z.string().trim().min(1),
+});
+
+// this route exists only for redirect, this is done to maintain compatibility with old UI and retain existing links
+export const Route = createFileRoute('/openid/callback')({
+ component: RouteComponent,
+ validateSearch: schema,
+ loaderDeps: ({ search }) => ({ search }),
+ beforeLoad: ({ search }) => {
+ throw redirect({
+ to: '/client-setup',
+ search: search,
+ });
+ },
+});
+
+function RouteComponent() {
+ return null;
+}
diff --git a/webnext/src/shared/defguard-ui/components/FieldBox/FieldBox.tsx b/webnext/src/shared/defguard-ui/components/FieldBox/FieldBox.tsx
new file mode 100644
index 00000000..7912214b
--- /dev/null
+++ b/webnext/src/shared/defguard-ui/components/FieldBox/FieldBox.tsx
@@ -0,0 +1,49 @@
+import './style.scss';
+import clsx from 'clsx';
+import { isPresent } from '../../utils/isPresent';
+import { Icon } from '../Icon';
+import { InteractionBox } from '../InteractionBox/InteractionBox';
+import type { FieldBoxProps } from './types';
+
+// generalized field box for components like Input, shouldn't be in layout on it's own
+export const FieldBox = ({
+ children,
+ disabled,
+ error,
+ className,
+ boxRef,
+ interactionRef,
+ iconLeft,
+ iconRight,
+ size,
+ onInteractionClick,
+ ...rest
+}: FieldBoxProps) => {
+ const hasIconLeft = isPresent(iconLeft);
+ const hasIconRight = isPresent(iconRight);
+ return (
+
+ {hasIconLeft && }
+ {children}
+ {hasIconRight && (
+
+ )}
+
+ );
+};
diff --git a/webnext/src/shared/defguard-ui/components/FieldBox/style.scss b/webnext/src/shared/defguard-ui/components/FieldBox/style.scss
new file mode 100644
index 00000000..9732c9fd
--- /dev/null
+++ b/webnext/src/shared/defguard-ui/components/FieldBox/style.scss
@@ -0,0 +1,97 @@
+.field-box {
+ --outline-width: 0;
+ --border-color: var(--border-default);
+ --background-color: var(--bg-default);
+
+ position: relative;
+ box-sizing: border-box;
+ display: grid;
+ grid-template-rows: 1fr;
+ align-items: center;
+ column-gap: var(--input-spacing-sm);
+ overflow: hidden;
+ cursor: pointer;
+ border: 1px solid var(--border-color);
+ border-radius: var(--input-border-radius);
+ padding: var(--input-spacing-sm) var(--input-spacing-lg);
+ outline: var(--outline-width) solid var(--border-action);
+ outline-offset: -2px;
+
+ @include animate(border-color, background-color);
+
+ p,
+ span {
+ max-width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ &.size-default {
+ min-height: var(--input-size-primary);
+
+ p,
+ span {
+ font: var(--t-input-text-primary);
+ color: var(--fg-default);
+ }
+ }
+
+ &.size-lg {
+ min-height: var(--input-size-lg);
+
+ p,
+ span {
+ font: var(--t-input-text-big);
+ }
+ }
+
+ &.grid-default {
+ grid-template-columns: 1fr;
+ }
+
+ &.grid-left {
+ grid-template-columns: 20px 1fr;
+ }
+
+ &.grid-right {
+ grid-template-columns: 1fr 20px;
+ }
+
+ &.grid-both {
+ grid-template-columns: 20px 1fr 20px;
+ }
+
+ .placeholder {
+ color: var(--fg-muted);
+ }
+
+ .interaction-box {
+ & > button {
+ height: 28px;
+ width: 28px;
+ }
+ }
+
+ &:not(.disabled, .error) {
+ &:hover {
+ --border-color: var(--border-emphasis);
+ }
+
+ &:focus-within {
+ --border-colo: var(--border-default);
+ --outline-width: 2px;
+ }
+ }
+
+ &.error {
+ --border-color: var(--border-critical);
+ }
+
+ &.disabled {
+ --border-color: var(--border-disabled);
+ --background-color: var(--bg-disabled);
+
+ cursor: not-allowed;
+ }
+}
diff --git a/webnext/src/shared/defguard-ui/components/FieldBox/types.ts b/webnext/src/shared/defguard-ui/components/FieldBox/types.ts
new file mode 100644
index 00000000..94f4ff05
--- /dev/null
+++ b/webnext/src/shared/defguard-ui/components/FieldBox/types.ts
@@ -0,0 +1,15 @@
+import type { HTMLAttributes, MouseEventHandler, PropsWithChildren, Ref } from 'react';
+import type { IconKindValue } from '../Icon/icon-types';
+
+export type FieldSize = 'lg' | 'default';
+
+export interface FieldBoxProps extends HTMLAttributes, PropsWithChildren {
+ boxRef?: Ref;
+ interactionRef?: Ref;
+ error?: boolean;
+ disabled?: boolean;
+ iconLeft?: IconKindValue;
+ iconRight?: IconKindValue;
+ size?: FieldSize;
+ onInteractionClick?: MouseEventHandler;
+}
diff --git a/webnext/src/shared/defguard-ui/components/Icon/Icon.tsx b/webnext/src/shared/defguard-ui/components/Icon/Icon.tsx
index fe7c73a9..85cb37e1 100644
--- a/webnext/src/shared/defguard-ui/components/Icon/Icon.tsx
+++ b/webnext/src/shared/defguard-ui/components/Icon/Icon.tsx
@@ -16,12 +16,14 @@ import { IconDesktop } from './icons/IconDesktop';
import { IconEmptyPoint } from './icons/IconEmptyPoint';
import { IconFile } from './icons/IconFile';
import { IconGlobe } from './icons/IconGlobe';
+import { IconHide } from './icons/IconHide';
import { IconLinux } from './icons/IconLinux';
import { IconLoader } from './icons/IconLoader';
import { IconLockOpen } from './icons/IconLock';
import { IconMobile } from './icons/IconMobile';
import { IconOpenInNewWindow } from './icons/IconOpenInNewWindow';
import { IconPlus } from './icons/IconPlus';
+import { IconShow } from './icons/IconShow';
import { IconStatusSimple } from './icons/IconStatusSimple';
import { IconWindows } from './icons/IconWindows';
@@ -63,6 +65,10 @@ export const Icon = ({
}: Props) => {
const IconToRender = useMemo(() => {
switch (iconKind) {
+ case 'show':
+ return IconShow;
+ case 'hide':
+ return IconHide;
case 'copy':
return IconCopy;
case 'config':
diff --git a/webnext/src/shared/defguard-ui/components/Icon/icon-types.ts b/webnext/src/shared/defguard-ui/components/Icon/icon-types.ts
index eacdc835..2ac20707 100644
--- a/webnext/src/shared/defguard-ui/components/Icon/icon-types.ts
+++ b/webnext/src/shared/defguard-ui/components/Icon/icon-types.ts
@@ -1,4 +1,5 @@
export const IconKind = {
+ Hide: 'hide',
ArrowBig: 'arrow-big',
ArrowSmall: 'arrow-small',
PlusCircle: 'plus-circle',
@@ -43,7 +44,7 @@ export const IconKind = {
Activity: 'activity',
AccessSettings: 'access-settings',
Profile: 'profile',
- Attension: 'attension',
+ Attention: 'attention',
Warning: 'warning',
Download: 'download',
Code: 'code',
diff --git a/webnext/src/shared/defguard-ui/components/Icon/icons/IconHide.tsx b/webnext/src/shared/defguard-ui/components/Icon/icons/IconHide.tsx
new file mode 100644
index 00000000..3dbe44a0
--- /dev/null
+++ b/webnext/src/shared/defguard-ui/components/Icon/icons/IconHide.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react';
+
+export const IconHide = (props: SVGProps) => {
+ return (
+
+ );
+};
diff --git a/webnext/src/shared/defguard-ui/components/Icon/icons/IconShow.tsx b/webnext/src/shared/defguard-ui/components/Icon/icons/IconShow.tsx
new file mode 100644
index 00000000..54ee682f
--- /dev/null
+++ b/webnext/src/shared/defguard-ui/components/Icon/icons/IconShow.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react';
+
+export const IconShow = (props: SVGProps) => {
+ return (
+
+ );
+};
diff --git a/webnext/src/shared/defguard-ui/components/Input/Input.tsx b/webnext/src/shared/defguard-ui/components/Input/Input.tsx
index c24d6a57..0196787c 100644
--- a/webnext/src/shared/defguard-ui/components/Input/Input.tsx
+++ b/webnext/src/shared/defguard-ui/components/Input/Input.tsx
@@ -1,10 +1,12 @@
-import { useId, useRef } from 'react';
+import { type HTMLInputTypeAttribute, useId, useMemo, useRef, useState } from 'react';
import './style.scss';
import clsx from 'clsx';
import { isPresent } from '../../utils/isPresent';
import { mergeRefs } from '../../utils/mergeRefs';
+import { FieldBox } from '../FieldBox/FieldBox';
import { FieldError } from '../FieldError/FieldError';
import { FieldLabel } from '../FieldLabel/FieldLabel';
+import type { IconKindValue } from '../Icon/icon-types';
import type { InputProps } from './types';
export const Input = ({
@@ -13,17 +15,33 @@ export const Input = ({
label,
ref,
name,
+ placeholder,
+ onChange,
+ onBlur,
+ onFocus,
+ boxProps,
size = 'default',
type = 'text',
required = false,
disabled = false,
- onChange,
- onBlur,
- onFocus,
- placeholder,
+ autocomplete = 'off',
}: InputProps) => {
+ const isPassword = useMemo(() => type === 'password', [type]);
+
+ const [inputTypeInner, setInputType] = useState(type);
const innerRef = useRef(null);
const id = useId();
+
+ const interactionIconRight = useMemo((): IconKindValue | undefined => {
+ if (isPassword) {
+ if (inputTypeInner === 'password') {
+ return 'show';
+ } else {
+ return 'hide';
+ }
+ }
+ }, [isPassword, inputTypeInner]);
+
return (
{isPresent(label) &&
}
-
{
innerRef.current?.focus();
}}
+ iconRight={interactionIconRight}
+ onInteractionClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (isPassword) {
+ setInputType((s) => {
+ if (s === 'password') {
+ return 'text';
+ }
+ return 'password';
+ });
+ }
+ }}
+ {...boxProps}
>
-
+
diff --git a/webnext/src/shared/defguard-ui/components/Input/style.scss b/webnext/src/shared/defguard-ui/components/Input/style.scss
index 34047cba..b2db68c8 100644
--- a/webnext/src/shared/defguard-ui/components/Input/style.scss
+++ b/webnext/src/shared/defguard-ui/components/Input/style.scss
@@ -11,34 +11,16 @@
user-select: none;
}
- .track {
- --outline-width: 0;
- --border-color: var(--border-default);
- --background-color: var(--bg-default);
- --input-size: var(--input-size-primary);
-
- box-sizing: border-box;
- cursor: pointer;
- border: 1px solid var(--border-color);
- border-radius: var(--input-border-radius);
- position: relative;
- padding: var(--input-spacing-sm) var(--input-spacing-lg);
- display: flex;
- flex-flow: row;
- align-items: center;
- outline: var(--outline-width) solid var(--border-action);
- outline-offset: -2px;
-
- @include animate(border-color, background-color);
-
+ .input-track {
input {
font: var(--t-input-text-primary);
color: var(--fg-default);
border: none;
background: none;
border-radius: 0;
- height: var(--input-size);
width: 100%;
+ max-width: 100%;
+ overflow: hidden;
margin: 0;
padding: 0;
@@ -46,30 +28,15 @@
font: var(--t-input-text-primary);
color: var(--fg-muted);
}
- }
-
- &:not(.disabled) {
- &:hover {
- --border-color: var(--border-emphasis);
- }
- &:focus-within {
- --border-colo: var(--border-default);
- --outline-width: 2px;
+ &:disabled {
+ cursor: not-allowed;
}
}
-
- &.disabled {
- --border-color: var(--border-disabled);
- --background-color: var(--bg-disabled);
-
- cursor: not-allowed;
- pointer-events: none;
- }
}
&.disabled {
- pointer-events: none;
+ user-select: none;
& > .field-label {
cursor: not-allowed;
diff --git a/webnext/src/shared/defguard-ui/components/Input/types.ts b/webnext/src/shared/defguard-ui/components/Input/types.ts
index 4af97b19..b406c109 100644
--- a/webnext/src/shared/defguard-ui/components/Input/types.ts
+++ b/webnext/src/shared/defguard-ui/components/Input/types.ts
@@ -1,9 +1,10 @@
-import type { HTMLAttributes, HTMLInputTypeAttribute, Ref } from 'react';
+import type { HTMLAttributes, HTMLInputAutoCompleteAttribute, Ref } from 'react';
+import type { FieldBoxProps, FieldSize } from '../FieldBox/types';
export type InputProps = {
value: string | null;
- size?: 'default' | 'big';
- type?: HTMLInputTypeAttribute;
+ size?: FieldSize;
+ type?: 'password' | 'text';
ref?: Ref;
error?: string;
name?: string;
@@ -12,11 +13,13 @@ export type InputProps = {
disabled?: boolean;
placeholder?: string;
onChange?: (value: string) => void;
+ boxProps?: Partial;
+ autocomplete?: HTMLInputAutoCompleteAttribute;
} & Pick, 'onBlur' | 'onFocus'>;
export type FormInputProps = Pick<
InputProps,
- 'name' | 'placeholder' | 'disabled' | 'required' | 'label'
+ 'name' | 'placeholder' | 'disabled' | 'required' | 'label' | 'autocomplete' | 'type'
> & {
mapError?: (error: string) => string | undefined;
};
diff --git a/webnext/src/shared/defguard-ui/components/InteractionBox/InteractionBox.tsx b/webnext/src/shared/defguard-ui/components/InteractionBox/InteractionBox.tsx
index 81531e4f..c648e536 100644
--- a/webnext/src/shared/defguard-ui/components/InteractionBox/InteractionBox.tsx
+++ b/webnext/src/shared/defguard-ui/components/InteractionBox/InteractionBox.tsx
@@ -1,4 +1,4 @@
-import type { Ref } from 'react';
+import type { MouseEventHandler, Ref } from 'react';
import { Icon } from '../Icon';
import type { IconKindValue } from '../Icon/icon-types';
import './style.scss';
@@ -7,11 +7,12 @@ import clsx from 'clsx';
type Props = {
iconSize: number;
icon: IconKindValue;
- onClick?: () => void;
+ onClick?: MouseEventHandler;
id?: string;
className?: string;
ref?: Ref;
disabled?: boolean;
+ tabIndex?: number;
};
export const InteractionBox = ({
@@ -21,12 +22,21 @@ export const InteractionBox = ({
className,
id,
ref,
+ tabIndex,
disabled = false,
}: Props) => {
return (
-
+
);
};
diff --git a/webnext/src/test_components/page/TestPage.tsx b/webnext/src/test_components/page/TestPage.tsx
index ff226153..9f47f723 100644
--- a/webnext/src/test_components/page/TestPage.tsx
+++ b/webnext/src/test_components/page/TestPage.tsx
@@ -9,6 +9,7 @@ import { CounterLabel } from '../../shared/defguard-ui/components/CounterLabel/C
import { Divider } from '../../shared/defguard-ui/components/Divider/Divider';
import { EmptyState } from '../../shared/defguard-ui/components/EmptyState/EmptyState';
import { IconButton } from '../../shared/defguard-ui/components/IconButton/IconButton';
+import { Input } from '../../shared/defguard-ui/components/Input/Input';
import { Menu } from '../../shared/defguard-ui/components/Menu/Menu';
import type { MenuItemsGroup } from '../../shared/defguard-ui/components/Menu/types';
import { Modal } from '../../shared/defguard-ui/components/Modal/Modal';
@@ -126,6 +127,29 @@ export const TestPage = () => {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};