Compare commits

..

3 Commits

Author SHA1 Message Date
cmcooper1980
f16b00908d Refactor URL and agent list rendering in Compose.vue 2026-04-13 18:50:27 -05:00
cmcooper1980
3b9f0b9a4f Remove commented-out code and clean imports 2026-04-12 04:22:28 -05:00
cmcooper1980
ca9c8b4ba1 Implement variable highlighting in Compose.vue
Added a variable highlighting feature to the editor using CodeMirror.
2026-04-12 03:02:44 -05:00
14 changed files with 396 additions and 424 deletions

View File

@@ -21,30 +21,24 @@
<font-awesome-icon icon="terminal" /> <font-awesome-icon icon="terminal" />
Bash Bash
</router-link> </router-link>
<button <button v-if="this.serviceCount > 1 && !isEditMode && status !== 'running' && status !== 'healthy'"
v-if="serviceCount > 1 && !isEditMode && status !== 'running' && status !== 'healthy'"
class="btn btn-primary" class="btn btn-primary"
:disabled="processing" :disabled="processing"
@click="startService" @click="startService">
>
<font-awesome-icon icon="play" class="me-1" /> <font-awesome-icon icon="play" class="me-1" />
{{ $t("startStack") }} {{ $t("startStack") }}
</button> </button>
<button <button v-if="this.serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
v-if="serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
class="btn btn-normal" class="btn btn-normal"
:disabled="processing" :disabled="processing"
@click="restartService" @click="restartService">
>
<font-awesome-icon icon="rotate" class="me-1" /> <font-awesome-icon icon="rotate" class="me-1" />
{{ $t("restartStack") }} {{ $t("restartStack") }}
</button> </button>
<button <button v-if="this.serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
v-if="serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
class="btn btn-normal" class="btn btn-normal"
:disabled="processing" :disabled="processing"
@click="stopService" @click="stopService">
>
<font-awesome-icon icon="stop" class="me-1" /> <font-awesome-icon icon="stop" class="me-1" />
{{ $t("stopStack") }} {{ $t("stopStack") }}
</button> </button>

View File

