diff --git a/dashboard/components/icons/AlertCircleIconFilled.tsx b/dashboard/components/icons/AlertCircleIconFilled.tsx new file mode 100644 index 000000000..5e9559412 --- /dev/null +++ b/dashboard/components/icons/AlertCircleIconFilled.tsx @@ -0,0 +1,24 @@ +import { SVGProps } from 'react'; + +const AlertCircleIconFilled = (props: SVGProps) => ( + + + + +); + +export default AlertCircleIconFilled; diff --git a/dashboard/components/onboarding-wizard/CredentialsButton.tsx b/dashboard/components/onboarding-wizard/CredentialsButton.tsx index e561a66eb..d45950174 100644 --- a/dashboard/components/onboarding-wizard/CredentialsButton.tsx +++ b/dashboard/components/onboarding-wizard/CredentialsButton.tsx @@ -16,20 +16,14 @@ function CredentialsButton({ return (
-
diff --git a/dashboard/components/onboarding-wizard/DatabaseErrorMessage.tsx b/dashboard/components/onboarding-wizard/DatabaseErrorMessage.tsx new file mode 100644 index 000000000..8939b9b76 --- /dev/null +++ b/dashboard/components/onboarding-wizard/DatabaseErrorMessage.tsx @@ -0,0 +1,20 @@ +function DatabaseErrorMessage() { + return ( +
+ We're sorry, but we were unable to connect to your database using the + information provided. Please ensure that the information are correct and + try again. If you continue to experience issues, please{' '} + + contact our support + {' '} + team for assistance. +
+ ); +} + +export default DatabaseErrorMessage; diff --git a/dashboard/components/onboarding-wizard/InputFileSelect.tsx b/dashboard/components/onboarding-wizard/InputFileSelect.tsx index cd3b87c19..821b99d19 100644 --- a/dashboard/components/onboarding-wizard/InputFileSelect.tsx +++ b/dashboard/components/onboarding-wizard/InputFileSelect.tsx @@ -1,4 +1,8 @@ import { MutableRefObject, ReactNode } from 'react'; +import classNames from 'classnames'; +import AlertCircleIcon from '../icons/AlertCircleIcon'; +import AlertIcon from '../icons/AlertIcon'; +import AlertCircleIconFilled from '../icons/AlertCircleIconFilled'; interface InputFileSelectProps { id: string; @@ -8,6 +12,8 @@ interface InputFileSelectProps { icon?: ReactNode; subLabel?: string; disabled?: boolean; + hasError?: boolean; + errorMessage?: string; placeholder?: string; iconClick?: () => void; handleFileChange: (event: any) => void; @@ -25,10 +31,12 @@ function InputFileSelect({ placeholder, fileInputRef, handleFileChange, - disabled = false + disabled = false, + hasError = false, + errorMessage = '' }: InputFileSelectProps) { return ( -
+
@@ -39,7 +47,7 @@ function InputFileSelect({ )} -
+
{icon && ( @@ -65,6 +78,12 @@ function InputFileSelect({ )}
+ {hasError && errorMessage && ( +
+ + {errorMessage} +
+ )}
); } diff --git a/dashboard/components/onboarding-wizard/LabelledInput.tsx b/dashboard/components/onboarding-wizard/LabelledInput.tsx index 2ac06e60d..1450783a9 100644 --- a/dashboard/components/onboarding-wizard/LabelledInput.tsx +++ b/dashboard/components/onboarding-wizard/LabelledInput.tsx @@ -9,6 +9,7 @@ interface LabelledInputProps { subLabel?: string; disabled?: boolean; placeholder?: string; + required?: boolean; onChange?: (e: ChangeEvent) => void; } @@ -20,6 +21,7 @@ function LabelledInput({ value, subLabel, placeholder, + required = false, disabled = false, onChange }: LabelledInputProps) { @@ -48,6 +50,7 @@ function LabelledInput({ value={value} disabled={disabled} placeholder={placeholder} + required={required} className={`block w-full rounded py-[14.5px] text-sm text-black-900 outline outline-black-200 focus:outline-2 focus:outline-primary ${ icon ? 'pl-10' : 'pl-3' }`} diff --git a/dashboard/pages/onboarding/choose-database.tsx b/dashboard/pages/onboarding/choose-database.tsx index 1754ef075..b6f5ebb54 100644 --- a/dashboard/pages/onboarding/choose-database.tsx +++ b/dashboard/pages/onboarding/choose-database.tsx @@ -31,8 +31,10 @@ function DatabaseLeftItem({ return (
{ - // TODO: (onboarding-wizard) complete form inputs, validation, submission and navigation + const { toast, setToast, dismissToast } = useToast(); + + const [isError, setIsError] = useState(false); + const [hostname, setHostname] = useState(''); + const [database, setDatabase] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const handleNext = (e: FormEvent) => { + e.preventDefault(); + + const payload = JSON.stringify({ + type: 'POSTGRES', + hostname, + database, + username, + password + }); + + settingsService.saveDatabaseConfig(payload).then(res => { + setIsError(false); + + if (res === Error) { + setIsError(true); + } else { + setToast({ + hasError: false, + title: 'Database connected', + message: + 'Your Postgres database has been successfully connected to Komiser.' + }); + } + }); }; return ( @@ -38,45 +74,63 @@ export default function PostgreSQLCredentials() {
-
-
- - - - -
-
+ {isError && } - +
+
+
+ setHostname(e.target.value)} + subLabel="The server where the Postgres server is hosted" + placeholder="my-postgres-server" + /> + setDatabase(e.target.value)} + subLabel="The name of the database where Komiser will insert/save the data" + placeholder="my_database" + /> + setUsername(e.target.value)} + subLabel="The Postgres username" + placeholder="user" + /> + setPassword(e.target.value)} + subLabel="The Postgres password" + placeholder="Example0000*" + /> +
+
+ + - + + + {/* Toast component */} + {toast && }
); diff --git a/dashboard/pages/onboarding/database/sqlite.tsx b/dashboard/pages/onboarding/database/sqlite.tsx index 8e94bee89..4d5185920 100644 --- a/dashboard/pages/onboarding/database/sqlite.tsx +++ b/dashboard/pages/onboarding/database/sqlite.tsx @@ -1,5 +1,5 @@ import Head from 'next/head'; -import { useRef } from 'react'; +import { ChangeEvent, useRef, useState, FormEvent } from 'react'; import { allDBProviders } from '../../../utils/providerHelper'; @@ -11,12 +11,45 @@ import Folder2Icon from '../../../components/icons/Folder2Icon'; import DatabasePurplin from '../../../components/onboarding-wizard/DatabasePurplin'; import InputFileSelect from '../../../components/onboarding-wizard/InputFileSelect'; import CredentialsButton from '../../../components/onboarding-wizard/CredentialsButton'; +import settingsService from '../../../services/settingsService'; +import useToast from '../../../components/toast/hooks/useToast'; +import Toast from '../../../components/toast/Toast'; +import DatabaseErrorMessage from '../../../components/onboarding-wizard/DatabaseErrorMessage'; export default function SqliteCredentials() { const database = allDBProviders.SQLITE; - const handleNext = () => { - // TODO: (onboarding-wizard) complete form inputs, validation, submission and navigation + const { toast, setToast, dismissToast } = useToast(); + + const [filePath, setFilePath] = useState(''); + const [isValidationError, setIsValidationError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [isError, setIsError] = useState(false); + + const handleNext = (e: FormEvent) => { + e.preventDefault(); + + if (!filePath || isError || isValidationError) return; + + const payload = JSON.stringify({ + type: 'SQLITE', + filePath + }); + + settingsService.saveDatabaseConfig(payload).then(res => { + setIsError(false); + + if (res === Error) { + setIsError(true); + } else { + setToast({ + hasError: false, + title: 'Database connected', + message: + 'Your Postgres database has been successfully connected to Komiser.' + }); + } + }); }; const fileInputRef = useRef(null); @@ -26,9 +59,26 @@ export default function SqliteCredentials() { } }; - const handleFileChange = (event: any) => { - const file = event.target.files[0]; - // TODO: (onboarding-wizard) handle file change and naming. Set Input field to file.name and use temporary file path for the upload value + const handleFileChange = (e: ChangeEvent) => { + setIsValidationError(false); + setIsError(false); + setErrorMessage(''); + setFilePath(''); + + const fileName = e.target.files?.[0].name; + + if (fileName) { + setFilePath(fileName); + if (!fileName.endsWith('.db')) { + setIsValidationError(true); + setErrorMessage( + 'Wrong file or file type not supported. Please choose a different file.' + ); + } + } else { + setIsValidationError(true); + setErrorMessage('Please choose a file.'); + } }; return ( @@ -51,28 +101,41 @@ export default function SqliteCredentials() { -
-
- } - fileInputRef={fileInputRef} - iconClick={handleButtonClick} - handleFileChange={handleFileChange} - /> + {isError && } + +
+
+
+ } + fileInputRef={fileInputRef} + iconClick={handleButtonClick} + value={filePath} + hasError={isValidationError} + errorMessage={errorMessage} + handleFileChange={handleFileChange} + /> +
-
- + + + + {/* Toast component */} + {toast && }
); diff --git a/dashboard/services/settingsService.ts b/dashboard/services/settingsService.ts index 8ebd2d28d..66a922ede 100644 --- a/dashboard/services/settingsService.ts +++ b/dashboard/services/settingsService.ts @@ -408,6 +408,19 @@ const settingsService = { } catch (error) { return Error; } + }, + + async saveDatabaseConfig(payload: string) { + try { + const res = await fetch( + `${BASE_URL}/databases`, + settings('POST', payload) + ); + const data = await res.json(); + return data; + } catch (error) { + return Error; + } } };