Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/foxops/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from foxops.logger import get_logger, setup_logging
from foxops.middlewares import request_id_middleware, request_time_middleware
from foxops.openapi import custom_openapi
from foxops.routers import auth, incarnations, not_found, version
from foxops.routers import auth, incarnations, not_found, template, version

#: Holds the module logger instance
logger = get_logger(__name__)
Expand Down Expand Up @@ -48,6 +48,7 @@ def create_app():
# Add routes to the protected router (authentication required)
protected_router = APIRouter(dependencies=[Depends(static_token_auth_scheme)])
protected_router.include_router(incarnations.router)
protected_router.include_router(template.router)

app.include_router(public_router)
app.include_router(protected_router)
Expand Down
7 changes: 7 additions & 0 deletions src/foxops/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from foxops.logger import get_logger
from foxops.services.change import ChangeService
from foxops.services.incarnation import IncarnationService
from foxops.services.template import TemplateService
from foxops.settings import (
DatabaseSettings,
GitlabHosterSettings,
Expand Down Expand Up @@ -94,6 +95,12 @@ def get_incarnation_service(
return IncarnationService(incarnation_repository=incarnation_repository, hoster=hoster)


def get_template_service(
hoster: Hoster = Depends(get_hoster),
):
return TemplateService(hoster=hoster)


def get_change_service(
hoster: Hoster = Depends(get_hoster),
change_repository: ChangeRepository = Depends(get_change_repository),
Expand Down
15 changes: 15 additions & 0 deletions src/foxops/routers/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import APIRouter, Depends

from foxops.dependencies import get_template_service
from foxops.services.template import TemplateService

router = APIRouter(prefix="/api/templates", tags=["template"])


@router.get("/variables")
async def get_template_variables(
template_repository: str,
template_version: str,
template_service: TemplateService = Depends(get_template_service),
):
return await template_service.get_template_variables(template_repository, template_version)
13 changes: 13 additions & 0 deletions src/foxops/services/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from foxops.engine.models.template_config import TemplateConfig
from foxops.hosters import Hoster


class TemplateService:
def __init__(self, hoster: Hoster) -> None:
self.hoster = hoster

async def get_template_variables(self, template_repository: str, template_version: str) -> dict[str, str]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some automated tests around this?

async with self.hoster.cloned_repository(template_repository, refspec=template_version) as repo:
template_config = TemplateConfig.from_path(repo.directory / "fengine.yaml")

return {k: v.get("default", "") for k, v in template_config.model_dump().get("variables", {}).items()}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the casting into a dict? We can have better type safety in the code by just doing

Suggested change
return {k: v.get("default", "") for k, v in template_config.model_dump().get("variables", {}).items()}
return {k: v.get("default", "") for k, v in template_config.variables.items()}

but then, also we have to be aware here that variables can be nested, complex objects. So this logic would have to be more recursive.

I would recommend adding functionality into the *VariableDefinition classes for getting an empty "example object" of that variable. This should then be easy to combine "up the tree" to ultimately get an "empty example" of a full template config.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just added a commit to this branch which added a mock_data() method on the TemplateConfig object - including a test for it :-) check it out!

3 changes: 2 additions & 1 deletion ui/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ const ErrorWrapper = styled.div({
display: 'flex',
alignItems: 'start',
justifyContent: 'right',
paddingRight: '1rem'
paddingRight: '1rem',
pointerEvents: 'none'
})

const LoadbarWrapper = styled.div({
Expand Down
8 changes: 5 additions & 3 deletions ui/src/components/common/JsonEditor/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useThemeModeStore } from 'stores/theme-mode'
import { InputError } from '../InputError/InputError'

export interface JsonEditorProps {
defaultValue?: string
value: string
height?: number | string
onChange?: (value: string) => void
invalid?: boolean
Expand All @@ -14,7 +14,7 @@ export interface JsonEditorProps {
}

export const JsonEditor = ({
defaultValue,
value,
height = 300,
onChange = () => {},
invalid,
Expand All @@ -30,7 +30,7 @@ export const JsonEditor = ({
setEditor(editor => {
if (editor) return editor
const _editor = monaco.editor.create(monacoEl.current!, {
value: defaultValue,
value,
language: 'json',
automaticLayout: true,
theme: mode === 'dark' ? 'vs-dark' : 'vs',
Expand All @@ -54,6 +54,8 @@ export const JsonEditor = ({
monaco.editor.setTheme(mode === 'dark' ? 'vs-dark' : 'vs')
}, [mode])

useEffect(() => editor?.setValue(value), [value])

return (
<>
<EditorWrapper ref={monacoEl} style={{ height }} invalid={invalid} />
Expand Down
63 changes: 41 additions & 22 deletions ui/src/routes/incarnations/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Tabs } from 'components/common/Tabs/Tabs'
import { useNavigate } from 'react-router-dom'
import { Dialog } from 'components/common/Dialog/Dialog'
import { useErrorStore } from 'stores/error'
import { template } from '../../services/template'

const DeleteIncarnationLink = styled.span`
cursor: pointer;
Expand Down Expand Up @@ -121,14 +122,7 @@ export const IncarnationsForm = ({
commitUrl,
templateDataFull
}: FormProps) => {
const {
register,
handleSubmit,
formState: { errors },
control,
watch,
getValues
} = useForm({
const { register, handleSubmit, formState: { errors }, control, watch, setValue, getValues } = useForm({
defaultValues
})

Expand All @@ -138,6 +132,8 @@ export const IncarnationsForm = ({
}

const templateRepo = watch('templateRepository')
const templateVersion = watch('templateVersion')

const failed = templateRepo === '' && isEdit
const navigate = useNavigate()

Expand All @@ -152,6 +148,18 @@ export const IncarnationsForm = ({
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [resetDialogOpen, setResetDialogOpen] = useState(false)

const [isLoadingTemplateData, setIsLoadingTemplateData] = useState(false)
const [fetchDialogOpen, setFetchDialogOpen] = useState(false)

const fetchTemplateData = async () => {
setFetchDialogOpen(false)
setIsLoadingTemplateData(true)

const data = await template.getDefaultVariables(templateRepo, templateVersion)
setValue('templateData', JSON.stringify(data, null, 2))
setIsLoadingTemplateData(false)
}

const onDelete = async () => {
setDeleteDialogOpen(false)
try {
Expand All @@ -175,7 +183,6 @@ export const IncarnationsForm = ({
const onSubmit: SubmitHandler<IncarnationInput> = async incarnation => {
errorStore.clearError()
try {
console.log(incarnation)
await mutateAsync(incarnation)
await delay(1000)
queryClient.invalidateQueries(['incarnations'])
Expand Down Expand Up @@ -219,15 +226,13 @@ export const IncarnationsForm = ({
render={({
field: { onChange, value },
fieldState: { error, invalid }
}) => (
<JsonEditor
defaultValue={value}
onChange={onChange}
invalid={invalid}
error={error?.message}
height="100%"
/>
)}
}) => <JsonEditor
value={value}
onChange={onChange}
invalid={invalid}
error={error?.message}
height="100%"
/>}
/>
)

Expand Down Expand Up @@ -315,6 +320,11 @@ export const IncarnationsForm = ({
/>
</Hug>
)}
{!isEdit && (
<Hug mb={16}>
<Button type="button" minWidth="14rem" onClick={() => setFetchDialogOpen(true)} loading={isLoadingTemplateData} disabled={!templateRepo || !templateVersion || isLoadingTemplateData}>Populate Template data</Button>
</Hug>
)}
</Hug>
<Hug w="calc(60% - 2rem)" h="100%">
{isEdit ? (
Expand All @@ -333,7 +343,7 @@ export const IncarnationsForm = ({
content: (
<Hug my={8} h="100%">
<JsonEditor
defaultValue={JSON.stringify(
value={JSON.stringify(
templateDataFull,
null,
2
Expand All @@ -349,9 +359,7 @@ export const IncarnationsForm = ({
) : (
<>
<strong>Template data JSON</strong>
<Hug my={16} h="100%">
{editTemplateDataController}
</Hug>
{editTemplateDataController}
</>
)}
</Hug>
Expand Down Expand Up @@ -478,6 +486,17 @@ export const IncarnationsForm = ({
</span>
<span>The merge request will not be automatically merged</span>
</Dialog>
<Dialog
open={fetchDialogOpen}
onAbort={() => setFetchDialogOpen(false)}
onConfirm={fetchTemplateData}
title="Fetch template data"
>
<span>Are you sure you want to fetch the template data?</span>
<span>
Doing this will overwrite the current template data. This action is <strong>not</strong> reversible.
</span>
</Dialog>
</Hug>
{failed ? (
failedFeedback
Expand Down
9 changes: 9 additions & 0 deletions ui/src/services/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { api } from './api'

export const template = {
getDefaultVariables: async (templateRepository: string, templateVersion: string) => {
const data = await api.get<undefined, Record<string, string>>(`/templates/variables?template_repository=${templateRepository}&template_version=${templateVersion}`)
return data
}
}