Skip to content

Commit 27bc023

Browse files
committed
Implement functionality to automatically populate template
1 parent 53bbe47 commit 27bc023

8 files changed

Lines changed: 202 additions & 13 deletions

File tree

src/foxops/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from foxops.logger import get_logger, setup_logging
99
from foxops.middlewares import request_id_middleware, request_time_middleware
1010
from foxops.openapi import custom_openapi
11-
from foxops.routers import auth, incarnations, not_found, version
11+
from foxops.routers import auth, incarnations, not_found, template, version
1212

1313
#: Holds the module logger instance
1414
logger = get_logger(__name__)
@@ -48,6 +48,7 @@ def create_app():
4848
# Add routes to the protected router (authentication required)
4949
protected_router = APIRouter(dependencies=[Depends(static_token_auth_scheme)])
5050
protected_router.include_router(incarnations.router)
51+
protected_router.include_router(template.router)
5152

5253
app.include_router(public_router)
5354
app.include_router(protected_router)

src/foxops/dependencies.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from foxops.logger import get_logger
1515
from foxops.services.change import ChangeService
1616
from foxops.services.incarnation import IncarnationService
17+
from foxops.services.template import TemplateService
1718
from foxops.settings import (
1819
DatabaseSettings,
1920
GitlabHosterSettings,
@@ -94,6 +95,12 @@ def get_incarnation_service(
9495
return IncarnationService(incarnation_repository=incarnation_repository, hoster=hoster)
9596

9697

98+
def get_template_service(
99+
hoster: Hoster = Depends(get_hoster),
100+
):
101+
return TemplateService(hoster=hoster)
102+
103+
97104
def get_change_service(
98105
hoster: Hoster = Depends(get_hoster),
99106
change_repository: ChangeRepository = Depends(get_change_repository),

src/foxops/routers/template.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from fastapi import APIRouter, Depends
2+
3+
from foxops.dependencies import get_template_service
4+
from foxops.services.template import TemplateService
5+
6+
router = APIRouter(prefix="/api/templates", tags=["template"])
7+
8+
9+
@router.get("/variables")
10+
async def get_template_variables(
11+
template_repository: str,
12+
template_version: str,
13+
template_service: TemplateService = Depends(get_template_service),
14+
):
15+
return await template_service.get_template_variables(template_repository, template_version)

src/foxops/services/template.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from foxops.engine.models.template_config import TemplateConfig
2+
from foxops.hosters import Hoster
3+
4+
5+
class TemplateService:
6+
def __init__(self, hoster: Hoster) -> None:
7+
self.hoster = hoster
8+
9+
async def get_template_variables(self, template_repository: str, template_version: str) -> dict[str, str]:
10+
async with self.hoster.cloned_repository(template_repository, refspec=template_version) as repo:
11+
template_config = TemplateConfig.from_path(repo.directory / "fengine.yaml")
12+
13+
return {k: v.get("default", "") for k, v in template_config.model_dump().get("variables", {}).items()}

ui/src/components/Layout/Layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const ErrorMessage = styled.div(({ theme }) => ({
2929
color: theme.colors.contrastText,
3030
float: 'right'
3131
},
32-
width: 'min(25rem, 80%)'
32+
width: 'min(25rem, 80%)',
3333
}))
3434

3535
const ErrorText = styled.div({
@@ -86,7 +86,8 @@ const ErrorWrapper = styled.div({
8686
display: 'flex',
8787
alignItems: 'start',
8888
justifyContent: 'right',
89-
paddingRight: '1rem'
89+
paddingRight: '1rem',
90+
pointerEvents: 'none'
9091
})
9192

9293
const LoadbarWrapper = styled.div({

ui/src/routes/incarnations/Form.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Tabs } from 'components/common/Tabs/Tabs'
2424
import { useNavigate } from 'react-router-dom'
2525
import { Dialog } from 'components/common/Dialog/Dialog'
2626
import { useErrorStore } from 'stores/error'
27+
import { TemplateDataPrefetcher } from './parts/TemplateDataPrefetcher'
2728

2829
const DeleteIncarnationLink = styled.span`
2930
cursor: pointer;
@@ -121,14 +122,7 @@ export const IncarnationsForm = ({
121122
commitUrl,
122123
templateDataFull
123124
}: FormProps) => {
124-
const {
125-
register,
126-
handleSubmit,
127-
formState: { errors },
128-
control,
129-
watch,
130-
getValues
131-
} = useForm({
125+
const { register, handleSubmit, formState: { errors }, control, watch, setValue, getValues } = useForm({
132126
defaultValues
133127
})
134128

@@ -138,6 +132,7 @@ export const IncarnationsForm = ({
138132
}
139133

140134
const templateRepo = watch('templateRepository')
135+
const templateVersion = watch('templateVersion')
141136
const failed = templateRepo === '' && isEdit
142137
const navigate = useNavigate()
143138

@@ -349,9 +344,10 @@ export const IncarnationsForm = ({
349344
) : (
350345
<>
351346
<strong>Template data JSON</strong>
352-
<Hug my={16} h="100%">
347+
348+
<TemplateDataPrefetcher templateVersion={templateVersion} templateRepository={templateRepo} onFetchSuccess={data => setValue('templateData', JSON.stringify(data, null, 2))}>
353349
{editTemplateDataController}
354-
</Hug>
350+
</TemplateDataPrefetcher>
355351
</>
356352
)}
357353
</Hug>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import styled from '@emotion/styled'
2+
import { Button } from 'components/common/Button/Button'
3+
import { Hug } from 'components/common/Hug/Hug'
4+
import { Tooltip } from 'components/common/Tooltip/Tooltip'
5+
import { useState } from 'react'
6+
import { template } from '../../../services/template'
7+
8+
type TemplateDataPrefetcherProps = {
9+
templateRepository: string,
10+
templateVersion: string,
11+
children: React.ReactNode,
12+
onFetchSuccess?: (data: Record<string, string>) => void
13+
};
14+
15+
const PreFetchWrapper = styled.div(({
16+
height: '100%',
17+
position: 'relative'
18+
}))
19+
20+
const PreFetchBoxWrapper = styled.div(({
21+
height: '100%',
22+
position: 'absolute',
23+
top: 0,
24+
left: 0,
25+
width: '100%',
26+
zIndex: 100,
27+
display: 'flex',
28+
justifyContent: 'center',
29+
alignItems: 'center',
30+
backgroundColor: 'rgba(165, 165, 165, 0.6)'
31+
}))
32+
33+
const PreFetchBox = styled.div(({
34+
backgroundColor: 'rgba(49, 49, 49, 0.6)',
35+
width: '80%',
36+
minHeight: '60%',
37+
maxHeight: '85%',
38+
padding: '1rem',
39+
borderRadius: '.8rem',
40+
display: 'flex',
41+
flexDirection: 'column',
42+
justifyContent: 'center',
43+
alignItems: 'center',
44+
color: 'white',
45+
boxShadow: '0 0 5px rgba(0, 0, 0, 0.2)'
46+
}))
47+
48+
const TemplateDataPrefetcherTitle = styled.h1(({
49+
textAlign: 'center'
50+
}))
51+
52+
const TemplateDataPrefetcherContent = styled.div(({
53+
padding: '.5rem',
54+
textAlign: 'center' }))
55+
56+
const TemplateDataPrefetcherActions = styled.div(({
57+
display: 'flex',
58+
justifyContent: 'center',
59+
padding: '.5rem'
60+
}))
61+
62+
const MagicButton = styled.button(({
63+
position: 'absolute',
64+
bottom: '.8rem',
65+
right: '.8rem',
66+
width: '2.5rem',
67+
height: '2.5rem',
68+
fontSize: '1.5rem',
69+
borderRadius: '50%',
70+
border: '1px solid rgb(137, 137, 137)',
71+
cursor: 'pointer',
72+
padding: 0,
73+
paddingBottom: '.2rem',
74+
paddingLeft: '.2rem',
75+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
76+
77+
'&:hover': {
78+
backgroundColor: 'rgba(255, 255, 255, 0.2)'
79+
}
80+
81+
}))
82+
83+
export const TemplateDataPrefetcher = ({ templateRepository, templateVersion, children, onFetchSuccess }: TemplateDataPrefetcherProps) => {
84+
const [isActivated, setIsActivated] = useState(false)
85+
const [isLoading, setIsLoading] = useState(false)
86+
87+
const fetchData = async () => {
88+
if (onFetchSuccess) {
89+
setIsLoading(true)
90+
const data = await template.getDefaultVariables(templateRepository, templateVersion)
91+
setIsLoading(false)
92+
setIsActivated(false)
93+
onFetchSuccess(data)
94+
95+
// TODO implement error handeling, if the fetch fails
96+
}
97+
}
98+
99+
const missingFields = []
100+
if (!templateRepository) {
101+
missingFields.push('Template repository')
102+
}
103+
if (!templateVersion) {
104+
missingFields.push('Template version')
105+
}
106+
107+
return isActivated ? (
108+
<PreFetchWrapper>
109+
<PreFetchBoxWrapper>
110+
<PreFetchBox>
111+
<TemplateDataPrefetcherTitle>Automatically populate the template data</TemplateDataPrefetcherTitle>
112+
<TemplateDataPrefetcherContent>
113+
You can automatically prefetch the data from the repository, which you have provided.
114+
</TemplateDataPrefetcherContent>
115+
<TemplateDataPrefetcherContent>
116+
If you would wish to do so, click the button &quot;Prefetch data&quot; below. This will populate the template data with the data found in the repository.
117+
</TemplateDataPrefetcherContent>
118+
<TemplateDataPrefetcherContent>
119+
<strong>Note: This will overwrite the current template data. And is not reversible.</strong>
120+
</TemplateDataPrefetcherContent>
121+
<TemplateDataPrefetcherActions style={{ height: '2rem' }}>
122+
{missingFields.length > 0 && (
123+
<span style={{ color: '#fc2121', margin: 0 }}>
124+
Please fill in the missing data: <strong>{missingFields.join(', ')}</strong>
125+
</span>
126+
)}
127+
</TemplateDataPrefetcherActions>
128+
129+
<TemplateDataPrefetcherActions>
130+
<Hug m=".5rem">
131+
<Tooltip title="Fetch data">
132+
<Button loading={isLoading} minWidth="9.5rem" disabled={(!templateRepository || !templateVersion) || isLoading} type="button" onClick={fetchData}>Prefetch data</Button>
133+
</Tooltip>
134+
</Hug><Hug m=".5rem">
135+
<Button variant="warning" minWidth="9.5rem" type="button" disabled={isLoading} onClick={() => setIsActivated(false)}>Continue Editing</Button>
136+
</Hug>
137+
</TemplateDataPrefetcherActions>
138+
</PreFetchBox>
139+
</PreFetchBoxWrapper>
140+
{children}
141+
</PreFetchWrapper>) : (
142+
<PreFetchWrapper>
143+
{children}
144+
<MagicButton type="button" onClick={() => setIsActivated(true)}>🪄</MagicButton>
145+
</PreFetchWrapper>
146+
)
147+
}

ui/src/services/template.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { api } from './api'
2+
3+
export const template = {
4+
getDefaultVariables: async (templateRepository: string, templateVersion: string) => {
5+
const data = await api.get<undefined, Record<string, string>>(`/templates/variables?template_repository=${templateRepository}&template_version=${templateVersion}`)
6+
return data
7+
}
8+
}
9+

0 commit comments

Comments
 (0)