({
+ resolver: yupResolver(gitSettingsSchema),
+ defaultValues: {
+ repoUrl: '',
+ branch: '',
+ username: '',
+ password: '',
+ email: '',
+ },
+ mode: 'onBlur',
+ })
+
+ const isControlled = typeof open === 'boolean'
+ const actualOpen = useMemo(() => (isControlled ? !!open : !!showGitWizard), [isControlled, open, showGitWizard])
+
+ const resetModalState = () => {
+ setShowFormStep(false)
+ setSubmitError('')
+ setMigrationSucceeded(false)
+ setIsTransitioning(false)
+ reset()
+ }
+
+ useEffect(() => {
+ if (showGitWizard === undefined) setShowGitWizard(true)
+ }, [showGitWizard, setShowGitWizard])
+
+ useEffect(() => {
+ if (!isPreInstalled && !isControlled) setShowGitWizard(false)
+ }, [isPreInstalled, isControlled, setShowGitWizard])
+
+ useEffect(() => {
+ if (!actualOpen) resetModalState()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [actualOpen])
+
+ const handleClose = () => {
+ resetModalState()
+
+ if (isControlled) {
+ onClose?.()
+ return
+ }
+
+ setShowGitWizard(false)
+ }
+
+ const goToFormStep = () => {
+ setIsTransitioning(true)
+
+ setTimeout(() => {
+ setShowFormStep(true)
+ setIsTransitioning(false)
+ }, 180)
+ }
+
+ const onSubmit = async (data: GitSettingsFormValues) => {
+ setSubmitError('')
+ setMigrationSucceeded(false)
+
+ try {
+ const result = await migrateGit({
+ body: {
+ repoUrl: data.repoUrl.trim(),
+ branch: data.branch.trim(),
+ username: data.username?.trim() || undefined,
+ password: data.password,
+ email: data.email.trim(),
+ },
+ })
+
+ 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)
+ }
+ }
+
+ if (!isPlatformAdmin || !isPreInstalled) return null
+ if (!isControlled && !showGitWizard) return null
+
+ return (
+ {
+ if (reason === 'backdropClick') return
+ if (isMigrating) return
+ handleClose()
+ }}
+ >
+
+
+ {!showFormStep ? (
+ <>
+
+ {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.
+
+
+
+
+
+
+
+
+ >
+ ) : migrationSucceeded ? (
+ <>
+
+
+
+
+
+
+ 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)
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/modals/gitSettingsValidator.ts b/src/components/modals/gitSettingsValidator.ts
new file mode 100644
index 000000000..8f3930cac
--- /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().optional(),
+ 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..fc13bb4dc 100644
--- a/src/layouts/Paper.tsx
+++ b/src/layouts/Paper.tsx
@@ -5,7 +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 ObjWizardModal from 'components/ObjWizardModal'
+import ConfigureGitModal from 'components/modals/ConfigureGitModal'
import MainLayout from './Base'
interface Props {
@@ -38,7 +38,7 @@ 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)} />
)
diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts
index e4548f7f2..f2952d5ee 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,
})
@@ -6889,6 +6895,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,
@@ -7035,4 +7056,6 @@ export const {
useToggleAppsMutation,
useGetTeamAppQuery,
useEditAppMutation,
+ useMigrateGitMutation,
+ useGetApiStatusQuery,
} = injectedRtkApi