From a7db52478c41a661eccedd6ef95e2a2ea389e49b Mon Sep 17 00:00:00 2001 From: nikec <43032218+niikeec@users.noreply.github.com> Date: Wed, 19 Apr 2023 09:54:55 +0200 Subject: [PATCH] [ENG 485] Improve create library dialog (#712) * Move dialogs into router * Prevent tab focus on show password button * Add className props to radio group * Add password strength meter to password input * Add steps to dialog * Update create library dialog * Update select component * Add select form field * Add advanced options to create lib dialog * Remove spacing * Update checkbox styles * Revert to single page, add checkbox & accordion, remove privacy * Revert "Add className props to radio group" This reverts commit e53e53cbdf6b57e8f5eb95ffe2a3a531fc6e2966. * Move schema, switch to camel case * Revert "Add steps to dialog" This reverts commit e672e1c47258fb170c8670e3769ba0e66f3119de. * Revert "Prevent tab focus on show password button" This reverts commit d42e4dc9887aced63eb0b67eac4e56d5d96bb511. * Add margin * add debounce to password strength * Update select --- .../settings/node/libraries/CreateDialog.tsx | 347 +++++++++--------- interface/app/index.tsx | 8 +- interface/app/onboarding/privacy.tsx | 2 +- interface/index.tsx | 2 - packages/ui/package.json | 3 +- packages/ui/src/CheckBox.tsx | 6 +- packages/ui/src/Select.tsx | 101 ++--- packages/ui/src/forms/FormField.tsx | 5 +- packages/ui/src/forms/Input.tsx | 107 +++++- packages/ui/src/forms/Select.tsx | 24 ++ packages/ui/src/forms/index.ts | 1 + pnpm-lock.yaml | Bin 815131 -> 825577 bytes 12 files changed, 369 insertions(+), 237 deletions(-) create mode 100644 packages/ui/src/forms/Select.tsx diff --git a/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx b/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx index c4aaca481..f35ddafaa 100644 --- a/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx +++ b/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx @@ -1,223 +1,222 @@ import { useQueryClient } from '@tanstack/react-query'; -import { ArrowsClockwise, Clipboard, Eye, EyeSlash, Info } from 'phosphor-react'; -import { useState } from 'react'; +import clsx from 'clsx'; +import { CaretRight } from 'phosphor-react'; +import { useEffect, useState } from 'react'; +import { Controller } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; import { - Algorithm, HASHING_ALGOS, - HashingAlgoSlug, + LibraryConfigWrapped, hashingAlgoSlugSchema, useBridgeMutation, usePlausibleEvent } from '@sd/client'; import { Button, - CheckBox, Dialog, - Select, + RadixCheckbox, SelectOption, - Tooltip, UseDialogProps, forms, useDialog } from '@sd/ui'; -import { PasswordMeter } from '~/components/PasswordMeter'; -import { generatePassword } from '~/util'; -const { Input, z, useZodForm } = forms; +const { Input, z, useZodForm, PasswordInput, Select } = forms; -const schema = z.object({ - name: z.string(), - password: z.string(), - password_validate: z.string(), - algorithm: z.string(), - hashing_algorithm: hashingAlgoSlugSchema, - share_telemetry: z.boolean() -}); - -export default (props: UseDialogProps) => { - const dialog = useDialog(props); - const submitPlausibleEvent = usePlausibleEvent(); - - const form = useZodForm({ - schema, - defaultValues: { - password: '', - algorithm: 'XChaCha20Poly1305', - hashing_algorithm: 'Argon2id-s' +const schema = z + .object({ + name: z.string().min(1), + encryptLibrary: z.boolean(), + password: z.string(), + passwordValidate: z.string(), + algorithm: z.enum(['XChaCha20Poly1305', 'Aes256Gcm']), + hashingAlgorithm: hashingAlgoSlugSchema + }) + .superRefine((data, ctx) => { + if (data.encryptLibrary && !data.password) { + ctx.addIssue({ + code: 'custom', + path: ['password'], + message: 'Password is required' + }); + } + if (data.password && data.password !== data.passwordValidate) { + ctx.addIssue({ + code: 'custom', + path: ['passwordValidate'], + message: 'Passwords do not match' + }); } }); - const [showMasterPassword1, setShowMasterPassword1] = useState(false); - const [showMasterPassword2, setShowMasterPassword2] = useState(false); - const MP1CurrentEyeIcon = showMasterPassword1 ? EyeSlash : Eye; - const MP2CurrentEyeIcon = showMasterPassword2 ? EyeSlash : Eye; - +export default (props: UseDialogProps) => { + const dialog = useDialog(props); + const navigate = useNavigate(); const queryClient = useQueryClient(); + const submitPlausibleEvent = usePlausibleEvent(); + + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const createLibrary = useBridgeMutation('library.create', { onSuccess: (library) => { - queryClient.setQueryData(['library.list'], (libraries: any) => [ - ...(libraries || []), - library - ]); + queryClient.setQueryData( + ['library.list'], + (libraries: LibraryConfigWrapped[] | undefined) => [...(libraries || []), library] + ); submitPlausibleEvent({ event: { type: 'libraryCreate' } }); + + navigate(`/${library.uuid}/overview`); }, - onError: (err: any) => { - console.error(err); + onError: (err) => console.log(err) + }); + + const form = useZodForm({ + schema: schema, + defaultValues: { + encryptLibrary: false, + password: '', + passwordValidate: '', + algorithm: 'XChaCha20Poly1305', + hashingAlgorithm: 'Argon2id-s' } }); const onSubmit = form.handleSubmit(async (data) => { - if (data.password !== data.password_validate) { - alert('Passwords are not the same'); - } else { - await createLibrary.mutateAsync({ - ...data, - algorithm: data.algorithm as Algorithm, - hashing_algorithm: HASHING_ALGOS[data.hashing_algorithm], - auth: { - type: 'Password', - value: data.password - } - }); - } + await createLibrary.mutateAsync({ + name: data.name, + algorithm: data.algorithm, + hashing_algorithm: HASHING_ALGOS[data.hashingAlgorithm], + auth: { + type: 'Password', + value: data.encryptLibrary ? data.password : '' + } + }); }); + const encryptLibrary = form.watch('encryptLibrary'); + + useEffect(() => { + if (showAdvancedOptions) setShowAdvancedOptions(false); + }, [encryptLibrary]); + return ( - - -
-
- -
- Share anonymous usage - - - -
- - {/* TODO: Proper UI for this. Maybe checkbox for encrypted or not and then reveal these fields. Select encrypted by default. */} - {/* Make the secret key field empty to skip key setup. */} - -
-

Key Manager

-
- +
- - - -
- } + {...form.register('name')} + label="Library name" + placeholder={'e.g. "James\' Library"'} + size="md" /> + + ( + + )} + /> + + {encryptLibrary && ( + <> +
+ + + form.trigger('passwordValidate') + })} + label="Confirm password" + /> + +
+ + + {showAdvancedOptions && ( +
+
+ + + +
+ )} +
+ + )}
- - setShowMasterPassword2(!showMasterPassword2)} - size="icon" - > - - - } - {...form.register('password_validate')} - /> - -
-
- Encryption - -
-
- Hashing - -
-
- -
); }; diff --git a/interface/app/index.tsx b/interface/app/index.tsx index 03b36590a..dd855d699 100644 --- a/interface/app/index.tsx +++ b/interface/app/index.tsx @@ -1,5 +1,6 @@ import { Navigate, Outlet, RouteObject } from 'react-router-dom'; import { currentLibraryCache, useCachedLibraries, useInvalidateQuery } from '@sd/client'; +import { Dialogs } from '@sd/ui'; import { useKeybindHandler } from '~/hooks/useKeyboardHandler'; import libraryRoutes from './$libraryId'; import onboardingRoutes from './onboarding'; @@ -23,7 +24,12 @@ const Wrapper = () => { useKeybindHandler(); useInvalidateQuery(); - return ; + return ( + <> + + + + ); }; // NOTE: all route `Layout`s below should contain diff --git a/interface/app/onboarding/privacy.tsx b/interface/app/onboarding/privacy.tsx index 5520fe3b4..1dccf1b64 100644 --- a/interface/app/onboarding/privacy.tsx +++ b/interface/app/onboarding/privacy.tsx @@ -5,7 +5,7 @@ import { Form, RadioGroup, useZodForm, z } from '@sd/ui/src/forms'; import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './Layout'; import { useUnlockOnboardingScreen } from './Progress'; -const shareTelemetry = RadioGroup.options([ +export const shareTelemetry = RadioGroup.options([ z.literal('share-telemetry'), z.literal('no-telemetry') ]).details({ diff --git a/interface/index.tsx b/interface/index.tsx index fe7473622..34f69f398 100644 --- a/interface/index.tsx +++ b/interface/index.tsx @@ -9,7 +9,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import { ErrorBoundary } from 'react-error-boundary'; import { RouterProvider, RouterProviderProps } from 'react-router-dom'; import { useDebugState } from '@sd/client'; -import { Dialogs } from '@sd/ui'; import ErrorFallback from './ErrorFallback'; export * from './util/keybind'; @@ -49,7 +48,6 @@ export const SpacedriveInterface = (props: { router: RouterProviderProps['router - ); }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 47c740940..cda086203 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -41,7 +41,8 @@ "react-loading-icons": "^1.1.0", "react-router-dom": "6.9.0", "react-spring": "^9.5.5", - "tailwindcss-radix": "^2.6.0" + "tailwindcss-radix": "^2.6.0", + "use-debounce": "^9.0.4" }, "devDependencies": { "@babel/core": "^7.19.3", diff --git a/packages/ui/src/CheckBox.tsx b/packages/ui/src/CheckBox.tsx index 394b1130e..6b82a1b2d 100644 --- a/packages/ui/src/CheckBox.tsx +++ b/packages/ui/src/CheckBox.tsx @@ -25,18 +25,18 @@ export interface RadixCheckboxProps extends ComponentProps // TODO: Replace above with this, requires refactor of usage export const RadixCheckbox = (props: RadixCheckboxProps) => ( -
+
- + {props.label && ( -