frontend/src/components/RegistryWizard/steps/RegistryResources.vue (758 lines of code) (raw):
<script lang="ts">
import { defineComponent, type PropType, ref } from 'vue';
import * as Yup from 'yup';
import { useForm, useField } from 'vee-validate';
import TextField from '@/components/common/TextField.vue';
import Typography from '@/components/common/Typography.vue';
import IconButton from '@/components/common/IconButton.vue';
import ToggleSwitch from '@/components/common/ToggleSwitch.vue';
import Banner from '@/components/common/Banner.vue';
import { jsonDiff } from '@/utils/registry';
import type { RegistryResource } from '@/types/registry';
import { REGISTRY_COMPONENTS } from '@/types/registry';
import { cloneDeep } from 'lodash';
interface WizardTemplateVariables {
registryValues: any;
defaultRegistryValues: any;
}
export default defineComponent({
components: { TextField, Typography, IconButton, ToggleSwitch, Banner },
setup() {
const registryResources = ref({
encoded: '',
category: '' as any,
categories: [] as Array<RegistryResource>,
listOfCategoryNames: Object.keys(REGISTRY_COMPONENTS),
});
const crunchyPostgres = ref({
maxConnections: '',
storageSize: '',
});
const diffRegistryResourcesAndDefaultResources = ref<string[]>([]);
const resourcesWithoutHpa = [
REGISTRY_COMPONENTS.geoServer,
REGISTRY_COMPONENTS.redis,
REGISTRY_COMPONENTS.sentinel,
];
const defaultEmptyResource = {
config: {
istio: {
sidecar: {
enabled: true,
resources: {
requests: {
cpu: '',
memory: '',
},
limits: {
cpu: '',
memory: '',
},
},
},
},
container: {
resources: {
requests: {
cpu: '',
memory: '',
},
limits: {
cpu: '',
memory: '',
},
},
envVars: [{ name: '', value: '' }],
},
},
};
const commonValidationSchema = Yup.object().shape({
container: Yup.object().shape({
envVars: Yup.array().of(
Yup.object().shape({
name: Yup.string().test((value, context) => {
return !(!value && context.parent.value);
}),
value: Yup.string().test((value, context) => {
return !(!value && context.parent.name);
}),
})
),
}),
});
const validationSchemaWithHpa = Yup.object().shape({
hpa: Yup.object().shape({
enabled: Yup.bool(),
minReplicas: Yup.number().when('enabled', {
is: true,
then: (schema) => schema.required().min(1).integer(),
}),
maxReplicas: Yup.number().when('enabled', {
is: true,
then: (schema) =>
schema.required().min(1).integer().moreThan(Yup.ref('minReplicas')),
}),
}),
replicas: Yup.number().when('hpa.enabled', {
is: false,
then: (schema) => schema.required().min(1).integer(),
}),
});
const validationSchema = Yup.object({
registryResourcesForm: Yup.array().of(
Yup.object().shape({
name: Yup.string(),
config: Yup.object().when('name', {
is: (name: string) => {
return (
name === REGISTRY_COMPONENTS.kafkaApi ||
name === REGISTRY_COMPONENTS.restApi
);
},
then: (schema) => {
return schema
.shape({
datasource: Yup.object().shape({
maxPoolSize: Yup.number().required().min(1).integer(),
}),
})
.concat(validationSchemaWithHpa)
.concat(commonValidationSchema);
},
otherwise: (schema) => {
return schema.when('name', {
is: (name: REGISTRY_COMPONENTS) => {
return resourcesWithoutHpa.includes(name);
},
then: (schema) =>
schema
.shape({
replicas: Yup.number().required().min(1).integer(),
})
.concat(commonValidationSchema),
otherwise: (schema) =>
schema
.concat(validationSchemaWithHpa)
.concat(commonValidationSchema),
});
},
}),
})
),
});
const { errors, validate } = useForm({
validationSchema,
});
const { value: registryResourcesForm } = useField<RegistryResource[]>(
'registryResourcesForm'
);
registryResourcesForm.value = [];
function validator() {
return new Promise((resolve) => {
validate().then((res) => {
if (res.valid) {
resolve(true);
}
});
});
}
return {
errors,
validator,
registryResources,
crunchyPostgres,
diffRegistryResourcesAndDefaultResources,
registryResourcesForm,
resourcesWithoutHpa,
defaultEmptyResource,
REGISTRY_COMPONENTS,
};
},
props: {
templatePreloadedData: Object,
formSubmitted: Boolean,
isEditAction: Boolean,
templateVariables: {
required: true,
type: Object as PropType<WizardTemplateVariables>,
},
},
watch: {
templatePreloadedData(data: any) {
this.preloadRegistryResources(data);
},
formSubmitted() {
this.encodeRegistryResources();
},
},
methods: {
decodeResourcesEnvVars(inEnvVars: Record<string, unknown>) {
const envVars = [];
for (const j in inEnvVars) {
envVars.push({
name: j,
value: inEnvVars[j],
});
}
return envVars;
},
addEnvVar(envVars: Array<Record<string, unknown>>, event: any) {
event.preventDefault();
envVars.push({ name: '', value: '' });
},
removeEnvVar(
envVars: Array<Record<string, unknown>>,
env: Record<string, unknown>
) {
envVars.splice(envVars.indexOf(env), 1);
},
removeResource(cat: RegistryResource, event: any) {
event.preventDefault();
this.registryResources.listOfCategoryNames.unshift(cat.name);
this.registryResources.categories.splice(
this.registryResources.categories.indexOf(cat),
1
);
this.registryResourcesForm.splice(
this.registryResourcesForm.indexOf(cat),
1
);
},
addResource() {
const category = this.registryResources.categories.find(
(c) => c.name === this.registryResources.category
);
const indexListOfCategoryNames =
this.registryResources.listOfCategoryNames.indexOf(
this.registryResources.category
);
if (!category) {
const emptyResource = {
name: this.registryResources.category,
config: {
...cloneDeep(this.defaultEmptyResource.config),
...(!this.resourcesWithoutHpa.includes(
this.registryResources.category
) && {
hpa: {
enabled: false,
maxReplicas: 3,
minReplicas: 1,
},
}),
...((this.registryResources.category ===
REGISTRY_COMPONENTS.kafkaApi ||
this.registryResources.category ===
REGISTRY_COMPONENTS.restApi) && {
datasource: {
maxPoolSize: 10,
},
}),
replicas: 1,
},
};
this.registryResources.categories.unshift(emptyResource);
this.registryResourcesForm.push(emptyResource);
this.registryResources.listOfCategoryNames.splice(
indexListOfCategoryNames,
1
);
this.registryResources.category = '';
return;
}
this.registryResourcesForm.push(category);
this.registryResources.listOfCategoryNames.splice(
indexListOfCategoryNames,
1
);
this.registryResources.category = '';
},
encodeRegistryResources() {
const prepare = {} as Record<string, unknown>;
this.registryResources.categories.forEach((el: any) => {
const cloneEL = JSON.parse(JSON.stringify(el));
const envVars = {} as Record<string, unknown>;
cloneEL.config.container.envVars.forEach(function (el: any) {
envVars[el.name] = el.value;
});
cloneEL.config.container.envVars = envVars;
prepare[cloneEL.name] = { ...cloneEL.config };
});
this.cleanEmptyProperties(prepare);
this.registryResources.encoded = JSON.stringify(prepare);
},
cleanEmptyProperties(obj: Record<string, unknown>) {
if (this.isObject(obj)) {
for (const key in obj) {
if (this.isObject(obj[key])) {
this.cleanEmptyProperties(obj[key] as Record<string, unknown>);
if (Object.keys(obj[key] as Record<string, unknown>).length === 0) {
delete obj[key];
}
} else if (obj[key] === '') {
delete obj[key];
}
}
}
},
mergeResource(data: Record<string, unknown>) {
const emptyResource = {...cloneDeep(this.defaultEmptyResource.config)};
this.mergeDeep(emptyResource, data);
return emptyResource;
},
preloadRegistryResources(values: any) {
const crunchyPostgres = values?.global?.crunchyPostgres;
if (crunchyPostgres) {
this.crunchyPostgres.maxConnections =
crunchyPostgres.postgresql?.parameters?.max_connections;
this.crunchyPostgres.storageSize = crunchyPostgres.storageSize;
}
const data = values?.global?.registry;
if (!data) {
return;
}
for (const i in data) {
if (
'container' in data[i] &&
this.isObject(data[i].container) &&
'envVars' in data[i].container
) {
data[i].container.envVars = this.decodeResourcesEnvVars(
data[i].container.envVars
);
}
const mergedData = this.mergeResource(data[i]);
this.registryResources.categories.push({
name: i as any,
config: mergedData,
});
}
if (
this.diffRegistryResourcesAndDefaultResources.length &&
this.isEditAction
) {
this.registryResources.categories.forEach((cat) => {
if (this.diffRegistryResourcesAndDefaultResources.includes(cat.name)) {
this.registryResourcesForm.push(cat);
}
});
this.registryResources.listOfCategoryNames =
this.registryResources.listOfCategoryNames.filter(
(cat) =>
!this.diffRegistryResourcesAndDefaultResources.includes(cat)
);
}
},
isObject(item: unknown) {
return item && typeof item === 'object' && !Array.isArray(item);
},
mergeDeep(target: any, ...sources: any[]): any {
if (!sources.length) return target;
const source = sources.shift();
if (this.isObject(target) && this.isObject(source)) {
for (const key in source) {
if (source[key] === null) {
continue;
}
if (this.isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
this.mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return this.mergeDeep(target, ...sources);
},
preloadDiffResult() {
if (this.isEditAction) {
const data = this.changeMaxPoolSizeToNumber(this.templateVariables.registryValues.global?.registry);
const baseData = this.templateVariables.defaultRegistryValues?.global?.registry;
this.cleanEmptyProperties(data);
this.cleanEmptyProperties(baseData);
this.diffRegistryResourcesAndDefaultResources = jsonDiff(
data,
baseData
);
}
},
showBanner(categoryName: string) {
return (
this.isEditAction &&
(categoryName === REGISTRY_COMPONENTS.restApi ||
categoryName === REGISTRY_COMPONENTS.kafkaApi ||
categoryName === REGISTRY_COMPONENTS.soapApi)
);
},
changeMaxPoolSizeToNumber(categories: Record<string, any>) {
for (const i in categories) {
if (categories[i]?.datasource?.maxPoolSize) {
categories[i].datasource.maxPoolSize = parseInt(categories[i].datasource.maxPoolSize);
}
}
return categories;
}
},
mounted() {
this.preloadDiffResult();
this.preloadRegistryResources(this.templateVariables.registryValues);
},
});
</script>
<template>
<Typography variant="h3" class="h3">Ресурси реєстру</Typography>
<Typography variant="bodyText">
Ви можете додати окремі компоненти до реєстру.
</Typography>
<input type="hidden" name="resources" :value="registryResources.encoded" />
<div class="registry-resources">
<div class="rc-form-group crunchy-postgres">
<Typography variant="h3" class="mb24">Crunchy Postgres</Typography>
<TextField
label="Max Connections"
name="crunchy-postgres-max-connections"
v-model="crunchyPostgres.maxConnections"
/>
<TextField
label="Storage Size"
name="crunchy-postgres-storage-size"
v-model="crunchyPostgres.storageSize"
/>
</div>
<hr class="divider" />
<div
class="cat-line"
v-for="(cat, idx) in registryResourcesForm"
v-bind:key="cat.name"
>
<Typography variant="h3" class="category-name">
{{ cat.name }}
<IconButton @onClick="removeResource(cat, $event)">
<img src="@/assets/svg/trash.svg" />
</IconButton>
</Typography>
<Banner
v-if="showBanner(cat.name)"
classes="mb32"
description="Якщо потрібно одразу застосувати внесені зміни, необхідно викликати оновлення дата-моделі."
/>
<template
v-if="
!resourcesWithoutHpa.includes(cat.name) &&
typeof cat.config?.hpa?.enabled === 'boolean'
"
>
<Typography variant="h5" class="upperText">
HPA (Автоматичне горизонтальне масштабування)
</Typography>
<ToggleSwitch
:name="`cat[${idx}].config.hpa.enabled`"
label="Enable HPA (Автоматичне горизонтальне масштабування)"
v-model="cat.config.hpa.enabled"
classes="mt24"
/>
<div class="rc-form-group mt24" v-if="!cat.config?.hpa?.enabled">
<TextField
required
type="number"
label="Replicas Amount"
:name="`cat[${idx}].config.replicas`"
v-model="cat.config.replicas"
:error="errors[`registryResourcesForm[${idx}].config.replicas`]"
/>
</div>
<div
class="rc-form-group mt24 rc-form-group-horz"
v-if="cat.config?.hpa?.enabled"
>
<TextField
required
type="number"
label="Min Replicas"
:name="`cat[${idx}].config.hpa.minReplicas`"
v-model="cat.config.hpa.minReplicas"
:error="
errors[`registryResourcesForm[${idx}].config.hpa.minReplicas`]
"
rootClass="mb0"
/>
<div class="separator">–</div>
<TextField
required
type="number"
label="Max Replicas"
:name="`cat[${idx}].config.hpa.maxReplicas`"
v-model="cat.config.hpa.maxReplicas"
:error="
errors[`registryResourcesForm[${idx}].config.hpa.maxReplicas`]
"
rootClass="mb0"
/>
</div>
</template>
<template v-else>
<TextField
v-if="cat.name !== REGISTRY_COMPONENTS.geoServer"
required
type="number"
label="Replicas Amount"
:name="`cat[${idx}].config.replicas`"
v-model="cat.config.replicas"
:error="errors[`registryResourcesForm[${idx}].config.replicas`]"
/>
</template>
<div class="rc-form-group mt32">
<Typography variant="h5" class="upperText">Container limits</Typography>
<Typography variant="small" class="mt16">
Вказуйте значення та розмірність. Наприклад, “100m” для CPU
(millicores) та “400Mi” для RAM (mebibytes).
</Typography>
<div class="rc-form-group mt24 rc-form-group-horz">
<TextField
label="CPU requests"
:name="`cat[${idx}].config.container.resources.requests.cpu`"
v-model="cat.config.container.resources.requests.cpu"
rootClass="mb0"
/>
<div class="separator">&</div>
<TextField
label="Memory requests"
:name="`cat[${idx}].config.container.resources.requests.memory`"
v-model="cat.config.container.resources.requests.memory"
rootClass="mb0"
/>
</div>
<div class="rc-form-group rc-form-group-horz">
<TextField
label="CPU limits"
:name="`cat[${idx}].config.container.resources.limits.cpu`"
v-model="cat.config.container.resources.limits.cpu"
rootClass="mb0"
/>
<div class="separator">&</div>
<TextField
label="Memory limits"
:name="`cat[${idx}].config.container.resources.limits.memory`"
v-model="cat.config.container.resources.limits.memory"
rootClass="mb0"
/>
</div>
</div>
<div class="rc-form-group mt32" v-if="cat.name !== REGISTRY_COMPONENTS.redis">
<Typography variant="h5" class="upperText">Istio sidecar</Typography>
<ToggleSwitch
:name="`cat[${idx}].config.istio.sidecar.enabled`"
label="Enable Istio sidecar"
v-model="cat.config.istio.sidecar.enabled"
id="istio-sidecar-enabled"
classes="mt24"
/>
<template v-if="cat.config.istio.sidecar.enabled">
<div class="rc-form-group rc-form-group-horz mt24">
<TextField
label="CPU requests"
:name="`cat[${idx}].config.istio.sidecar.resources.requests.cpu`"
v-model="cat.config.istio.sidecar.resources.requests.cpu"
rootClass="mb0"
/>
<div class="separator">&</div>
<TextField
label="Memory requests"
:name="`cat[${idx}].config.istio.sidecar.resources.requests.memory`"
v-model="cat.config.istio.sidecar.resources.requests.memory"
rootClass="mb0"
/>
</div>
<div class="rc-form-group rc-form-group-horz">
<TextField
label="CPU limits"
:name="`cat[${idx}].config.istio.sidecar.resources.limits.cpu`"
v-model="cat.config.istio.sidecar.resources.limits.cpu"
rootClass="mb0"
/>
<div class="separator">&</div>
<TextField
label="Memory limits"
:name="`cat[${idx}].config.istio.sidecar.resources.limits.memory`"
v-model="cat.config.istio.sidecar.resources.limits.memory"
rootClass="mb0"
/>
</div>
</template>
</div>
<div class="rc-form-group mt32">
<Typography variant="h5" class="upperText">Змінні оточення</Typography>
<div class="rc-form-group rc-form-group-horz mb0 mt24">
<Typography variant="bodyText" class="env-name">Name</Typography>
<Typography variant="bodyText" class="env-value">Value</Typography>
</div>
<div
class="rc-form-group rc-form-group-horz mb8"
v-for="(env, index) in cat.config.container.envVars"
v-bind:key="index"
>
<TextField
:name="`env[${index}].name`"
v-model="env.name"
rootClass="mb0"
:error="
errors[
`registryResourcesForm[${idx}].config.container.envVars[${index}].name`
]
"
/>
<TextField
:name="`env[${index}].value`"
v-model="env.value"
rootClass="mb0"
:error="
errors[
`registryResourcesForm[${idx}].config.container.envVars[${index}].value`
]
"
/>
<IconButton
@click="removeEnvVar(cat.config.container.envVars, env)"
class="mt8"
>
<img src="@/assets/svg/close.svg" />
</IconButton>
</div>
<div class="env-vars">
<a
@click="addEnvVar(cat.config.container.envVars, $event)"
href="#"
class="env-add-lnk"
>
<Typography variant="small">+ Додати змінну оточення</Typography>
</a>
</div>
</div>
<div
v-if="
(cat.name === REGISTRY_COMPONENTS.restApi || cat.name === REGISTRY_COMPONENTS.kafkaApi) &&
typeof cat.config?.datasource?.maxPoolSize === 'number'
"
class="rc-form-group mb24"
>
<Typography variant="h5" class="upperText mb24">
Database connection parameters
</Typography>
<TextField
type="number"
label="Maximum pool size"
description="Допустиме значення параметру > 0"
:name="`cat[${idx}].config.datasource.maxPoolSize`"
v-model="cat.config.datasource.maxPoolSize"
:error="
errors[
`registryResourcesForm[${idx}].config.datasource.maxPoolSize`
]
"
/>
</div>
<hr class="divider" />
</div>
<div class="rc-form-group res-cat-select">
<select v-model="registryResources.category">
<option disabled selected value="">Оберіть компонент</option>
<option
v-for="name in registryResources.listOfCategoryNames"
v-bind:key="name"
>
{{ name }}
</option>
</select>
<button
@click.prevent="addResource"
:class="['btn', { 'btn-active': registryResources.category }]"
>
Додати
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.h3 {
margin-bottom: 16px;
}
.divider {
margin-top: 32px;
margin-bottom: 32px;
border-color: $grey3;
}
.btn {
width: auto;
padding: 8px 16px;
height: auto;
margin-left: 16px;
background-color: $grey-border-color;
pointer-events: none;
cursor: none;
}
.btn-active {
background-color: $success-color;
pointer-events: all;
}
.category-name {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.upperText {
text-transform: uppercase;
}
.crunchy-postgres {
margin-top: 16px;
}
.mt8 {
margin-top: 8px;
}
.mb8 {
margin-bottom: 8px;
}
.mb24 {
margin-bottom: 24px;
}
.mt16 {
margin-top: 16px;
}
.mt24 {
margin-top: 24px;
}
.mt32 {
margin-top: 32px;
}
.mb0 {
margin-bottom: 0;
}
.mb32 {
margin-bottom: 32px;
}
.rc-form-group-horz {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: stretch;
}
.separator {
padding-top: 40px;
}
.env-add-lnk p {
color: $blue-main;
padding-left: 8px;
}
.env-add-lnk:hover {
color: $blue-main;
}
.env-name {
flex: 1;
font-weight: 700;
}
.env-value {
flex: 1;
margin-left: -40px;
font-weight: 700;
}
</style>