Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VITE_API_URL=http://localhost:8080
Copy link

Choose a reason for hiding this comment

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

The API URL has been changed from port 8080 to 8090 and appended with /api. Ensure this change is intentional for the example environment and matched in server-side configurations.

Suggested change
VITE_API_URL=http://localhost:8080
VITE_API_URL=http://localhost:8090/api

VITE_API_URL=http://localhost:8090/api
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 35 additions & 12 deletions src/components/NewDeploymentModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -754,24 +754,38 @@
Password
<span v-if="form.database.mode === 'create'" class="required">*</span>
</label>
<input
id="dbPassword"
v-model="form.database.dbPassword"
type="password"
placeholder="••••••••"
/>
<div class="input-wrapper">
<input
id="dbPassword"
v-model="form.database.dbPassword"
:type="showDbPassword ? 'text' : 'password'"
placeholder="••••••••"
/>
<button type="button" class="input-icon-btn" @click="showDbPassword = !showDbPassword">
<i :class="showDbPassword ? 'pi pi-eye-slash' : 'pi pi-eye'" />
</button>
</div>
</div>
</div>

<Transition name="expand">
<div v-if="form.database.mode === 'create'" class="form-field">
<label for="dbRootPassword">Root Password</label>
<input
id="dbRootPassword"
v-model="form.database.dbRootPassword"
type="password"
placeholder="Leave empty to use same as password"
/>
<div class="input-wrapper">
<input
id="dbRootPassword"
v-model="form.database.dbRootPassword"
:type="showDbRootPassword ? 'text' : 'password'"
placeholder="Leave empty to use same as password"
/>
<button
type="button"
class="input-icon-btn"
@click="showDbRootPassword = !showDbRootPassword"
>
<i :class="showDbRootPassword ? 'pi pi-eye-slash' : 'pi pi-eye'" />
</button>
</div>
<span class="field-hint">Admin password for new database</span>
</div>
</Transition>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<RegistryCredential[]>([]);
const loadingCredentials = ref(false);

Expand Down Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions src/utils/yaml.ts
Original file line number Diff line number Diff line change
@@ -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);
Copy link

Choose a reason for hiding this comment

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

By default, the yaml package's parse function might only throw for critical syntax errors. For stricter validation (e.g., catching duplicate keys which are common in Compose files), it is better to check for warnings or use a stricter parsing configuration if available in the version installed.

Suggested change
parse(trimmed);
parse(trimmed, { uniqueKeys: true });

return { valid: true };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Invalid YAML syntax";
return { valid: false, error: message };
}
}
10 changes: 8 additions & 2 deletions src/views/DeploymentDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down