diff --git a/.env.example b/.env.example index 460e575..76a74b6 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_API_URL=http://localhost:8080 +VITE_API_URL=http://localhost:8090/api diff --git a/package-lock.json b/package-lock.json index 6bc5b9e..4a01fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "primevue": "^3.50.0", "vue": "^3.4.21", "vue-codemirror": "^6.1.1", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "yaml": "^2.8.2" }, "devDependencies": { "@pinia/testing": "^0.1.6", @@ -5391,6 +5392,21 @@ "node": ">=12" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 005ce5c..a757144 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "primevue": "^3.50.0", "vue": "^3.4.21", "vue-codemirror": "^6.1.1", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "yaml": "^2.8.2" }, "devDependencies": { "@pinia/testing": "^0.1.6", diff --git a/src/components/NewDeploymentModal.vue b/src/components/NewDeploymentModal.vue index f2dca77..76f0212 100644 --- a/src/components/NewDeploymentModal.vue +++ b/src/components/NewDeploymentModal.vue @@ -754,24 +754,38 @@ Password * - +
+ + +
- +
+ + +
Admin password for new database
@@ -1345,6 +1359,7 @@ import BaseModal from "@/components/base/BaseModal.vue"; import { deploymentsApi, templatesApi, settingsApi, containersApi, composeApi, credentialsApi } from "@/services/api"; import type { RegistryCredential } from "@/types"; import { useNotificationsStore } from "@/stores/notifications"; +import { validateComposeYaml } from "@/utils/yaml"; interface TemplateMount { id: string; @@ -1407,6 +1422,8 @@ const extensions = shallowRef([yaml(), oneDark]); const deploymentMode = ref<"" | "easy" | "compose" | "image">(""); const showRegistryPassword = ref(false); +const showDbPassword = ref(false); +const showDbRootPassword = ref(false); const existingCredentials = ref([]); const loadingCredentials = ref(false); @@ -2331,6 +2348,12 @@ const validate = () => { if (!form.composeContent.trim()) { errors.composeContent = "Compose configuration is required"; valid = false; + } else { + const yamlResult = validateComposeYaml(form.composeContent); + if (!yamlResult.valid) { + errors.composeContent = yamlResult.error; + valid = false; + } } return valid; diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts new file mode 100644 index 0000000..53a7bf0 --- /dev/null +++ b/src/utils/yaml.ts @@ -0,0 +1,21 @@ +import { parse } from "yaml"; + +export type ComposeYamlValidation = { valid: true } | { valid: false; error: string }; + +/** + * This function validates that a string is valid YAML. It is used before sending compose content + * to the API (e.g.during a deployment update or create). + */ +export function validateComposeYaml(content: string): ComposeYamlValidation { + const trimmed = content.trim(); + if (!trimmed) { + return { valid: false, error: "Compose content is empty" }; + } + try { + parse(trimmed); + return { valid: true }; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Invalid YAML syntax"; + return { valid: false, error: message }; + } +} diff --git a/src/views/DeploymentDetailView.vue b/src/views/DeploymentDetailView.vue index dbdd9c9..9d0bc7c 100644 --- a/src/views/DeploymentDetailView.vue +++ b/src/views/DeploymentDetailView.vue @@ -1391,8 +1391,9 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { Codemirror } from "vue-codemirror"; -import { yaml } from "@codemirror/lang-yaml"; +import { yaml as yamlLang } from "@codemirror/lang-yaml"; import { oneDark } from "@codemirror/theme-one-dark"; +import { validateComposeYaml } from "@/utils/yaml"; import { deploymentsApi, proxyApi, @@ -1526,7 +1527,7 @@ const composeFilename = ref("docker-compose.yml"); const isEditingConfig = ref(false); const serviceConfig = ref(""); const isEditingServiceConfig = ref(false); -const configExtensions = [yaml(), oneDark]; +const configExtensions = [yamlLang(), oneDark]; const activeConfigTab = ref<"compose" | "service">("compose"); const showOperationModal = ref(false); @@ -2331,6 +2332,11 @@ const cancelServiceConfigEdit = () => { }; const saveConfig = async () => { + const result = validateComposeYaml(composeConfig.value); + if (!result.valid) { + notifications.error("Invalid YAML", result.error); + return; + } try { await deploymentsApi.update(route.params.name as string, { compose_content: composeConfig.value,