@@ -3,10 +3,8 @@
<div class="list-header"> <div class="list-header">
<div class="header-top"> <div class="header-top">
<!-- TODO --> <!-- TODO -->
<button <button v-if="false" class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button"
v-if="false" class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
@click="selectMode = !selectMode"
>
{{ $t("Select") }} {{ $t("Select") }}
</button> </button>
@@ -33,14 +31,10 @@
<div v-if="selectMode && false" class="selection-controls px-2 pt-2"> <div v-if="selectMode && false" class="selection-controls px-2 pt-2">
<input v-model="selectAll" class="form-check-input select-input" type="checkbox" /> <input v-model="selectAll" class="form-check-input select-input" type="checkbox" />
<button class="btn-outline-normal" @click="pauseDialog"> <button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{
<font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
$t("Pause") }} <button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" />
</button> {{ $t("Resume") }}</button>
<button class="btn-outline-normal" @click="resumeSelected">
<font-awesome-icon icon="play" size="sm" />
{{ $t("Resume") }}
</button>
<span v-if="selectedStackCount > 0"> <span v-if="selectedStackCount > 0">
{{ $t("selectedStackCount", [selectedStackCount]) }} {{ $t("selectedStackCount", [selectedStackCount]) }}
@@ -51,11 +45,9 @@
<div v-if="agentStackList[0] && agentStackList[0].stacks.length === 0" class="text-center mt-3"> <div v-if="agentStackList[0] && agentStackList[0].stacks.length === 0" class="text-center mt-3">
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link> <router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
</div> </div>
<div v-for="(agent, agentIndex) in agentStackList" :key="agentIndex" class="stack-list-inner"> <div class="stack-list-inner" v-for="(agent, index) in agentStackList" :key="index">
<div <div v-if="$root.agentCount > 1" class="p-2 agent-select"
v-if="$root.agentCount > 1" class="p-2 agent-select" @click="closedAgents.set(agent.endpoint, !closedAgents.get(agent.endpoint))">
@click="closedAgents.set(agent.endpoint, !closedAgents.get(agent.endpoint))"
>
<span class="me-1"> <span class="me-1">
<font-awesome-icon v-show="closedAgents.get(agent.endpoint)" icon="chevron-circle-right" /> <font-awesome-icon v-show="closedAgents.get(agent.endpoint)" icon="chevron-circle-right" />
<font-awesome-icon v-show="!closedAgents.get(agent.endpoint)" icon="chevron-circle-down" /> <font-awesome-icon v-show="!closedAgents.get(agent.endpoint)" icon="chevron-circle-down" />
@@ -63,11 +55,9 @@
<span v-if="agent.endpoint === 'current'">{{ $t("currentEndpoint") }}</span> <span v-if="agent.endpoint === 'current'">{{ $t("currentEndpoint") }}</span>
<span v-else>{{ agent.endpoint }}</span> <span v-else>{{ agent.endpoint }}</span>
</div> </div>
<StackListItem <StackListItem v-show="$root.agentCount === 1 || !closedAgents.get(agent.endpoint)"
v-for="(item, index) in agent.stacks" v-for="(item, index) in agent.stacks" :key="index" :stack="item" :isSelectMode="selectMode"
v-show="$root.agentCount === 1 || !closedAgents.get(agent.endpoint)" :key="index" :stack="item" :isSelectMode="selectMode" :isSelected="isSelected" :select="select" :deselect="deselect" />
:isSelected="isSelected" :select="select" :deselect="deselect"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -205,7 +195,7 @@ export default {
// and the rest are sorted alphabetically // and the rest are sorted alphabetically
result = [ result = [
...result.reduce((acc, stack) => { ...result.reduce((acc, stack) => {
const endpoint = stack.endpoint || "current"; const endpoint = stack.endpoint || 'current';
if (!acc.has(endpoint)) { if (!acc.has(endpoint)) {
acc.set(endpoint, []); acc.set(endpoint, []);
} }
@@ -216,9 +206,9 @@ export default {
endpoint, endpoint,
stacks stacks
})).sort((a, b) => { })).sort((a, b) => {
if (a.endpoint === "current" && b.endpoint !== "current") { if (a.endpoint === 'current' && b.endpoint !== 'current') {
return -1; return -1;
} else if (a.endpoint !== "current" && b.endpoint === "current") { } else if (a.endpoint !== 'current' && b.endpoint === 'current') {
return 1; return 1;
} }
return a.endpoint.localeCompare(b.endpoint); return a.endpoint.localeCompare(b.endpoint);

View File

@@ -20,24 +20,22 @@ export default {
props: { props: {
name: { name: {
type: String, type: String,
required: true, require: true,
}, },
endpoint: { endpoint: {
type: String, type: String,
required: true, require: true,
}, },
// Require if mode is interactive // Require if mode is interactive
stackName: { stackName: {
type: String, type: String,
default: "",
}, },
// Require if mode is interactive // Require if mode is interactive
serviceName: { serviceName: {
type: String, type: String,
default: "",
}, },
// Require if mode is interactive // Require if mode is interactive
@@ -104,7 +102,7 @@ export default {
this.terminal.focus(); this.terminal.focus();
// Add right-click context menu handler for paste // Add right-click context menu handler for paste
this.$refs.terminal.addEventListener("contextmenu", this.handleContextMenu); this.$refs.terminal.addEventListener('contextmenu', this.handleContextMenu);
// Add selection handler for copy to clipboard // Add selection handler for copy to clipboard
this.terminal.onSelectionChange(() => { this.terminal.onSelectionChange(() => {
@@ -145,7 +143,7 @@ export default {
window.removeEventListener("resize", this.onResizeEvent); // Remove the resize event listener from the window object. window.removeEventListener("resize", this.onResizeEvent); // Remove the resize event listener from the window object.
this.$root.unbindTerminal(this.name); this.$root.unbindTerminal(this.name);
this.terminal.dispose(); this.terminal.dispose();
this.$refs.terminal?.removeEventListener("contextmenu", this.handleContextMenu); this.$refs.terminal?.removeEventListener('contextmenu', this.handleContextMenu);
}, },
methods: { methods: {

View File

@@ -57,8 +57,7 @@ export default {
EditorView.focusChangeEffect.of(focusEffectHandler), EditorView.focusChangeEffect.of(focusEffectHandler),
]; ];
return { editorFocus, return { editorFocus, extensionsEnv };
extensionsEnv };
}, },
computed: { computed: {

View File

@@ -94,18 +94,10 @@
<TwoFADialog ref="TwoFADialog" /> <TwoFADialog ref="TwoFADialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth"> <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<i18n-t keypath="disableauth.message1" tag="p"> <!-- eslint-disable-next-line vue/no-v-html -->
<template #disableAuth> <p v-html="$t('disableauth.message1')"></p>
<strong>{{ $t('disableAuth') }}</strong> <!-- eslint-disable-next-line vue/no-v-html -->
</template> <p v-html="$t('disableauth.message2')"></p>
</i18n-t>
<i18n-t keypath="disableauth.message2" tag="p">
<template #scenarios>
<strong>{{ $t('scenarios') }}</strong>
</template>
</i18n-t>
<p>{{ $t("Please use this option carefully!") }}</p> <p>{{ $t("Please use this option carefully!") }}</p>
<div class="mb-3"> <div class="mb-3">

View File

@@ -47,10 +47,8 @@
"deleteContainer": "Delete", "deleteContainer": "Delete",
"addContainer": "Add Container", "addContainer": "Add Container",
"addNetwork": "Add Network", "addNetwork": "Add Network",
"disableauth.message1": "Are you sure want to {disableAuth}?", "disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
"disableauth.message2": "It is designed for scenarios {scenarios}", "disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Dockge such as Cloudflare Access, Authelia or other authentication mechanisms.",
"disableAuth": "disable authentication",
"scenarios": "where you intend to implement third-party authentication",
"passwordNotMatchMsg": "The repeat password does not match.", "passwordNotMatchMsg": "The repeat password does not match.",
"autoGet": "Auto Get", "autoGet": "Auto Get",
"add": "Add", "add": "Add",
@@ -141,12 +139,8 @@
"networkIO": "Network I/O", "networkIO": "Network I/O",
"blockIO": "Block I/O", "blockIO": "Block I/O",
"Console is not enabled": "Console is not enabled", "Console is not enabled": "Console is not enabled",
"ConsoleNotEnabledMSG1": "Console is a powerful tool that allows you to execute any commands such as {docker}, {rm} within the Dockge's container in this Web UI.", "ConsoleNotEnabledMSG1": "Console is a powerful tool that allows you to execute any commands such as <code>docker</code>, <code>rm</code> within the Dockge's container in this Web UI.",
"ConsoleNotEnabledMSG2": "It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like {rmRf}", "ConsoleNotEnabledMSG2": "It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like <code>rm -rf</code>" ,
"ConsoleNotEnabledMSG3": "If you understand the risk, you can enable it by setting {envVar} in the environment variables.", "ConsoleNotEnabledMSG3": "If you understand the risk, you can enable it by setting <code>DOCKGE_ENABLE_CONSOLE=true</code> in the environment variables.",
"dockerCode": "docker",
"rmCode": "rm",
"rmRfCode": "rm -rf",
"envVarCode": "DOCKGE_ENABLE_CONSOLE=true",
"confirmLeaveStack": "You are currently editing a stack. Are you sure you want to leave?" "confirmLeaveStack": "You are currently editing a stack. Are you sure you want to leave?"
} }

View File

@@ -221,15 +221,6 @@
<NetworkInput /> <NetworkInput />
</div> </div>
</div> </div>
<!-- <div class="shadow-box big-padding mb-3">
<div class="mb-3">
<label for="name" class="form-label"> Search Templates</label>
<input id="name" v-model="name" type="text" class="form-control" placeholder="Search..." required>
</div>
<prism-editor v-if="false" v-model="yamlConfig" class="yaml-editor" :highlight="highlighter" line-numbers @input="yamlCodeChange"></prism-editor>
</div>-->
</div> </div>
</div> </div>
@@ -250,9 +241,9 @@ import CodeMirror from "vue-codemirror6";
import { yaml } from "@codemirror/lang-yaml"; import { yaml } from "@codemirror/lang-yaml";
import { python } from "@codemirror/lang-python"; import { python } from "@codemirror/lang-python";
import { dracula as editorTheme } from "thememirror"; import { dracula as editorTheme } from "thememirror";
import { lineNumbers, EditorView } from "@codemirror/view"; import { lineNumbers, EditorView, Decoration, ViewPlugin } from "@codemirror/view";
import { parseDocument, Document } from "yaml"; import { parseDocument, Document } from "yaml";
import { RangeSetBuilder } from "@codemirror/state";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { import {
COMBINED_TERMINAL_COLS, COMBINED_TERMINAL_COLS,
@@ -283,6 +274,43 @@ let yamlErrorTimeout = null;
let serviceStatusTimeout = null; let serviceStatusTimeout = null;
let dockerStatsTimeout = null; let dockerStatsTimeout = null;
// Highlight $VAR and ${VAR}
const variableHighlight = ViewPlugin.fromClass(class {
constructor(view) {
this.decorations = this.buildDecorations(view);
}
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view) {
const builder = new RangeSetBuilder();
for (const { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
const variableRegex = /\$\{?[A-Za-z0-9_]+\}?/g;
let match;
while ((match = variableRegex.exec(text)) !== null) {
const start = from + match.index;
const end = start + match[0].length;
builder.add(
start,
end,
Decoration.mark({ class: "cm-variable-highlight" })
);
}
}
return builder.finish();
}
}, {
decorations: v => v.decorations
});
export default { export default {
components: { components: {
NetworkInput, NetworkInput,
@@ -307,6 +335,7 @@ export default {
const extensions = [ const extensions = [
editorTheme, editorTheme,
yaml(), yaml(),
variableHighlight,
lineNumbers(), lineNumbers(),
EditorView.focusChangeEffect.of(focusEffectHandler) EditorView.focusChangeEffect.of(focusEffectHandler)
]; ];
@@ -314,11 +343,13 @@ export default {
const extensionsEnv = [ const extensionsEnv = [
editorTheme, editorTheme,
python(), python(),
variableHighlight,
lineNumbers(), lineNumbers(),
EditorView.focusChangeEffect.of(focusEffectHandler) EditorView.focusChangeEffect.of(focusEffectHandler)
]; ];
return { extensions, return {
extensions,
extensionsEnv, extensionsEnv,
editorFocus }; editorFocus };
}, },
@@ -773,7 +804,7 @@ export default {
}, },
checkYAML() { checkYAML() {
// TODO: implement validation
}, },
addContainer() { addContainer() {
@@ -853,6 +884,11 @@ export default {
height: 200px; height: 200px;
} }
:deep(.cm-variable-highlight) {
color: #fe6000;
font-weight: 600;
}
.editor-box { .editor-box {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 14px; font-size: 14px;

View File

@@ -7,22 +7,9 @@
<div v-else class="alert alert-warning shadow-box" role="alert"> <div v-else class="alert alert-warning shadow-box" role="alert">
<h4 class="alert-heading">{{ $t("Console is not enabled") }}</h4> <h4 class="alert-heading">{{ $t("Console is not enabled") }}</h4>
<i18n-t keypath="ConsoleNotEnabledMSG1" tag="p"> <p v-html="$t('ConsoleNotEnabledMSG1')"></p>
<template #docker><code>{{ $t('dockerCode') }}</code></template> <p v-html="$t('ConsoleNotEnabledMSG2')"></p>
<template #rm><code>{{ $t('rmCode') }}</code></template> <p v-html="$t('ConsoleNotEnabledMSG3')"></p>
</i18n-t>
<i18n-t keypath="ConsoleNotEnabledMSG2" tag="p">
<template #rmRf>
<code>{{ $t('rmRfCode') }}</code>
</template>
</i18n-t>
<i18n-t keypath="ConsoleNotEnabledMSG3" tag="p">
<template #envVar>
<code>{{ $t('envVarCode') }}</code>
</template>
</i18n-t>
</div> </div>
</div> </div>
</transition> </transition>

View File

@@ -40,7 +40,7 @@
<div class="shadow-box big-padding"> <div class="shadow-box big-padding">
<h4 class="mb-3">{{ $tc("dockgeAgent", 2) }} <span class="badge bg-warning" style="font-size: 12px;">beta</span></h4> <h4 class="mb-3">{{ $tc("dockgeAgent", 2) }} <span class="badge bg-warning" style="font-size: 12px;">beta</span></h4>
<div v-for="(agentItem, endpoint) in $root.agentList" :key="endpoint" class="mb-3 agent"> <div v-for="(agent, endpoint) in $root.agentList" :key="endpoint" class="mb-3 agent">
<!-- Agent Status --> <!-- Agent Status -->
<template v-if="$root.agentStatusList[endpoint]"> <template v-if="$root.agentStatusList[endpoint]">
<span v-if="$root.agentStatusList[endpoint] === 'online'" class="badge bg-primary me-2">{{ $t("agentOnline") }}</span> <span v-if="$root.agentStatusList[endpoint] === 'online'" class="badge bg-primary me-2">{{ $t("agentOnline") }}</span>
@@ -50,26 +50,26 @@
<!-- Agent Display Name --> <!-- Agent Display Name -->
<template v-if="$root.agentStatusList[endpoint]"> <template v-if="$root.agentStatusList[endpoint]">
<span v-if="endpoint === '' && agentItem.name === ''" class="badge bg-secondary me-2">Current</span> <span v-if="endpoint === '' && agent.name === ''" class="badge bg-secondary me-2">Current</span>
<span v-else-if="agentItem.name === ''" :href="agentItem.url" class="me-2">{{ endpoint }}</span> <span v-else-if="agent.name === ''" :href="agent.url" class="me-2">{{ endpoint }}</span>
<span v-else :href="agentItem.url" class="me-2">{{ agentItem.name }}</span> <span v-else :href="agent.url" class="me-2">{{ agent.name }}</span>
</template> </template>
<!-- Edit Name --> <!-- Edit Name -->
<font-awesome-icon v-if="agentItem.name !== ''" icon="pen-to-square" @click="showEditAgentNameDialog[agentItem.name] = !showEditAgentNameDialog[agentItem.Name]" /> <font-awesome-icon v-if="agent.name !== ''" icon="pen-to-square" @click="showEditAgentNameDialog[agent.name] = !showEditAgentNameDialog[agent.Name]" />
<!-- Edit Dialog --> <!-- Edit Dialog -->
<BModal v-model="showEditAgentNameDialog[agentItem.name]" :no-close-on-backdrop="true" :close-on-esc="true" :okTitle="$t('Update Name')" okVariant="info" @ok="updateName(agentItem.url, agentItem.updatedName)"> <BModal v-model="showEditAgentNameDialog[agent.name]" :no-close-on-backdrop="true" :close-on-esc="true" :okTitle="$t('Update Name')" okVariant="info" @ok="updateName(agent.url, agent.updatedName)">
<label for="Update Name" class="form-label">Current value: {{ $t(agentItem.name) }}</label> <label for="Update Name" class="form-label">Current value: {{ $t(agent.name) }}</label>
<input id="updatedName" v-model="agentItem.updatedName" type="text" class="form-control" optional> <input id="updatedName" v-model="agent.updatedName" type="text" class="form-control" optional>
</BModal> </BModal>
<!-- Remove Button --> <!-- Remove Button -->
<font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agentItem.url] = !showRemoveAgentDialog[agentItem.url]" /> <font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agent.url] = !showRemoveAgentDialog[agent.url]" />
<!-- Remove Agent Dialog --> <!-- Remove Agent Dialog -->
<BModal v-model="showRemoveAgentDialog[agentItem.url]" :okTitle="$t('removeAgent')" okVariant="danger" @ok="removeAgent(agentItem.url)"> <BModal v-model="showRemoveAgentDialog[agent.url]" :okTitle="$t('removeAgent')" okVariant="danger" @ok="removeAgent(agent.url)">
<p>{{ agentItem.url }}</p> <p>{{ agent.url }}</p>
{{ $t("removeAgentMsg") }} {{ $t("removeAgentMsg") }}
</BModal> </BModal>
</div> </div>

570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@
"ts-command-line-args": "~2.5.1", "ts-command-line-args": "~2.5.1",
"tsx": "~4.19.3", "tsx": "~4.19.3",
"type-fest": "~4.3.3", "type-fest": "~4.3.3",
"yaml": "~2.8.3" "yaml": "~2.3.4"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",