From bed20eaea3801aeb17466f8ab3c69f3112551058 Mon Sep 17 00:00:00 2001 From: dvankeke Date: Mon, 20 Apr 2026 09:50:22 +0200 Subject: [PATCH 1/9] feat: git configure modal init --- public/assets/git_icon.svg | 1 + src/components/modals/ConfigureGitModal.tsx | 262 ++++++++++++++++++ src/components/modals/gitSettingsValidator.ts | 20 ++ src/layouts/Paper.tsx | 2 - src/pages/Apps.tsx | 18 +- src/pages/SettingsOverview.tsx | 101 +++++-- 6 files changed, 365 insertions(+), 39 deletions(-) create mode 100644 public/assets/git_icon.svg create mode 100644 src/components/modals/ConfigureGitModal.tsx create mode 100644 src/components/modals/gitSettingsValidator.ts diff --git a/public/assets/git_icon.svg b/public/assets/git_icon.svg new file mode 100644 index 000000000..5bf444b9b --- /dev/null +++ b/public/assets/git_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx new file mode 100644 index 000000000..f09831c72 --- /dev/null +++ b/src/components/modals/ConfigureGitModal.tsx @@ -0,0 +1,262 @@ +import { yupResolver } from '@hookform/resolvers/yup' +import { LoadingButton } from '@mui/lab' +import { Box, Button, Modal, Typography, styled } from '@mui/material' +import { TextField } from 'components/forms/TextField' +import { useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { GitSettingsFormValues, gitSettingsSchema } from './gitSettingsValidator' + +const ModalBox = styled(Box)(({ theme }) => ({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 800, + maxWidth: '95vw', + backgroundColor: theme.palette.background.paper, + boxShadow: + 'rgb(0 0 0 / 20%) 0px 11px 15px -7px, rgb(0 0 0 / 14%) 0px 24px 38px 3px, rgb(0 0 0 / 12%) 0px 9px 46px 8px', + borderRadius: 16, + padding: 0, +})) + +const ModalContent = styled('div')({ + padding: '40px 52px 32px 52px', +}) + +const ModalFooter = styled('div')({ + borderTop: '1px dashed rgba(145, 158, 171, 0.24)', + display: 'flex', + justifyContent: 'flex-end', + padding: '24px 36px', + gap: '16px', +}) + +const SuccessBanner = styled(Box)(({ theme }) => ({ + border: `1px solid ${theme.palette.success.main}`, + color: theme.palette.common.white, + borderRadius: 2, + minHeight: 50, + display: 'flex', + alignItems: 'center', + padding: '0 32px', + whiteSpace: 'nowrap', +})) + +interface ConfigureGitModalProps { + open: boolean + onClose: () => void +} + +export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalProps) { + const [connectionLoading, setConnectionLoading] = useState(false) + const [connectionSuccess, setConnectionSuccess] = useState(false) + + const { + control, + handleSubmit, + trigger, + getValues, + formState: { errors, isSubmitting }, + reset, + } = useForm({ + resolver: yupResolver(gitSettingsSchema), + defaultValues: { + repoUrl: '', + branch: '', + username: '', + password: '', + email: '', + }, + mode: 'onBlur', + }) + + const handleTestConnection = async () => { + const valid = await trigger() + if (!valid) { + setConnectionSuccess(false) + return + } + + setConnectionLoading(true) + setConnectionSuccess(false) + + try { + const values = getValues() + + // replace this with the real API call later + await new Promise((resolve) => { + setTimeout(resolve, 800) + }) + + if (values.repoUrl && values.branch && values.username && values.password && values.email) + setConnectionSuccess(true) + } finally { + setConnectionLoading(false) + } + } + + const onSubmit = async (data: GitSettingsFormValues) => { + // wire actual save mutation here + console.log('Git config submit:', data) + onClose() + reset() + setConnectionSuccess(false) + } + + const handleClose = () => { + onClose() + reset() + setConnectionSuccess(false) + } + + return ( + + +
+ + + Configure Git - Bring your own Git + + + + Repository + + + + ( + + )} + /> + + + + ( + + )} + /> + + + + + Credentials + + + Username and password will be used to authenticate to Git repository + + + + ( + + )} + /> + + ( + + )} + /> + + + + ( + + )} + /> + + + + + Test Connection + + + {connectionSuccess && Successfully connected with Git repository} + + + + + + + + + Proceed + + +
+
+
+ ) +} diff --git a/src/components/modals/gitSettingsValidator.ts b/src/components/modals/gitSettingsValidator.ts new file mode 100644 index 000000000..6839efa58 --- /dev/null +++ b/src/components/modals/gitSettingsValidator.ts @@ -0,0 +1,20 @@ +import * as yup from 'yup' + +export interface GitSettingsFormValues { + repoUrl: string + branch: string + username: string + password: string + email: string +} + +export const gitSettingsSchema = yup.object({ + repoUrl: yup + .string() + .required('Git repository URL is required') + .matches(/^(https:\/\/|git@).+/, 'Enter a valid Git repository URL starting with https:// or git@'), + branch: yup.string().required('Branch is required'), + username: yup.string().required('Username is required'), + password: yup.string().required('Password is required'), + email: yup.string().email('Enter a valid email address').required('Email address is required'), +}) diff --git a/src/layouts/Paper.tsx b/src/layouts/Paper.tsx index e8c389053..37243ad34 100644 --- a/src/layouts/Paper.tsx +++ b/src/layouts/Paper.tsx @@ -5,7 +5,6 @@ import { useLocation } from 'react-router-dom' import Error from 'components/Error' import { useAppDispatch, useAppSelector } from 'redux/hooks' import { setError } from 'redux/reducers' -import ObjWizardModal from 'components/ObjWizardModal' import MainLayout from './Base' interface Props { @@ -38,7 +37,6 @@ export default function ({ loading, comp, title, children }: Props): React.React - ) } diff --git a/src/pages/Apps.tsx b/src/pages/Apps.tsx index 0df7b5732..bdf67263c 100644 --- a/src/pages/Apps.tsx +++ b/src/pages/Apps.tsx @@ -1,4 +1,5 @@ import Apps from 'components/Apps' +import ObjWizardModal from 'components/ObjWizardModal' import useAuthzSession from 'hooks/useAuthzSession' import useSettings from 'hooks/useSettings' import PaperLayout from 'layouts/Paper' @@ -57,13 +58,16 @@ export default function ({ const loading = isLoading || isLoadingSettings const comp = apps && ( - + <> + + + ) return diff --git a/src/pages/SettingsOverview.tsx b/src/pages/SettingsOverview.tsx index d9a318367..ef5d2ff8b 100644 --- a/src/pages/SettingsOverview.tsx +++ b/src/pages/SettingsOverview.tsx @@ -3,23 +3,28 @@ /* eslint-disable react/function-component-definition */ import PaperLayout from 'layouts/Paper' import SvgIconStyle from 'components/SvgIconStyle' -import React from 'react' +import { useState } from 'react' import { Link } from 'react-router-dom' import { Box, Grid, Typography } from '@mui/material' import Versions from 'components/Versions' import { jsx } from '@emotion/react' import { useSession } from 'providers/Session' +import ConfigureGitModal from 'components/modals/ConfigureGitModal' interface Settings { title: string - path: string + path?: string icon: jsx.JSX.Element id: string + onClick?: () => void } + export default function SettingsOverview() { const getIcon = (name: string) => const session = useSession() - const settings = [ + const [openGitModal, setOpenGitModal] = useState(false) + + const settings: Settings[] = [ { title: 'Cluster', path: '/settings/cluster', icon: getIcon('cluster_icon.svg'), id: 'cluster' }, { title: 'Platform', path: '/settings/otomi', icon: getIcon('akamai_icon.svg'), id: 'aplSettings' }, { title: 'Secrets', path: '/settings/kms', icon: getIcon('secrets_icon.svg'), id: 'kms' }, @@ -29,51 +34,87 @@ export default function SettingsOverview() { { title: 'OIDC', path: '/settings/oidc', icon: getIcon('oidc_icon.svg'), id: 'oidc' }, { title: 'Backup', path: '/settings/platformBackups', icon: getIcon('backup_icon.svg'), id: 'backup' }, { title: 'Object Storage', path: '/settings/obj', icon: getIcon('cloud_upload.svg'), id: 'objectStorage' }, + { + title: 'Git', + icon: getIcon('git_icon.svg'), + id: 'git', + onClick: () => setOpenGitModal(true), + }, ] + const removePreInstalledSpecificSettings = ['kms', 'dns', 'ingress'] let filteredSettings: Settings[] = settings + if (session.settings.otomi.isPreInstalled) filteredSettings = settings.filter((setting) => !removePreInstalledSpecificSettings.includes(setting.id)) - // TODO: remove inline styling and use theming - const SettingsCard = ({ title, path, icon }) => ( + + const CardContent = ({ title, icon }: { title: string; icon: jsx.JSX.Element }) => ( + + + {icon} + + + + {title} + + + + ) + + const SettingsCard = ({ title, path, icon, onClick }: Settings) => ( - + {path ? ( + + + + ) : ( - - {icon} - - - - {title} - - + - + )} ) const comp = (
- {filteredSettings.map((setting) => { - return - })} + {filteredSettings.map((setting) => ( + + ))} + + setOpenGitModal(false)} />
) From 4d4090de06a159571ba5bad1a1e8527f147a532f Mon Sep 17 00:00:00 2001 From: dvankeke Date: Mon, 20 Apr 2026 10:41:49 +0200 Subject: [PATCH 2/9] feat: welcome modal with transition and api store --- src/components/modals/ConfigureGitModal.tsx | 363 ++++++++++++-------- src/redux/otomiApi.ts | 23 ++ 2 files changed, 237 insertions(+), 149 deletions(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index f09831c72..2f5248060 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -2,7 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup' import { LoadingButton } from '@mui/lab' import { Box, Button, Modal, Typography, styled } from '@mui/material' import { TextField } from 'components/forms/TextField' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { GitSettingsFormValues, gitSettingsSchema } from './gitSettingsValidator' @@ -18,10 +18,12 @@ const ModalBox = styled(Box)(({ theme }) => ({ 'rgb(0 0 0 / 20%) 0px 11px 15px -7px, rgb(0 0 0 / 14%) 0px 24px 38px 3px, rgb(0 0 0 / 12%) 0px 9px 46px 8px', borderRadius: 16, padding: 0, + overflow: 'hidden', })) const ModalContent = styled('div')({ padding: '40px 52px 32px 52px', + minHeight: 360, }) const ModalFooter = styled('div')({ @@ -51,6 +53,8 @@ interface ConfigureGitModalProps { export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalProps) { const [connectionLoading, setConnectionLoading] = useState(false) const [connectionSuccess, setConnectionSuccess] = useState(false) + const [showFormStep, setShowFormStep] = useState(false) + const [isTransitioning, setIsTransitioning] = useState(false) const { control, @@ -71,6 +75,25 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr mode: 'onBlur', }) + useEffect(() => { + if (!open) { + setShowFormStep(false) + setConnectionSuccess(false) + setConnectionLoading(false) + setIsTransitioning(false) + reset() + } + }, [open, reset]) + + const goToFormStep = () => { + setIsTransitioning(true) + + setTimeout(() => { + setShowFormStep(true) + setIsTransitioning(false) + }, 180) + } + const handleTestConnection = async () => { const valid = await trigger() if (!valid) { @@ -83,11 +106,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr try { const values = getValues() - - // replace this with the real API call later - await new Promise((resolve) => { - setTimeout(resolve, 800) - }) + await new Promise((resolve) => setTimeout(resolve, 800)) if (values.repoUrl && values.branch && values.username && values.password && values.email) setConnectionSuccess(true) @@ -97,165 +116,211 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr } const onSubmit = async (data: GitSettingsFormValues) => { - // wire actual save mutation here console.log('Git config submit:', data) - onClose() - reset() - setConnectionSuccess(false) + handleClose() } const handleClose = () => { onClose() - reset() - setConnectionSuccess(false) } return ( -
- - - Configure Git - Bring your own Git - - - - Repository - - - - ( - + {!showFormStep ? ( + <> + + + Configure Git + + + + App Platform is installed on a light weight Git server. + + + + This Git server is ideal for testing the platform but not recommended for production workloads. + + + + We recommend to configure an external Git Repo to install App Platform on, this step will only take a + few minutes + + + + + + + + + + ) : ( + + + + Configure Git - Bring your own Git + + + + Repository + + + + ( + + )} /> - )} - /> - - - - ( - + + + ( + + )} /> - )} - /> - - - - - Credentials - - - Username and password will be used to authenticate to Git repository - - - - ( - + + + + Credentials + + + + Username and password will be used to authenticate to Git repository + + + + ( + + )} /> - )} - /> - - ( - ( + + )} /> - )} - /> - - - - ( - + + + ( + + )} /> - )} - /> - - - - - Test Connection - + - {connectionSuccess && Successfully connected with Git repository} - - - - - - - - - Proceed - - - + + + Test Connection + + + {connectionSuccess && Successfully connected with Git repository} + + +
+ + + + + + Proceed + + + + )} +
) diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 71cf51677..7166dbf47 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -567,6 +567,12 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + migrateGit: build.mutation({ + query: (queryArg) => ({ url: `/v2/git`, method: 'PUT', body: queryArg.body }), + }), + getApiStatus: build.query({ + query: () => ({ url: `/v2/status` }), + }), }), overrideExisting: false, }) @@ -6903,6 +6909,21 @@ export type EditAppApiArg = { } } } +export type MigrateGitApiResponse = /** status 200 Migration successful. API is now locked. */ undefined +export type MigrateGitApiArg = { + /** New git configuration to migrate to. */ + body: { + repoUrl: string + username?: string + password: string + email: string + branch: string + } +} +export type GetApiStatusApiResponse = /** status 200 Successfully obtained API status. */ { + locked: boolean +} +export type GetApiStatusApiArg = void export const { useGetValuesQuery, useGetTeamsQuery, @@ -7049,4 +7070,6 @@ export const { useToggleAppsMutation, useGetTeamAppQuery, useEditAppMutation, + useMigrateGitMutation, + useGetApiStatusQuery, } = injectedRtkApi From 0170bdac93becbb49ff8fa246a8c4f1c8fbbdbe0 Mon Sep 17 00:00:00 2001 From: dvankeke Date: Mon, 20 Apr 2026 11:16:58 +0200 Subject: [PATCH 3/9] feat: api calls and succeed modal --- src/components/modals/ConfigureGitModal.tsx | 149 +++++++++++------- src/components/modals/gitSettingsValidator.ts | 2 +- 2 files changed, 97 insertions(+), 54 deletions(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index 2f5248060..cacb9d2eb 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -1,9 +1,14 @@ +/* eslint-disable no-nested-ternary */ import { yupResolver } from '@hookform/resolvers/yup' +import { Check } from '@mui/icons-material' import { LoadingButton } from '@mui/lab' import { Box, Button, Modal, Typography, styled } from '@mui/material' +import { FetchBaseQueryError } from '@reduxjs/toolkit/query' +import InformationBanner from 'components/InformationBanner' import { TextField } from 'components/forms/TextField' import { useEffect, useState } from 'react' import { Controller, useForm } from 'react-hook-form' +import { useMigrateGitMutation } from 'redux/otomiApi' import { GitSettingsFormValues, gitSettingsSchema } from './gitSettingsValidator' const ModalBox = styled(Box)(({ theme }) => ({ @@ -34,15 +39,15 @@ const ModalFooter = styled('div')({ gap: '16px', }) -const SuccessBanner = styled(Box)(({ theme }) => ({ - border: `1px solid ${theme.palette.success.main}`, - color: theme.palette.common.white, - borderRadius: 2, - minHeight: 50, +const SuccessIconWrapper = styled(Box)(({ theme }) => ({ + width: 108, + height: 108, + borderRadius: '50%', + backgroundColor: theme.palette.success.main, display: 'flex', alignItems: 'center', - padding: '0 32px', - whiteSpace: 'nowrap', + justifyContent: 'center', + margin: '24px auto 40px', })) interface ConfigureGitModalProps { @@ -50,18 +55,34 @@ interface ConfigureGitModalProps { onClose: () => void } +function getErrorMessage(error: unknown): string { + const fetchError = error as FetchBaseQueryError & { data?: { message?: string; error?: string } } + + if ('status' in (fetchError || {})) { + if (typeof fetchError.data === 'object' && fetchError.data !== null) + return fetchError.data.message || fetchError.data.error || 'Something went wrong while migrating Git settings.' + + if (fetchError.status === 503) return 'The API is currently unavailable.' + + if (fetchError.status === 400) + return 'Cannot connect to the provided Git repository. Check the repository URL and credentials.' + } + + return 'Something went wrong while migrating Git settings.' +} + export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalProps) { - const [connectionLoading, setConnectionLoading] = useState(false) - const [connectionSuccess, setConnectionSuccess] = useState(false) const [showFormStep, setShowFormStep] = useState(false) const [isTransitioning, setIsTransitioning] = useState(false) + const [submitError, setSubmitError] = useState('') + const [migrationSucceeded, setMigrationSucceeded] = useState(false) + + const [migrateGit, { isLoading: isMigrating }] = useMigrateGitMutation() const { control, handleSubmit, - trigger, - getValues, - formState: { errors, isSubmitting }, + formState: { errors }, reset, } = useForm({ resolver: yupResolver(gitSettingsSchema), @@ -78,8 +99,8 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr useEffect(() => { if (!open) { setShowFormStep(false) - setConnectionSuccess(false) - setConnectionLoading(false) + setSubmitError('') + setMigrationSucceeded(false) setIsTransitioning(false) reset() } @@ -94,38 +115,33 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr }, 180) } - const handleTestConnection = async () => { - const valid = await trigger() - if (!valid) { - setConnectionSuccess(false) - return - } - - setConnectionLoading(true) - setConnectionSuccess(false) + const onSubmit = async (data: GitSettingsFormValues) => { + setSubmitError('') + setMigrationSucceeded(false) try { - const values = getValues() - await new Promise((resolve) => setTimeout(resolve, 800)) - - if (values.repoUrl && values.branch && values.username && values.password && values.email) - setConnectionSuccess(true) - } finally { - setConnectionLoading(false) + await migrateGit({ + body: { + repoUrl: data.repoUrl.trim(), + branch: data.branch.trim(), + username: data.username?.trim() || undefined, + password: data.password, + email: data.email.trim(), + }, + }).unwrap() + + setMigrationSucceeded(true) + } catch (error) { + setSubmitError(getErrorMessage(error)) } } - const onSubmit = async (data: GitSettingsFormValues) => { - console.log('Git config submit:', data) - handleClose() - } - const handleClose = () => { onClose() } return ( - + + ) : migrationSucceeded ? ( + <> + + + Configure Git - Bring your own Git + + + + + + + + + Successfully connected to external repo + + + + App platform will be restarted, and might be unavailable for a few minutes + + + + (You can now close this window) + + + + + + + + ) : (
@@ -178,9 +226,17 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr Configure Git - Bring your own Git + {!!submitError && } + Repository @@ -277,7 +333,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr /> - + - - - - Test Connection - - - {connectionSuccess && Successfully connected with Git repository} - - - + Proceed diff --git a/src/components/modals/gitSettingsValidator.ts b/src/components/modals/gitSettingsValidator.ts index 6839efa58..8f3930cac 100644 --- a/src/components/modals/gitSettingsValidator.ts +++ b/src/components/modals/gitSettingsValidator.ts @@ -14,7 +14,7 @@ export const gitSettingsSchema = yup.object({ .required('Git repository URL is required') .matches(/^(https:\/\/|git@).+/, 'Enter a valid Git repository URL starting with https:// or git@'), branch: yup.string().required('Branch is required'), - username: yup.string().required('Username is required'), + username: yup.string().optional(), password: yup.string().required('Password is required'), email: yup.string().email('Enter a valid email address').required('Email address is required'), }) From fb1bb3730c84b6b6b0290bec44a3f77a15433b58 Mon Sep 17 00:00:00 2001 From: dvankeke Date: Mon, 20 Apr 2026 11:24:25 +0200 Subject: [PATCH 4/9] feat: lil flex --- src/components/modals/ConfigureGitModal.tsx | 38 +++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index cacb9d2eb..a769a37e7 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -1,6 +1,5 @@ /* eslint-disable no-nested-ternary */ import { yupResolver } from '@hookform/resolvers/yup' -import { Check } from '@mui/icons-material' import { LoadingButton } from '@mui/lab' import { Box, Button, Modal, Typography, styled } from '@mui/material' import { FetchBaseQueryError } from '@reduxjs/toolkit/query' @@ -48,6 +47,7 @@ const SuccessIconWrapper = styled(Box)(({ theme }) => ({ alignItems: 'center', justifyContent: 'center', margin: '24px auto 40px', + animation: 'iconPop 280ms ease-out', })) interface ConfigureGitModalProps { @@ -55,6 +55,25 @@ interface ConfigureGitModalProps { onClose: () => void } +function AnimatedCheckmark() { + return ( + + + + ) +} + function getErrorMessage(error: unknown): string { const fetchError = error as FetchBaseQueryError & { data?: { message?: string; error?: string } } @@ -148,6 +167,21 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr opacity: isTransitioning ? 0 : 1, transform: isTransitioning ? 'translateY(8px)' : 'translateY(0)', transition: 'opacity 180ms ease, transform 180ms ease', + '@keyframes drawCheck': { + to: { + strokeDashoffset: 0, + }, + }, + '@keyframes iconPop': { + '0%': { + transform: 'scale(0.7)', + opacity: 0, + }, + '100%': { + transform: 'scale(1)', + opacity: 1, + }, + }, }} > {!showFormStep ? ( @@ -196,7 +230,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr - + From 578b91ab54295d9da08391eab741fc7445173eb3 Mon Sep 17 00:00:00 2001 From: dvankeke Date: Tue, 21 Apr 2026 11:03:05 +0200 Subject: [PATCH 5/9] fix: styling tweaks --- src/components/forms/TextField.tsx | 1 - src/components/modals/ConfigureGitModal.tsx | 56 +++++++++------------ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 959f7c1ae..f5ba8d91c 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -49,7 +49,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ width: '415px', }, helperTextTop: { - color: theme.palette.cl.text.subTitle, marginTop: '-5px', fontSize: '13px', inlineSize: '70%', diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index a769a37e7..793ed4b90 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -27,7 +27,7 @@ const ModalBox = styled(Box)(({ theme }) => ({ const ModalContent = styled('div')({ padding: '40px 52px 32px 52px', - minHeight: 360, + minHeight: 300, }) const ModalFooter = styled('div')({ @@ -39,8 +39,8 @@ const ModalFooter = styled('div')({ }) const SuccessIconWrapper = styled(Box)(({ theme }) => ({ - width: 108, - height: 108, + width: 90, + height: 90, borderRadius: '50%', backgroundColor: theme.palette.success.main, display: 'flex', @@ -160,7 +160,14 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr } return ( - + { + if (reason === 'backdropClick') return + if (isMigrating) return + handleClose() + }} + > - - Configure Git + + Configure Git Repository - We recommend to configure an external Git Repo to install App Platform on, this step will only take a - few minutes + Configuring an external Git Repo is recommended for installing App Platform. @@ -224,21 +230,17 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr ) : migrationSucceeded ? ( <> - - Configure Git - Bring your own Git - - - Successfully connected to external repo + Successfully connected to Git repository - App platform will be restarted, and might be unavailable for a few minutes + The App Platform web interface is going to be restarted and will be unavailable for few minutes. @@ -256,25 +258,15 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr ) : ( - - Configure Git - Bring your own Git - - - {!!submitError && } - - Repository + Configure Git Repository + {!!submitError && } + @@ -341,7 +333,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr render={({ field }) => ( ( Date: Tue, 21 Apr 2026 11:26:04 +0200 Subject: [PATCH 6/9] feat: git modal on init console --- src/components/modals/ConfigureGitModal.tsx | 105 ++++++++++++++++---- src/layouts/Paper.tsx | 2 + 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index 793ed4b90..614f1dc18 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -5,8 +5,10 @@ import { Box, Button, Modal, Typography, styled } from '@mui/material' import { FetchBaseQueryError } from '@reduxjs/toolkit/query' import InformationBanner from 'components/InformationBanner' import { TextField } from 'components/forms/TextField' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Controller, useForm } from 'react-hook-form' +import { useLocalStorage } from 'react-use' +import { useSession } from 'providers/Session' import { useMigrateGitMutation } from 'redux/otomiApi' import { GitSettingsFormValues, gitSettingsSchema } from './gitSettingsValidator' @@ -51,8 +53,8 @@ const SuccessIconWrapper = styled(Box)(({ theme }) => ({ })) interface ConfigureGitModalProps { - open: boolean - onClose: () => void + open?: boolean + onClose?: () => void } function AnimatedCheckmark() { @@ -75,22 +77,39 @@ function AnimatedCheckmark() { } function getErrorMessage(error: unknown): string { - const fetchError = error as FetchBaseQueryError & { data?: { message?: string; error?: string } } + const fetchError = error as FetchBaseQueryError & { + data?: { message?: string; error?: string } + error?: string + originalStatus?: number + status?: number | string + } if ('status' in (fetchError || {})) { if (typeof fetchError.data === 'object' && fetchError.data !== null) return fetchError.data.message || fetchError.data.error || 'Something went wrong while migrating Git settings.' + if (fetchError.status === 'PARSING_ERROR' && fetchError.originalStatus === 200) return '' + if (fetchError.status === 503) return 'The API is currently unavailable.' if (fetchError.status === 400) return 'Cannot connect to the provided Git repository. Check the repository URL and credentials.' + + if (typeof fetchError.error === 'string' && fetchError.error.length > 0) return fetchError.error } return 'Something went wrong while migrating Git settings.' } export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalProps) { + const { + user: { isPlatformAdmin }, + settings: { + otomi: { isPreInstalled }, + }, + } = useSession() + + const [showGitWizard, setShowGitWizard] = useLocalStorage('showGitConfigureWizard', true) const [showFormStep, setShowFormStep] = useState(false) const [isTransitioning, setIsTransitioning] = useState(false) const [submitError, setSubmitError] = useState('') @@ -115,15 +134,45 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr mode: 'onBlur', }) + const isControlled = typeof open === 'boolean' + + const actualOpen = useMemo(() => { + if (isControlled) return !!open + return !!showGitWizard + }, [isControlled, open, showGitWizard]) + + useEffect(() => { + if (showGitWizard === undefined) setShowGitWizard(true) + }, [showGitWizard, setShowGitWizard]) + useEffect(() => { - if (!open) { + if (!isPreInstalled && !isControlled) setShowGitWizard(false) + }, [isPreInstalled, isControlled, setShowGitWizard]) + + useEffect(() => { + if (!actualOpen) { setShowFormStep(false) setSubmitError('') setMigrationSucceeded(false) setIsTransitioning(false) reset() } - }, [open, reset]) + }, [actualOpen, reset]) + + const handleClose = () => { + setShowFormStep(false) + setSubmitError('') + setMigrationSucceeded(false) + setIsTransitioning(false) + reset() + + if (isControlled) { + onClose?.() + return + } + + setShowGitWizard(false) + } const goToFormStep = () => { setIsTransitioning(true) @@ -139,7 +188,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr setMigrationSucceeded(false) try { - await migrateGit({ + const result = await migrateGit({ body: { repoUrl: data.repoUrl.trim(), branch: data.branch.trim(), @@ -147,21 +196,37 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr password: data.password, email: data.email.trim(), }, - }).unwrap() - - setMigrationSucceeded(true) + }) + + if ('data' in result) { + setMigrationSucceeded(true) + return + } + + if ('error' in result) { + const message = getErrorMessage(result.error) + if (!message) { + setMigrationSucceeded(true) + return + } + setSubmitError(message) + } } catch (error) { - setSubmitError(getErrorMessage(error)) + const message = getErrorMessage(error) + if (!message) { + setMigrationSucceeded(true) + return + } + setSubmitError(message) } } - const handleClose = () => { - onClose() - } + if (!isPlatformAdmin || !isPreInstalled) return null + if (!isControlled && !showGitWizard) return null return ( { if (reason === 'backdropClick') return if (isMigrating) return @@ -249,10 +314,12 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr - - + + + + ) : ( diff --git a/src/layouts/Paper.tsx b/src/layouts/Paper.tsx index 37243ad34..fc13bb4dc 100644 --- a/src/layouts/Paper.tsx +++ b/src/layouts/Paper.tsx @@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom' import Error from 'components/Error' import { useAppDispatch, useAppSelector } from 'redux/hooks' import { setError } from 'redux/reducers' +import ConfigureGitModal from 'components/modals/ConfigureGitModal' import MainLayout from './Base' interface Props { @@ -37,6 +38,7 @@ export default function ({ loading, comp, title, children }: Props): React.React + ) } From 7f8a0afaab71789bc05cc616ac7e582cc1a2ee6c Mon Sep 17 00:00:00 2001 From: dvankeke Date: Tue, 21 Apr 2026 11:46:51 +0200 Subject: [PATCH 7/9] fix: code quality --- src/components/modals/ConfigureGitModal.tsx | 256 +++++++++++--------- 1 file changed, 148 insertions(+), 108 deletions(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index 614f1dc18..e0b0bea12 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -12,6 +12,8 @@ import { useSession } from 'providers/Session' import { useMigrateGitMutation } from 'redux/otomiApi' import { GitSettingsFormValues, gitSettingsSchema } from './gitSettingsValidator' +const MODAL_TITLE = 'Configure Git Repository' + const ModalBox = styled(Box)(({ theme }) => ({ position: 'absolute', top: '50%', @@ -27,6 +29,29 @@ const ModalBox = styled(Box)(({ theme }) => ({ overflow: 'hidden', })) +const AnimatedContainer = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isTransitioning', +})<{ isTransitioning: boolean }>(({ isTransitioning }) => ({ + opacity: isTransitioning ? 0 : 1, + transform: isTransitioning ? 'translateY(8px)' : 'translateY(0)', + transition: 'opacity 180ms ease, transform 180ms ease', + '@keyframes drawCheck': { + to: { + strokeDashoffset: 0, + }, + }, + '@keyframes iconPop': { + '0%': { + transform: 'scale(0.7)', + opacity: 0, + }, + '100%': { + transform: 'scale(1)', + opacity: 1, + }, + }, +})) + const ModalContent = styled('div')({ padding: '40px 52px 32px 52px', minHeight: 300, @@ -40,6 +65,73 @@ const ModalFooter = styled('div')({ gap: '16px', }) +const CenteredFooterActions = styled(Box)({ + width: '100%', + display: 'flex', + justifyContent: 'center', +}) + +const ModalTitle = styled(Typography)({ + marginBottom: '25px', + fontWeight: 600, + letterSpacing: 0, + fontSize: '1.8rem', +}) + +const BodyText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: '1.05rem', + lineHeight: 1.5, +})) + +const IntroParagraph = styled(BodyText)({ + marginBottom: '32px', +}) + +const SectionTitle = styled(Typography)({ + marginBottom: '4px', + fontWeight: 550, + letterSpacing: '0.035em', +}) + +const SectionDescription = styled(Typography)(({ theme }) => ({ + marginBottom: '10px', + color: theme.palette.text.secondary, +})) + +const DividerSection = styled(Box)({ + borderTop: '1px solid rgba(145, 158, 171, 0.24)', + paddingTop: '16px', +}) + +const RepoFieldBlock = styled(Box)({ + marginBottom: '24px', +}) + +const BranchFieldBlock = styled(Box)({ + marginBottom: '24px', +}) + +const FieldsGrid = styled(Box)({ + display: 'grid', + gridTemplateColumns: '1fr', + gap: '24px', + marginBottom: '24px', + '@media (min-width: 900px)': { + gridTemplateColumns: '1fr 1fr', + }, +}) + +const EmailFieldWrapper = styled(Box)({ + maxWidth: 840, + marginBottom: '16px', +}) + +const SuccessContainer = styled(Box)({ + textAlign: 'center', + paddingTop: '16px', +}) + const SuccessIconWrapper = styled(Box)(({ theme }) => ({ width: 90, height: 90, @@ -52,6 +144,17 @@ const SuccessIconWrapper = styled(Box)(({ theme }) => ({ animation: 'iconPop 280ms ease-out', })) +const SuccessHeading = styled(Typography)({ + marginBottom: '16px', + fontWeight: 600, +}) + +const SuccessCaption = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, + marginTop: '16px', + opacity: 0.8, +})) + interface ConfigureGitModalProps { open?: boolean onClose?: () => void @@ -89,9 +192,7 @@ function getErrorMessage(error: unknown): string { return fetchError.data.message || fetchError.data.error || 'Something went wrong while migrating Git settings.' if (fetchError.status === 'PARSING_ERROR' && fetchError.originalStatus === 200) return '' - if (fetchError.status === 503) return 'The API is currently unavailable.' - if (fetchError.status === 400) return 'Cannot connect to the provided Git repository. Check the repository URL and credentials.' @@ -135,11 +236,15 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr }) const isControlled = typeof open === 'boolean' + const actualOpen = useMemo(() => (isControlled ? !!open : !!showGitWizard), [isControlled, open, showGitWizard]) - const actualOpen = useMemo(() => { - if (isControlled) return !!open - return !!showGitWizard - }, [isControlled, open, showGitWizard]) + const resetModalState = () => { + setShowFormStep(false) + setSubmitError('') + setMigrationSucceeded(false) + setIsTransitioning(false) + reset() + } useEffect(() => { if (showGitWizard === undefined) setShowGitWizard(true) @@ -150,21 +255,11 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr }, [isPreInstalled, isControlled, setShowGitWizard]) useEffect(() => { - if (!actualOpen) { - setShowFormStep(false) - setSubmitError('') - setMigrationSucceeded(false) - setIsTransitioning(false) - reset() - } - }, [actualOpen, reset]) + if (!actualOpen) resetModalState() + }, [actualOpen]) const handleClose = () => { - setShowFormStep(false) - setSubmitError('') - setMigrationSucceeded(false) - setIsTransitioning(false) - reset() + resetModalState() if (isControlled) { onClose?.() @@ -234,52 +329,21 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr }} > - + {!showFormStep ? ( <> - - Configure Git Repository - - - - App Platform is installed on a light weight Git server. - - - + {MODAL_TITLE} + + App Platform is installed on a light weight Git server. + + This Git server is ideal for testing the platform but not recommended for production workloads. - + - + Configuring an external Git Repo is recommended for installing App Platform. - + @@ -295,46 +359,37 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr ) : migrationSucceeded ? ( <> - + - - Successfully connected to Git repository - + Successfully connected to Git repository - + The App Platform web interface is going to be restarted and will be unavailable for few minutes. - + - - (You can now close this window) - - + (You can now close this window) + - + - + ) : ( - - Configure Git Repository - + {MODAL_TITLE} {!!submitError && } - + )} /> - + - + )} /> - - - - - Credentials - - - + + + + Credentials + Username and password will be used to authenticate to Git repository - - - + + + )} /> - + - + )} /> - - + + @@ -456,7 +496,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr )} - + ) From 656d6c28bc80d87e9b18e495bf4cf74f6fbf155e Mon Sep 17 00:00:00 2001 From: dvankeke Date: Tue, 21 Apr 2026 11:49:42 +0200 Subject: [PATCH 8/9] fix: minor introParagraph lineheight tweak --- src/components/modals/ConfigureGitModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index e0b0bea12..20ec3b27c 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -85,7 +85,7 @@ const BodyText = styled(Typography)(({ theme }) => ({ })) const IntroParagraph = styled(BodyText)({ - marginBottom: '32px', + marginBottom: '24px', }) const SectionTitle = styled(Typography)({ From 5492f2117b189ae6d72cea0fafebbb3588bed40e Mon Sep 17 00:00:00 2001 From: dvankeke Date: Fri, 24 Apr 2026 14:51:27 +0200 Subject: [PATCH 9/9] fix: improve error handling --- src/components/modals/ConfigureGitModal.tsx | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/modals/ConfigureGitModal.tsx b/src/components/modals/ConfigureGitModal.tsx index 20ec3b27c..0ce86d7db 100644 --- a/src/components/modals/ConfigureGitModal.tsx +++ b/src/components/modals/ConfigureGitModal.tsx @@ -160,6 +160,11 @@ interface ConfigureGitModalProps { onClose?: () => void } +type GitMigrationResponse = { + message?: string + statusCode?: number +} + function AnimatedCheckmark() { return ( @@ -179,6 +184,15 @@ function AnimatedCheckmark() { ) } +function getGitMigrationDataError(data: unknown): string { + const response = data as GitMigrationResponse + + if (response?.statusCode && response.statusCode >= 400) + return response.message || 'Something went wrong while migrating Git settings.' + + return '' +} + function getErrorMessage(error: unknown): string { const fetchError = error as FetchBaseQueryError & { data?: { message?: string; error?: string } @@ -193,7 +207,7 @@ function getErrorMessage(error: unknown): string { if (fetchError.status === 'PARSING_ERROR' && fetchError.originalStatus === 200) return '' if (fetchError.status === 503) return 'The API is currently unavailable.' - if (fetchError.status === 400) + if (fetchError.status === 400 || fetchError.status === 404) return 'Cannot connect to the provided Git repository. Check the repository URL and credentials.' if (typeof fetchError.error === 'string' && fetchError.error.length > 0) return fetchError.error @@ -256,6 +270,7 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr useEffect(() => { if (!actualOpen) resetModalState() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [actualOpen]) const handleClose = () => { @@ -294,24 +309,35 @@ export default function ConfigureGitModal({ open, onClose }: ConfigureGitModalPr }) if ('data' in result) { + const dataError = getGitMigrationDataError(result.data) + + if (dataError) { + setSubmitError(dataError) + return + } + setMigrationSucceeded(true) return } if ('error' in result) { const message = getErrorMessage(result.error) + if (!message) { setMigrationSucceeded(true) return } + setSubmitError(message) } } catch (error) { const message = getErrorMessage(error) + if (!message) { setMigrationSucceeded(true) return } + setSubmitError(message) } }