mirror of
https://github.com/louislam/dockge.git
synced 2026-05-21 14:02:17 +00:00
wip
This commit is contained in:
5
frontend/components.d.ts
vendored
5
frontend/components.d.ts
vendored
@@ -7,14 +7,13 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BDropdown: typeof import('bootstrap-vue-next')['BDropdown']
|
||||
BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem']
|
||||
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
||||
Login: typeof import('./src/components/Login.vue')['default']
|
||||
MonitorListItem: typeof import('./src/components/MonitorListItem.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StackList: typeof import('./src/components/StackList.vue')['default']
|
||||
StackListItem: typeof import('./src/components/StackListItem.vue')['default']
|
||||
Terminal: typeof import('./src/components/Terminal.vue')['default']
|
||||
Uptime: typeof import('./src/components/Uptime.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ export default {
|
||||
this.$root.loggedIn = true;
|
||||
this.$root.username = this.$root.getJWTPayload()?.username;
|
||||
|
||||
this.$root.afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- TODO -->
|
||||
<div v-if="false" class="header-filter">
|
||||
<!--<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
<!--<StackListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
</div>
|
||||
|
||||
<!-- TODO: Selection Controls -->
|
||||
@@ -40,12 +40,12 @@
|
||||
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
|
||||
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
|
||||
|
||||
<span v-if="selectedMonitorCount > 0">
|
||||
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
|
||||
<span v-if="selectedStackCount > 0">
|
||||
{{ $t("selectedStackCount", [ selectedStackCount ]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
|
||||
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
|
||||
<div v-if="Object.keys($root.stackList).length === 0" class="text-center mt-3">
|
||||
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<StackListItem
|
||||
v-for="(item, index) in sortedStackList"
|
||||
:key="index"
|
||||
:monitor="item"
|
||||
:stack="item"
|
||||
:showPathName="filtersActive"
|
||||
:isSelectMode="selectMode"
|
||||
:isSelected="isSelected"
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
|
||||
{{ $t("pauseMonitorMsg") }}
|
||||
{{ $t("pauseStackMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
selectMode: false,
|
||||
selectAll: false,
|
||||
disableSelectAllWatcher: false,
|
||||
selectedMonitors: {},
|
||||
selectedStacks: {},
|
||||
windowTop: 0,
|
||||
filterState: {
|
||||
status: null,
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
* Improve the sticky appearance of the list by increasing its
|
||||
* height as user scrolls down.
|
||||
* Not used on mobile.
|
||||
* @returns {object} Style for monitor list
|
||||
* @returns {object} Style for stack list
|
||||
*/
|
||||
boxStyle() {
|
||||
if (window.innerWidth > 550) {
|
||||
@@ -119,80 +119,43 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a sorted list of monitors based on the applied filters and search text.
|
||||
* @returns {Array} The sorted list of monitors.
|
||||
* Returns a sorted list of stacks based on the applied filters and search text.
|
||||
* @returns {Array} The sorted list of stacks.
|
||||
*/
|
||||
sortedStackList() {
|
||||
let result = Object.values(this.$root.stackList);
|
||||
|
||||
result = result.filter(monitor => {
|
||||
result = result.filter(stack => {
|
||||
// filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
// finds stack name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
stack.name.toLowerCase().includes(loweredSearchText)
|
||||
|| stack.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
|
||||
// filter by status
|
||||
let statusMatch = true;
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
statusMatch = this.filterState.status.includes(monitor.status);
|
||||
}
|
||||
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
activeMatch = this.filterState.active.includes(monitor.active);
|
||||
activeMatch = this.filterState.active.includes(stack.active);
|
||||
}
|
||||
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||
tagsMatch = stack.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(stackTagId => this.filterState.tags.includes(stackTagId)) // perform Array Intersaction between filter and stack's tags
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
// Hide children if not filtering
|
||||
let showChild = true;
|
||||
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
|
||||
if (monitor.parent !== null) {
|
||||
showChild = false;
|
||||
}
|
||||
}
|
||||
|
||||
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
|
||||
return searchTextMatch && activeMatch && tagsMatch;
|
||||
});
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
result.sort((m1, m2) => {
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === false) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
@@ -203,8 +166,9 @@ export default {
|
||||
return document.body.classList.contains("dark");
|
||||
},
|
||||
|
||||
monitorListStyle() {
|
||||
let listHeaderHeight = 107;
|
||||
stackListStyle() {
|
||||
//let listHeaderHeight = 107;
|
||||
let listHeaderHeight = 60;
|
||||
|
||||
if (this.selectMode) {
|
||||
listHeaderHeight += 42;
|
||||
@@ -215,8 +179,8 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
selectedMonitorCount() {
|
||||
return Object.keys(this.selectedMonitors).length;
|
||||
selectedStackCount() {
|
||||
return Object.keys(this.selectedStacks).length;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -229,8 +193,8 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
searchText() {
|
||||
for (let monitor of this.sortedMonitorList) {
|
||||
if (!this.selectedMonitors[monitor.id]) {
|
||||
for (let stack of this.sortedStackList) {
|
||||
if (!this.selectedStacks[stack.id]) {
|
||||
if (this.selectAll) {
|
||||
this.disableSelectAllWatcher = true;
|
||||
this.selectAll = false;
|
||||
@@ -241,11 +205,11 @@ export default {
|
||||
},
|
||||
selectAll() {
|
||||
if (!this.disableSelectAllWatcher) {
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
|
||||
if (this.selectAll) {
|
||||
this.sortedMonitorList.forEach((item) => {
|
||||
this.selectedMonitors[item.id] = true;
|
||||
this.sortedStackList.forEach((item) => {
|
||||
this.selectedStacks[item.id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -255,7 +219,7 @@ export default {
|
||||
selectMode() {
|
||||
if (!this.selectMode) {
|
||||
this.selectAll = false;
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -286,7 +250,7 @@ export default {
|
||||
this.searchText = "";
|
||||
},
|
||||
/**
|
||||
* Update the MonitorList Filter
|
||||
* Update the StackList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
* @returns {void}
|
||||
*/
|
||||
@@ -294,28 +258,28 @@ export default {
|
||||
this.filterState = newFilter;
|
||||
},
|
||||
/**
|
||||
* Deselect a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* Deselect a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
deselect(id) {
|
||||
delete this.selectedMonitors[id];
|
||||
delete this.selectedStacks[id];
|
||||
},
|
||||
/**
|
||||
* Select a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* Select a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
select(id) {
|
||||
this.selectedMonitors[id] = true;
|
||||
this.selectedStacks[id] = true;
|
||||
},
|
||||
/**
|
||||
* Determine if monitor is selected
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {bool} Is the monitor selected?
|
||||
* Determine if stack is selected
|
||||
* @param {number} id ID of stack
|
||||
* @returns {bool} Is the stack selected?
|
||||
*/
|
||||
isSelected(id) {
|
||||
return id in this.selectedMonitors;
|
||||
return id in this.selectedStacks;
|
||||
},
|
||||
/**
|
||||
* Disable select mode and reset selection
|
||||
@@ -323,7 +287,7 @@ export default {
|
||||
*/
|
||||
cancelSelectMode() {
|
||||
this.selectMode = false;
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
},
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
@@ -333,24 +297,24 @@ export default {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
/**
|
||||
* Pause each selected monitor
|
||||
* Pause each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseStack", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
/**
|
||||
* Resume each selected monitor
|
||||
* Resume each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
resumeSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => !this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => !this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeStack", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
@@ -428,7 +392,7 @@ export default {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.monitor-item {
|
||||
.stack-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,68 +7,44 @@
|
||||
class="form-check-input select-input"
|
||||
type="checkbox"
|
||||
:aria-label="$t('Check/Uncheck')"
|
||||
:checked="isSelected(monitor.id)"
|
||||
:checked="isSelected(stack.id)"
|
||||
@click.stop="toggleSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||
<router-link :to="`/compose/${stack.name}`" class="item">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<Uptime :monitor="monitor" type="24" :pill="true" />
|
||||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
||||
</span>
|
||||
{{ monitorName }}
|
||||
<Uptime :stack="stack" type="24" :pill="true" />
|
||||
{{ stackName }}
|
||||
</div>
|
||||
<div v-if="monitor.tags.length > 0" class="tags">
|
||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
<div v-if="stack.tags.length > 0" class="tags">
|
||||
<!--<Tag v-for="tag in stack.tags" :key="tag" :item="tag" :size="'sm'" />-->
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12 bottom-style">
|
||||
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<transition name="slide-fade-up">
|
||||
<div v-if="!isCollapsed" class="childs">
|
||||
<MonitorListItem
|
||||
v-for="(item, index) in sortedChildMonitorList"
|
||||
:key="index" :monitor="item"
|
||||
:showPathName="showPathName"
|
||||
:isSelectMode="isSelectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
:deselect="deselect"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Uptime from "./Uptime.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime
|
||||
|
||||
},
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
/** Stack this represents */
|
||||
stack: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Should the monitor name show it's parent */
|
||||
/** Should the stack name show it's parent */
|
||||
showPathName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -78,22 +54,22 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** How many ancestors are above this monitor */
|
||||
/** How many ancestors are above this stack */
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/** Callback to determine if monitor is selected */
|
||||
/** Callback to determine if stack is selected */
|
||||
isSelected: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Callback fired when monitor is selected */
|
||||
/** Callback fired when stack is selected */
|
||||
select: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Callback fired when monitor is deselected */
|
||||
/** Callback fired when stack is deselected */
|
||||
deselect: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
@@ -105,51 +81,16 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedChildMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
hasChildren() {
|
||||
return this.sortedChildMonitorList.length > 0;
|
||||
},
|
||||
depthMargin() {
|
||||
return {
|
||||
marginLeft: `${31 * this.depth}px`,
|
||||
};
|
||||
},
|
||||
monitorName() {
|
||||
stackName() {
|
||||
if (this.showPathName) {
|
||||
return this.monitor.pathName;
|
||||
return this.stack.pathName;
|
||||
} else {
|
||||
return this.monitor.name;
|
||||
return this.stack.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -161,28 +102,10 @@ export default {
|
||||
},
|
||||
beforeMount() {
|
||||
|
||||
// Always unfold if monitor is accessed directly
|
||||
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
|
||||
this.isCollapsed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set collapsed value based on local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
if (storage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let storageObject = JSON.parse(storage);
|
||||
if (storageObject[`monitor_${this.monitor.id}`] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Changes the collapsed value of the current monitor and saves
|
||||
* Changes the collapsed value of the current stack and saves
|
||||
* it to local storage
|
||||
* @returns {void}
|
||||
*/
|
||||
@@ -190,25 +113,25 @@ export default {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
// Save collapsed value into local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
let storage = window.localStorage.getItem("stackCollapsed");
|
||||
let storageObject = {};
|
||||
if (storage !== null) {
|
||||
storageObject = JSON.parse(storage);
|
||||
}
|
||||
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
|
||||
storageObject[`stack_${this.stack.id}`] = this.isCollapsed;
|
||||
|
||||
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
|
||||
window.localStorage.setItem("stackCollapsed", JSON.stringify(storageObject));
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle selection of monitor
|
||||
* Toggle selection of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleSelection() {
|
||||
if (this.isSelected(this.monitor.id)) {
|
||||
this.deselect(this.monitor.id);
|
||||
if (this.isSelected(this.stack.id)) {
|
||||
this.deselect(this.stack.id);
|
||||
} else {
|
||||
this.select(this.monitor.id);
|
||||
this.select(this.stack.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -228,7 +151,7 @@ export default {
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
|
||||
// .monitor-item {
|
||||
// .stack-item {
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
|
||||
93
frontend/src/components/Terminal.vue
Normal file
93
frontend/src/components/Terminal.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="shadow-box">
|
||||
<div v-pre ref="terminal" class="main-terminal"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Terminal } from "xterm";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
|
||||
|
||||
export default {
|
||||
/**
|
||||
* @type {Terminal}
|
||||
*/
|
||||
terminal: null,
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
allowInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: TERMINAL_ROWS,
|
||||
}
|
||||
},
|
||||
emits: [ "has-data" ],
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
first: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.terminal = new Terminal({
|
||||
fontSize: 16,
|
||||
fontFamily: "monospace",
|
||||
cursorBlink: this.allowInput,
|
||||
cols: TERMINAL_COLS,
|
||||
rows: this.rows,
|
||||
});
|
||||
|
||||
this.terminal.loadAddon(new WebLinksAddon());
|
||||
|
||||
// Bind to a div
|
||||
this.terminal.open(this.$refs.terminal);
|
||||
this.terminal.focus();
|
||||
|
||||
// Notify parent component when data is received
|
||||
this.terminal.onCursorMove(() => {
|
||||
console.debug("onData triggered");
|
||||
if (this.first) {
|
||||
this.$emit("has-data");
|
||||
this.first = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.$root.unbindTerminal(this.name);
|
||||
this.terminal.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
bind(name) {
|
||||
if (this.name) {
|
||||
this.$root.unbindTerminal(this.name);
|
||||
}
|
||||
this.name = name;
|
||||
this.$root.bindTerminal(this.name, this.terminal);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.main-terminal {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.terminal {
|
||||
padding: 10px 15px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
58
frontend/src/components/Uptime.vue
Normal file
58
frontend/src/components/Uptime.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<span :class="className" :title="title">{{ uptime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Type of monitor */
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/** Is this a pill? */
|
||||
pill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
uptime() {
|
||||
return this.$t("notAvailableShort");
|
||||
},
|
||||
|
||||
color() {
|
||||
return "secondary";
|
||||
},
|
||||
|
||||
className() {
|
||||
if (this.pill) {
|
||||
return `badge rounded-pill bg-${this.color}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
title() {
|
||||
if (this.type === "720") {
|
||||
return `30${this.$t("-day")}`;
|
||||
}
|
||||
|
||||
return `24${this.$t("-hour")}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
min-width: 62px;
|
||||
}
|
||||
</style>
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
faCertificate,
|
||||
faTerminal, faWarehouse, faHome,
|
||||
faTerminal, faWarehouse, faHome, faRocket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@@ -101,6 +101,7 @@ library.add(
|
||||
faTerminal,
|
||||
faWarehouse,
|
||||
faHome,
|
||||
faRocket,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
||||
@@ -8,5 +8,15 @@
|
||||
"console": "Console",
|
||||
"registry": "Registry",
|
||||
"compose": "Compose",
|
||||
"addFirstStackMsg": "Compose your first stack!"
|
||||
"addFirstStackMsg": "Compose your first stack!",
|
||||
"stackName" : "Stack Name",
|
||||
"deployStack": "Deploy",
|
||||
"deleteStack": "Delete",
|
||||
"stopStack": "Stop",
|
||||
"restartStack": "Restart",
|
||||
"startStack": "Start",
|
||||
"editStack": "Edit",
|
||||
"discardStack": "Discard",
|
||||
"saveStackDraft": "Save",
|
||||
"notAvailableShort" : "N/A"
|
||||
}
|
||||
|
||||
@@ -3,20 +3,13 @@ import { Socket } from "socket.io-client";
|
||||
import { defineComponent } from "vue";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
|
||||
const terminal = new Terminal({
|
||||
fontSize: 16,
|
||||
fontFamily: "monospace",
|
||||
cursorBlink: true,
|
||||
});
|
||||
terminal.loadAddon(new FitAddon());
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
let terminalInputBuffer = "";
|
||||
let cursorPosition = 0;
|
||||
let socket : Socket;
|
||||
|
||||
let terminalMap : Map<string, Terminal> = new Map();
|
||||
|
||||
function removeInput() {
|
||||
const backspaceCount = terminalInputBuffer.length;
|
||||
const backspaces = "\b \b".repeat(backspaceCount);
|
||||
@@ -44,7 +37,6 @@ export default defineComponent({
|
||||
loggedIn: false,
|
||||
allowLoginDialog: false,
|
||||
username: null,
|
||||
|
||||
stackList: {},
|
||||
};
|
||||
},
|
||||
@@ -66,6 +58,7 @@ export default defineComponent({
|
||||
this.initSocketIO();
|
||||
},
|
||||
mounted() {
|
||||
return;
|
||||
terminal.onKey(e => {
|
||||
const code = e.key.charCodeAt(0);
|
||||
console.debug("Encode: " + JSON.stringify(e.key));
|
||||
@@ -100,6 +93,7 @@ export default defineComponent({
|
||||
// TODO
|
||||
} else if (e.key === "\u0003") { // Ctrl + C
|
||||
console.debug("Ctrl + C");
|
||||
socket.emit("terminalInputRaw", e.key);
|
||||
removeInput();
|
||||
} else {
|
||||
cursorPosition++;
|
||||
@@ -131,7 +125,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
socket = io(url, {
|
||||
|
||||
transports: [ "websocket", "polling" ]
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
@@ -195,9 +189,20 @@ export default defineComponent({
|
||||
this.$router.push("/setup");
|
||||
});
|
||||
|
||||
socket.on("commandOutput", (data) => {
|
||||
socket.on("terminalWrite", (terminalName, data) => {
|
||||
const terminal = terminalMap.get(terminalName);
|
||||
if (!terminal) {
|
||||
console.error("Terminal not found: " + terminalName);
|
||||
return;
|
||||
}
|
||||
terminal.write(data);
|
||||
});
|
||||
|
||||
socket.on("stackList", (res) => {
|
||||
if (res.ok) {
|
||||
this.stackList = res.stackList;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -212,10 +217,6 @@ export default defineComponent({
|
||||
return socket;
|
||||
},
|
||||
|
||||
getTerminal() : Terminal {
|
||||
return terminal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get payload of JWT cookie
|
||||
* @returns {(object | undefined)} JWT payload
|
||||
@@ -269,13 +270,23 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
afterLogin() {
|
||||
terminal.clear();
|
||||
|
||||
},
|
||||
|
||||
bindTerminal(terminalName : string, terminal : Terminal) {
|
||||
// Load terminal, get terminal screen
|
||||
socket.emit("getTerminalBuffer", (res) => {
|
||||
console.log("getTerminalBuffer");
|
||||
terminal.write(res.buffer);
|
||||
socket.emit("terminalJoin", terminalName, (res) => {
|
||||
if (res.ok) {
|
||||
terminal.write(res.buffer);
|
||||
terminalMap.set(terminalName, terminal);
|
||||
} else {
|
||||
this.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unbindTerminal(terminalName : string) {
|
||||
terminalMap.delete(terminalName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,16 +1,224 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 v-if="isAdd" class="mb-3">Compose</h1>
|
||||
<h1 v-else class="mb-3">Stack: {{ stack.name }}</h1>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
|
||||
<font-awesome-icon icon="rocket" class="me-1" />
|
||||
{{ $t("deployStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="isEditMode" class="btn btn-normal" :disabled="processing" @click="saveStack">
|
||||
<font-awesome-icon icon="save" class="me-1" />
|
||||
{{ $t("saveStackDraft") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
|
||||
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-primary " :disabled="processing">{{ $t("restartStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("stopStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("deleteStack") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Terminal -->
|
||||
<transition name="slide-fade" appear>
|
||||
<Terminal
|
||||
v-show="showProgressTerminal"
|
||||
ref="progressTerminal"
|
||||
:allow-input="false"
|
||||
class="mb-3 terminal"
|
||||
:rows="progressTerminalRows"
|
||||
@has-data="showProgressTerminal = true"
|
||||
></Terminal>
|
||||
</transition>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="mb-3">General</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<!-- Stack Name -->
|
||||
<div v-if="isAdd" class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
||||
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mb-3">Containers</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<div v-for="(service, name) in jsonConfig.services" :key="name">
|
||||
{{ name }} {{ service }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4 class="mb-3">compose.yaml</h4>
|
||||
|
||||
<div class="shadow-box mb-3">
|
||||
<prism-editor v-model="stack.composeYAML" class="yaml-editor" :highlight="highlighter" line-numbers :readonly="!isEditMode" @input="yamlCodeChange"></prism-editor>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ yamlError }}
|
||||
</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>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
<script>
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import * as yaml from "yaml";
|
||||
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "../../../backend/util-common";
|
||||
|
||||
const template = `version: "3.8"
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
`;
|
||||
|
||||
let yamlErrorTimeout = null;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
PrismEditor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonConfig: {},
|
||||
yamlError: "",
|
||||
processing: true,
|
||||
showProgressTerminal: false,
|
||||
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
||||
stack: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isAdd() {
|
||||
return this.$route.path === "/compose" && !this.submitted;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"stack.composeYAML": {
|
||||
handler() {
|
||||
this.yamlCodeChange();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isAdd) {
|
||||
this.processing = false;
|
||||
this.isEditMode = true;
|
||||
|
||||
// Default Values
|
||||
this.stack = {
|
||||
name: "",
|
||||
composeYAML: template,
|
||||
};
|
||||
|
||||
} else {
|
||||
this.stack.name = this.$route.params.stackName;
|
||||
this.loadStack();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadStack() {
|
||||
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.stack = res.stack;
|
||||
this.processing = false;
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
deployStack() {
|
||||
this.processing = true;
|
||||
|
||||
// Bind Terminal output
|
||||
const terminalName = getComposeTerminalName(this.stack.name);
|
||||
this.$refs.progressTerminal.bind(terminalName);
|
||||
|
||||
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
} else {
|
||||
this.submitted = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
saveStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
discardStack() {
|
||||
this.loadStack();
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
highlighter(code) {
|
||||
return highlight(code, languages.yaml);
|
||||
},
|
||||
yamlCodeChange() {
|
||||
try {
|
||||
this.jsonConfig = yaml.parse(this.stack.composeYAML) ?? {};
|
||||
this.yamlError = "";
|
||||
} catch (e) {
|
||||
clearTimeout(yamlErrorTimeout);
|
||||
|
||||
if (this.yamlError) {
|
||||
this.yamlError = e.message;
|
||||
|
||||
} else {
|
||||
yamlErrorTimeout = setTimeout(() => {
|
||||
this.yamlError = e.message;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.terminal {
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
<div>
|
||||
<h1 class="mb-3">Console</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div v-pre id="terminal"></div>
|
||||
<div>
|
||||
<p>
|
||||
Allowed commands: <code>docker</code>, <code>ls</code>, <code>cd</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Terminal :allow-input="true" class="terminal"></Terminal>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -14,10 +18,9 @@
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getTerminal().open(document.querySelector("#terminal"));
|
||||
this.$root.terminalFit(50);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -27,9 +30,6 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.terminal {
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
padding: 10px 15px;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -4,8 +4,8 @@ import Layout from "./layouts/Layout.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
import Dashboard from "./pages/Dashboard.vue";
|
||||
import DashboardHome from "./pages/DashboardHome.vue";
|
||||
import EditStack from "./pages/EditStack.vue";
|
||||
import Console from "./pages/Console.vue";
|
||||
import Compose from "./pages/Compose.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -23,7 +23,13 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: "/compose",
|
||||
component: EditStack,
|
||||
component: Compose,
|
||||
},
|
||||
{
|
||||
path: "/compose/:stackName",
|
||||
name: "compose",
|
||||
component: Compose,
|
||||
props: true,
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
@@ -377,7 +377,7 @@ optgroup {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.stack-list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
@@ -474,7 +474,7 @@ optgroup {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.stack-list {
|
||||
&.scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -653,12 +653,28 @@ $shadow-box-padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#terminal {
|
||||
.main-terminal {
|
||||
.xterm-viewport {
|
||||
border-radius: 10px;
|
||||
background-color: $dark-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
background-color: rgba(239, 239, 239, 0.15);
|
||||
|
||||
border-radius: 6px;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
color: black;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Localization
|
||||
@import "localization.scss";
|
||||
|
||||
@@ -158,7 +158,7 @@ export function colorOptions(self) {
|
||||
export function loadToastSettings() {
|
||||
return {
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
containerClassName: "toast-container mb-5",
|
||||
containerClassName: "toast-container",
|
||||
showCloseButtonOnHover: true,
|
||||
|
||||
filterBeforeCreate: (toast, toasts) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
||||
},
|
||||
root: "./frontend",
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
outDir: "../frontend-dist",
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
|
||||
Reference in New Issue
Block a user