Compare commits
3 Commits
develop
...
feat/refac
Author | SHA1 | Date |
---|---|---|
zhengyi | d267e169fc | |
zhengyi | 484dfa1143 | |
zhengyi | f56331e400 |
|
@ -0,0 +1,12 @@
|
|||
export interface Stack {
|
||||
name?: string;
|
||||
composeYAML?: string;
|
||||
composeENV?: string;
|
||||
isManagedByDockge?: boolean;
|
||||
endpoint?: string;
|
||||
composeFileName?: string;
|
||||
}
|
||||
|
||||
export interface JsonConfig {
|
||||
services?: Record<string, object>;
|
||||
}
|
|
@ -2,8 +2,5 @@
|
|||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
};
|
||||
<script setup>
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="header-top">
|
||||
<!-- TODO -->
|
||||
<button v-if="false" class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
|
||||
{{ $t("Select") }}
|
||||
{{ t("Select") }}
|
||||
</button>
|
||||
|
||||
<div class="placeholder"></div>
|
||||
|
@ -23,7 +23,7 @@
|
|||
|
||||
<!-- TODO -->
|
||||
<div v-if="false" class="header-filter">
|
||||
<!-- <StackListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
<!--<StackListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
</div>
|
||||
|
||||
<!-- TODO: Selection Controls -->
|
||||
|
@ -34,17 +34,17 @@
|
|||
type="checkbox"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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="selectedStackCount > 0">
|
||||
{{ $t("selectedStackCount", [ selectedStackCount ]) }}
|
||||
{{ t("selectedStackCount", [ selectedStackCount ]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
|
||||
<div v-if="Object.keys(sortedStackList).length === 0" class="text-center mt-3">
|
||||
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
|
||||
<router-link to="/compose">{{ t("addFirstStackMsg") }}</router-link>
|
||||
</div>
|
||||
|
||||
<StackListItem
|
||||
|
@ -59,294 +59,290 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
|
||||
{{ $t("pauseStackMsg") }}
|
||||
<Confirm ref="confirmPause" :yes-text="t('Yes')" :no-text="t('No')" @yes="pauseSelected">
|
||||
{{ t("pauseStackMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import StackListItem from "../components/StackListItem.vue";
|
||||
import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../common/util-common";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useSocket } from "../sockets";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
StackListItem,
|
||||
},
|
||||
props: {
|
||||
/** Should the scrollbar be shown */
|
||||
scrollbar: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const socket = useSocket();
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{scrollbar: boolean}>();
|
||||
|
||||
const confirmPause = ref<typeof Confirm>();
|
||||
|
||||
const searchText = ref<string>("");
|
||||
const selectMode = ref<boolean>(false);
|
||||
const selectAll = ref<boolean>(false);
|
||||
const disableSelectAllWatcher = ref<boolean>(false);
|
||||
const selectedStacks = ref({});
|
||||
const windowTop = ref<number>(0);
|
||||
const filterState = ref ({
|
||||
status: null,
|
||||
active: null,
|
||||
tags: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Improve the sticky appearance of the list by increasing its
|
||||
* height as user scrolls down.
|
||||
* Not used on mobile.
|
||||
* @returns {object} Style for stack list
|
||||
*/
|
||||
const boxStyle = computed(() => {
|
||||
if (window.innerWidth > 550) {
|
||||
return {
|
||||
searchText: "",
|
||||
selectMode: false,
|
||||
selectAll: false,
|
||||
disableSelectAllWatcher: false,
|
||||
selectedStacks: {},
|
||||
windowTop: 0,
|
||||
filterState: {
|
||||
status: null,
|
||||
active: null,
|
||||
tags: null,
|
||||
}
|
||||
height: `calc(100vh - 160px + ${windowTop.value}px)`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Improve the sticky appearance of the list by increasing its
|
||||
* height as user scrolls down.
|
||||
* Not used on mobile.
|
||||
* @returns {object} Style for stack list
|
||||
*/
|
||||
boxStyle() {
|
||||
if (window.innerWidth > 550) {
|
||||
return {
|
||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
height: "calc(100vh - 160px)",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
height: "calc(100vh - 160px)",
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.completeStackList);
|
||||
/**
|
||||
* Returns a sorted list of stacks based on the applied filters and search text.
|
||||
* @returns {Array} The sorted list of stacks.
|
||||
*/
|
||||
const sortedStackList = computed(() => {
|
||||
let result = Object.values(socket.completeStackList.value);
|
||||
|
||||
result = result.filter(stack => {
|
||||
// filter by search text
|
||||
// finds stack name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
stack.name.toLowerCase().includes(loweredSearchText)
|
||||
|| stack.tags.find(tag => tag.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
activeMatch = this.filterState.active.includes(stack.active);
|
||||
}
|
||||
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
return searchTextMatch && activeMatch && tagsMatch;
|
||||
});
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
// sort by managed by dockge
|
||||
if (m1.isManagedByDockge && !m2.isManagedByDockge) {
|
||||
return -1;
|
||||
} else if (!m1.isManagedByDockge && m2.isManagedByDockge) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// sort by status
|
||||
if (m1.status !== m2.status) {
|
||||
if (m2.status === RUNNING) {
|
||||
return 1;
|
||||
} else if (m1.status === RUNNING) {
|
||||
return -1;
|
||||
} else if (m2.status === EXITED) {
|
||||
return 1;
|
||||
} else if (m1.status === EXITED) {
|
||||
return -1;
|
||||
} else if (m2.status === CREATED_STACK) {
|
||||
return 1;
|
||||
} else if (m1.status === CREATED_STACK) {
|
||||
return -1;
|
||||
} else if (m2.status === CREATED_FILE) {
|
||||
return 1;
|
||||
} else if (m1.status === CREATED_FILE) {
|
||||
return -1;
|
||||
} else if (m2.status === UNKNOWN) {
|
||||
return 1;
|
||||
} else if (m1.status === UNKNOWN) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
isDarkTheme() {
|
||||
return document.body.classList.contains("dark");
|
||||
},
|
||||
|
||||
stackListStyle() {
|
||||
//let listHeaderHeight = 107;
|
||||
let listHeaderHeight = 60;
|
||||
|
||||
if (this.selectMode) {
|
||||
listHeaderHeight += 42;
|
||||
}
|
||||
|
||||
return {
|
||||
"height": `calc(100% - ${listHeaderHeight}px)`
|
||||
};
|
||||
},
|
||||
|
||||
selectedStackCount() {
|
||||
return Object.keys(this.selectedStacks).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if any filters are active.
|
||||
* @returns {boolean} True if any filter is active, false otherwise.
|
||||
*/
|
||||
filtersActive() {
|
||||
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
|
||||
result = result.filter(stack => {
|
||||
// filter by search text
|
||||
// finds stack name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (searchText.value !== "") {
|
||||
const loweredSearchText = searchText.value.toLowerCase();
|
||||
searchTextMatch =
|
||||
stack.name.toLowerCase().includes(loweredSearchText)
|
||||
|| stack.tags.find(tag => tag.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchText() {
|
||||
for (let stack of this.sortedStackList) {
|
||||
if (!this.selectedStacks[stack.id]) {
|
||||
if (this.selectAll) {
|
||||
this.disableSelectAllWatcher = true;
|
||||
this.selectAll = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
if (!this.disableSelectAllWatcher) {
|
||||
this.selectedStacks = {};
|
||||
|
||||
if (this.selectAll) {
|
||||
this.sortedStackList.forEach((item) => {
|
||||
this.selectedStacks[item.id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.disableSelectAllWatcher = false;
|
||||
}
|
||||
},
|
||||
selectMode() {
|
||||
if (!this.selectMode) {
|
||||
this.selectAll = false;
|
||||
this.selectedStacks = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("scroll", this.onScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Handle user scroll
|
||||
* @returns {void}
|
||||
*/
|
||||
onScroll() {
|
||||
if (window.top.scrollY <= 133) {
|
||||
this.windowTop = window.top.scrollY;
|
||||
} else {
|
||||
this.windowTop = 133;
|
||||
}
|
||||
},
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (filterState.value.active != null && filterState.value.active.length > 0) {
|
||||
activeMatch = filterState.value.active.includes(stack.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search bar
|
||||
* @returns {void}
|
||||
*/
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
},
|
||||
/**
|
||||
* Update the StackList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
* @returns {void}
|
||||
*/
|
||||
updateFilter(newFilter) {
|
||||
this.filterState = newFilter;
|
||||
},
|
||||
/**
|
||||
* Deselect a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
deselect(id) {
|
||||
delete this.selectedStacks[id];
|
||||
},
|
||||
/**
|
||||
* Select a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
select(id) {
|
||||
this.selectedStacks[id] = true;
|
||||
},
|
||||
/**
|
||||
* Determine if stack is selected
|
||||
* @param {number} id ID of stack
|
||||
* @returns {bool} Is the stack selected?
|
||||
*/
|
||||
isSelected(id) {
|
||||
return id in this.selectedStacks;
|
||||
},
|
||||
/**
|
||||
* Disable select mode and reset selection
|
||||
* @returns {void}
|
||||
*/
|
||||
cancelSelectMode() {
|
||||
this.selectMode = false;
|
||||
this.selectedStacks = {};
|
||||
},
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseDialog() {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
/**
|
||||
* Pause each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseSelected() {
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseStack", id, () => {}));
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (filterState.value.tags != null && filterState.value.tags.length > 0) {
|
||||
tagsMatch = stack.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(stackTagId => filterState.value.tags.includes(stackTagId)) // perform Array Intersaction between filter and stack's tags
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
/**
|
||||
* Resume each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
resumeSelected() {
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => !this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeStack", id, () => {}));
|
||||
return searchTextMatch && activeMatch && tagsMatch;
|
||||
});
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
},
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
// sort by managed by dockge
|
||||
if (m1.isManagedByDockge && !m2.isManagedByDockge) {
|
||||
return -1;
|
||||
} else if (!m1.isManagedByDockge && m2.isManagedByDockge) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// sort by status
|
||||
if (m1.status !== m2.status) {
|
||||
if (m2.status === RUNNING) {
|
||||
return 1;
|
||||
} else if (m1.status === RUNNING) {
|
||||
return -1;
|
||||
} else if (m2.status === EXITED) {
|
||||
return 1;
|
||||
} else if (m1.status === EXITED) {
|
||||
return -1;
|
||||
} else if (m2.status === CREATED_STACK) {
|
||||
return 1;
|
||||
} else if (m1.status === CREATED_STACK) {
|
||||
return -1;
|
||||
} else if (m2.status === CREATED_FILE) {
|
||||
return 1;
|
||||
} else if (m1.status === CREATED_FILE) {
|
||||
return -1;
|
||||
} else if (m2.status === UNKNOWN) {
|
||||
return 1;
|
||||
} else if (m1.status === UNKNOWN) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const isDarkTheme = computed(() => {
|
||||
return document.body.classList.contains("dark");
|
||||
});
|
||||
|
||||
const stackListStyle = computed(() => {
|
||||
//let listHeaderHeight = 107;
|
||||
let listHeaderHeight = 60;
|
||||
|
||||
if (selectMode.value) {
|
||||
listHeaderHeight += 42;
|
||||
}
|
||||
|
||||
return {
|
||||
"height": `calc(100% - ${listHeaderHeight}px)`
|
||||
};
|
||||
});
|
||||
|
||||
const selectedStackCount = computed(() => {
|
||||
return Object.keys(selectedStacks.value).length;
|
||||
});
|
||||
|
||||
/**
|
||||
* Determines if any filters are active.
|
||||
* @returns {boolean} True if any filter is active, false otherwise.
|
||||
*/
|
||||
const filtersActive = computed(() => {
|
||||
return filterState.value.status != null || filterState.value.active != null
|
||||
|| filterState.value.tags != null || searchText.value !== "";
|
||||
});
|
||||
|
||||
watch(searchText, () => {
|
||||
for (let stack of sortedStackList.value) {
|
||||
if (!selectedStacks.value[stack.id]) {
|
||||
if (selectAll.value) {
|
||||
disableSelectAllWatcher.value = true;
|
||||
selectAll.value = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectAll, () => {
|
||||
if (!disableSelectAllWatcher.value) {
|
||||
selectedStacks.value = {};
|
||||
|
||||
if (selectAll.value) {
|
||||
sortedStackList.value.forEach((item) => {
|
||||
selectedStacks.value[item.id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
disableSelectAllWatcher.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectMode, () => {
|
||||
if (!selectMode.value) {
|
||||
selectAll.value = false;
|
||||
selectedStacks.value = {};
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("scroll", onScroll);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle user scroll
|
||||
* @returns {void}
|
||||
*/
|
||||
const onScroll = () => {
|
||||
if (window.top.scrollY <= 133) {
|
||||
windowTop.value = window.top.scrollY;
|
||||
} else {
|
||||
windowTop.value = 133;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the search bar
|
||||
*/
|
||||
const clearSearchText = () => {
|
||||
searchText.value = "";
|
||||
};
|
||||
/**
|
||||
* Update the StackList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
*/
|
||||
const updateFilter = (newFilter: object) => {
|
||||
filterState.value = newFilter;
|
||||
};
|
||||
/**
|
||||
* Deselect a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
const deselect = (id: number) => {
|
||||
delete selectedStacks.value[id];
|
||||
};
|
||||
/**
|
||||
* Select a stack
|
||||
* @param {number} id ID of stack
|
||||
*/
|
||||
const select = (id: number) => {
|
||||
selectedStacks.value[id] = true;
|
||||
};
|
||||
/**
|
||||
* Determine if stack is selected
|
||||
* @param {number} id ID of stack
|
||||
* @returns {bool} Is the stack selected?
|
||||
*/
|
||||
const isSelected = (id: number): boolean => {
|
||||
return id in selectedStacks.value;
|
||||
};
|
||||
/**
|
||||
* Disable select mode and reset selection
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelSelectMode = (): void => {
|
||||
selectMode.value = false;
|
||||
selectedStacks.value = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
* @returns {void}
|
||||
*/
|
||||
const pauseDialog = (): void => {
|
||||
confirmPause.value?.show();
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
const pauseSelected = (): void => {
|
||||
Object.keys(selectedStacks.value)
|
||||
.filter(id => socket.stackList.value[id].active)
|
||||
.forEach(id => socket.getSocket().emit("pauseStack", id, () => {}));
|
||||
|
||||
cancelSelectMode();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resume each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
const resumeSelected = (): void => {
|
||||
Object.keys(selectedStacks.value)
|
||||
.filter(id => !socket.stackList.value[id].active)
|
||||
.forEach(id => socket.getSocket().emit("resumeStack", id, () => {}));
|
||||
|
||||
cancelSelectMode();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
// @ts-ignore Performance issue when using "vue-i18n", so we use "vue-i18n/dist/vue-i18n.esm-browser.prod.js", but typescript doesn't like that.
|
||||
import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
|
||||
import en from "./lang/en.json";
|
||||
import { setPageLocale } from "./util-frontend";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const langModules = import.meta.glob("./lang/*.json");
|
||||
|
||||
const languageList = {
|
||||
"bg-BG": "Български",
|
||||
|
@ -56,9 +60,19 @@ export const localeDirection = () => {
|
|||
};
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: currentLocale(),
|
||||
fallbackLocale: "en",
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
export const changeLanguage = async (lang: string) => {
|
||||
const i18n = useI18n();
|
||||
const message = (await langModules["./lang/" + lang + ".json"]()).default;
|
||||
i18n.setLocaleMessage(lang, message);
|
||||
i18n.locale.value = lang;
|
||||
localStorage.locale = lang;
|
||||
setPageLocale();
|
||||
};
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<div v-if="! $root.socketIO.connected && ! $root.socketIO.firstConnect" class="lost-connection">
|
||||
<div v-if="! socket.socketIO.value.connected && ! socket.socketIO.value.firstConnect" class="lost-connection">
|
||||
<div class="container-fluid">
|
||||
{{ $root.socketIO.connectionErrorMsg }}
|
||||
<div v-if="$root.socketIO.showReverseProxyGuide">
|
||||
{{ $t("reverseProxyMsg1") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">{{ $t("reverseProxyMsg2") }}</a>
|
||||
{{ socket.socketIO.value.connectionErrorMsg }}
|
||||
<div v-if="socket.socketIO.value.showReverseProxyGuide">
|
||||
{{ t("reverseProxyMsg1") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">{{ t("reverseProxyMsg2") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<header v-if="true || ! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title">Dockge</span>
|
||||
</router-link>
|
||||
|
||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-warning me-3">
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("newUpdate") }}
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ t("newUpdate") }}
|
||||
</a>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||
<li v-if="socket.loggedIn" class="nav-item me-2">
|
||||
<router-link to="/" class="nav-link">
|
||||
<font-awesome-icon icon="home" /> {{ $t("home") }}
|
||||
<font-awesome-icon icon="home" /> {{ t("home") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||
<li v-if="socket.loggedIn" class="nav-item me-2">
|
||||
<router-link to="/console" class="nav-link">
|
||||
<font-awesome-icon icon="terminal" /> {{ $t("console") }}
|
||||
<font-awesome-icon icon="terminal" /> {{ t("console") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li v-if="$root.loggedIn" class="nav-item">
|
||||
<li v-if="socket.loggedIn" class="nav-item">
|
||||
<div class="dropdown dropdown-profile-pic">
|
||||
<div class="nav-link" data-bs-toggle="dropdown">
|
||||
<div class="profile-pic">{{ $root.usernameFirstChar }}</div>
|
||||
<div class="profile-pic">{{ socket.usernameFirstChar }}</div>
|
||||
<font-awesome-icon icon="angle-down" />
|
||||
</div>
|
||||
|
||||
|
@ -44,10 +44,10 @@
|
|||
<ul class="dropdown-menu">
|
||||
<!-- Username -->
|
||||
<li>
|
||||
<i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
|
||||
<strong>{{ $root.username }}</strong>
|
||||
<i18n-t v-if="socket.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
|
||||
<strong>{{ socket.username }}</strong>
|
||||
</i18n-t>
|
||||
<span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
|
||||
<span v-if="socket.username == null" class="dropdown-item-text">{{ t("signedInDispDisabled") }}</span>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
@ -56,26 +56,26 @@
|
|||
|
||||
<!--<li>
|
||||
<router-link to="/registry" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
||||
<font-awesome-icon icon="warehouse" /> {{ $t("registry") }}
|
||||
<font-awesome-icon icon="warehouse" /> {{ t("registry") }}
|
||||
</router-link>
|
||||
</li>-->
|
||||
|
||||
<li>
|
||||
<button class="dropdown-item" @click="scanFolder">
|
||||
<font-awesome-icon icon="arrows-rotate" /> {{ $t("scanFolder") }}
|
||||
<font-awesome-icon icon="arrows-rotate" /> {{ t("scanFolder") }}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||
<font-awesome-icon icon="cog" /> {{ t("Settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button class="dropdown-item" @click="$root.logout">
|
||||
<button class="dropdown-item" @click="socket.logout">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
{{ $t("Logout") }}
|
||||
{{ t("Logout") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -85,74 +85,53 @@
|
|||
</header>
|
||||
|
||||
<main>
|
||||
<div v-if="$root.socketIO.connecting" class="container mt-5">
|
||||
<h4>{{ $t("connecting...") }}</h4>
|
||||
<div v-if="socket.socketIO.value.connecting" class="container mt-5">
|
||||
<h4>{{ t("connecting...") }}</h4>
|
||||
</div>
|
||||
|
||||
<router-view v-if="$root.loggedIn" />
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
<router-view v-if="socket.loggedIn" />
|
||||
<Login v-if="! socket.loggedIn && socket.allowLoginDialog" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import Login from "../components/Login.vue";
|
||||
import { useSocket } from "../sockets";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { compareVersions } from "compare-versions";
|
||||
import { toastRes } from "../toast";
|
||||
import { useTheme } from "../theme";
|
||||
import { ALL_ENDPOINTS } from "../../../common/util-common";
|
||||
const socket = useSocket();
|
||||
const theme = useTheme();
|
||||
const { t } = useI18n();
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
Login,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
// Theme or Mobile
|
||||
classes() {
|
||||
const classes = {};
|
||||
classes[this.$root.theme] = true;
|
||||
classes["mobile"] = this.$root.isMobile;
|
||||
return classes;
|
||||
},
|
||||
|
||||
hasNewVersion() {
|
||||
if (this.$root.info.latestVersion && this.$root.info.version) {
|
||||
return compareVersions(this.$root.info.latestVersion, this.$root.info.version) >= 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
scanFolder() {
|
||||
this.$root.emitAgent(ALL_ENDPOINTS, "requestStackList", (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
// Theme or Mobile
|
||||
const classes = computed(() => {
|
||||
const classes = {};
|
||||
classes[theme.theme.value] = true;
|
||||
// classes["mobile"] = this.$root.isMobile;
|
||||
return classes;
|
||||
});
|
||||
|
||||
const hasNewVersion = computed(() => {
|
||||
if (socket.info.value.latestVersion && socket.info.value.version) {
|
||||
return compareVersions(socket.info.value.latestVersion, socket.info.value.version) >= 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const scanFolder = () => {
|
||||
socket.emitAgent(ALL_ENDPOINTS, "requestStackList", (res) => {
|
||||
toastRes(res);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log(socket.socketIO.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
// Dayjs init inside this, so it has to be the first import
|
||||
import "../../common/util-common";
|
||||
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import { createApp, defineComponent, h, provide, ref, watch } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { router } from "./router";
|
||||
import { FontAwesomeIcon } from "./icon.js";
|
||||
import { i18n } from "./i18n";
|
||||
import { changeLanguage, currentLocale, i18n } from "./i18n";
|
||||
|
||||
// Dependencies
|
||||
import "bootstrap";
|
||||
import Toast, { POSITION, useToast } from "vue-toastification";
|
||||
import Toast, { POSITION } from "vue-toastification";
|
||||
import "@xterm/xterm/lib/xterm.js";
|
||||
|
||||
// CSS
|
||||
|
@ -19,9 +19,10 @@ import "@xterm/xterm/css/xterm.css";
|
|||
import "./styles/main.scss";
|
||||
|
||||
// Minxins
|
||||
import socket from "./mixins/socket";
|
||||
import lang from "./mixins/lang";
|
||||
import theme from "./mixins/theme";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { setPageLocale } from "./util-frontend";
|
||||
import { SocketPlugin, useSocket } from "./sockets";
|
||||
import {theme, THEME_INJECT_KEY} from "./theme";
|
||||
|
||||
// Set Title
|
||||
document.title = document.title + " - " + location.host;
|
||||
|
@ -33,6 +34,7 @@ app.use(Toast, {
|
|||
showCloseButtonOnHover: true,
|
||||
});
|
||||
app.use(router);
|
||||
app.use(SocketPlugin);
|
||||
app.use(i18n);
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||
app.mount("#app");
|
||||
|
@ -41,64 +43,23 @@ app.mount("#app");
|
|||
* Root Vue component
|
||||
*/
|
||||
function rootApp() {
|
||||
const toast = useToast();
|
||||
|
||||
return defineComponent({
|
||||
mixins: [
|
||||
socket,
|
||||
lang,
|
||||
theme,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
loggedIn: false,
|
||||
allowLoginDialog: false,
|
||||
username: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const loggedIn = ref(false);
|
||||
const allowLoginDialog = ref(false);
|
||||
const username = ref(null);
|
||||
// Initialize Locale
|
||||
const locale = ref(currentLocale());
|
||||
changeLanguage(locale.value);
|
||||
provide("locale", locale);
|
||||
watch(locale, (val) => {
|
||||
changeLanguage(locale.value);
|
||||
});
|
||||
// Declare Theme
|
||||
provide(THEME_INJECT_KEY, theme());
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Show success or error toast dependant on response status code
|
||||
* @param {object} res Response object
|
||||
* @returns {void}
|
||||
*/
|
||||
toastRes(res) {
|
||||
let msg = res.msg;
|
||||
if (res.msgi18n) {
|
||||
if (msg != null && typeof msg === "object") {
|
||||
msg = this.$t(msg.key, msg.values);
|
||||
} else {
|
||||
msg = this.$t(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(msg);
|
||||
} else {
|
||||
toast.error(msg);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Show a success toast
|
||||
* @param {string} msg Message to show
|
||||
* @returns {void}
|
||||
*/
|
||||
toastSuccess(msg : string) {
|
||||
toast.success(this.$t(msg));
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an error toast
|
||||
* @param {string} msg Message to show
|
||||
* @returns {void}
|
||||
*/
|
||||
toastError(msg : string) {
|
||||
toast.error(this.$t(msg));
|
||||
},
|
||||
return { t };
|
||||
},
|
||||
render: () => h(App),
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { currentLocale } from "../i18n";
|
||||
import { setPageLocale } from "../util-frontend";
|
||||
import { defineComponent } from "vue";
|
||||
import {defineComponent, onBeforeMount, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
const langModules = import.meta.glob("../lang/*.json");
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -1,416 +1,436 @@
|
|||
import { io } from "socket.io-client";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { defineComponent } from "vue";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { AgentSocket } from "../../../common/agent-socket";
|
||||
|
||||
let socket : Socket;
|
||||
|
||||
let terminalMap : Map<string, Terminal> = new Map();
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
socketIO: {
|
||||
token: null,
|
||||
firstConnect: true,
|
||||
connected: false,
|
||||
connectCount: 0,
|
||||
initedSocketIO: false,
|
||||
connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
|
||||
showReverseProxyGuide: true,
|
||||
connecting: false,
|
||||
},
|
||||
info: {
|
||||
|
||||
},
|
||||
remember: (localStorage.remember !== "0"),
|
||||
loggedIn: false,
|
||||
allowLoginDialog: false,
|
||||
username: null,
|
||||
composeTemplate: "",
|
||||
|
||||
stackList: {},
|
||||
|
||||
// All stack list from all agents
|
||||
allAgentStackList: {} as Record<string, object>,
|
||||
|
||||
// online / offline / connecting
|
||||
agentStatusList: {
|
||||
|
||||
},
|
||||
|
||||
// Agent List
|
||||
agentList: {
|
||||
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
agentCount() {
|
||||
return Object.keys(this.agentList).length;
|
||||
},
|
||||
|
||||
completeStackList() {
|
||||
let list : Record<string, object> = {};
|
||||
|
||||
for (let stackName in this.stackList) {
|
||||
list[stackName + "_"] = this.stackList[stackName];
|
||||
}
|
||||
|
||||
for (let endpoint in this.allAgentStackList) {
|
||||
let instance = this.allAgentStackList[endpoint];
|
||||
for (let stackName in instance.stackList) {
|
||||
list[stackName + "_" + endpoint] = instance.stackList[stackName];
|
||||
}
|
||||
}
|
||||
return list;
|
||||
},
|
||||
|
||||
usernameFirstChar() {
|
||||
if (typeof this.username == "string" && this.username.length >= 1) {
|
||||
return this.username.charAt(0).toUpperCase();
|
||||
} else {
|
||||
return "🐬";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Frontend Version
|
||||
* It should be compiled to a static value while building the frontend.
|
||||
* Please see ./frontend/vite.config.ts, it is defined via vite.js
|
||||
* @returns {string}
|
||||
*/
|
||||
frontendVersion() {
|
||||
// eslint-disable-next-line no-undef
|
||||
return FRONTEND_VERSION;
|
||||
},
|
||||
|
||||
/**
|
||||
* Are both frontend and backend in the same version?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFrontendBackendVersionMatched() {
|
||||
if (!this.info.version) {
|
||||
return true;
|
||||
}
|
||||
return this.info.version === this.frontendVersion;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
"socketIO.connected"() {
|
||||
if (this.socketIO.connected) {
|
||||
this.agentStatusList[""] = "online";
|
||||
} else {
|
||||
this.agentStatusList[""] = "offline";
|
||||
}
|
||||
},
|
||||
|
||||
remember() {
|
||||
localStorage.remember = (this.remember) ? "1" : "0";
|
||||
},
|
||||
|
||||
// Reload the SPA if the server version is changed.
|
||||
"info.version"(to, from) {
|
||||
if (from && from !== to) {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initSocketIO();
|
||||
},
|
||||
mounted() {
|
||||
return;
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
endpointDisplayFunction(endpoint : string) {
|
||||
if (endpoint) {
|
||||
return endpoint;
|
||||
} else {
|
||||
return this.$t("currentEndpoint");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize connection to socket server
|
||||
* @param bypass Should the check for if we
|
||||
* are on a status page be bypassed?
|
||||
*/
|
||||
initSocketIO(bypass = false) {
|
||||
// No need to re-init
|
||||
if (this.socketIO.initedSocketIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socketIO.initedSocketIO = true;
|
||||
let url : string;
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
url = location.protocol + "//" + location.hostname + ":5001";
|
||||
} else {
|
||||
url = location.protocol + "//" + location.host;
|
||||
}
|
||||
|
||||
let connectingMsgTimeout = setTimeout(() => {
|
||||
this.socketIO.connecting = true;
|
||||
}, 1500);
|
||||
|
||||
socket = io(url);
|
||||
|
||||
// Handling events from agents
|
||||
let agentSocket = new AgentSocket();
|
||||
socket.on("agent", (eventName : unknown, ...args : unknown[]) => {
|
||||
agentSocket.call(eventName, ...args);
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to the socket server");
|
||||
|
||||
clearTimeout(connectingMsgTimeout);
|
||||
this.socketIO.connecting = false;
|
||||
|
||||
this.socketIO.connectCount++;
|
||||
this.socketIO.connected = true;
|
||||
this.socketIO.showReverseProxyGuide = false;
|
||||
const token = this.storage().token;
|
||||
|
||||
if (token) {
|
||||
if (token !== "autoLogin") {
|
||||
console.log("Logging in by token");
|
||||
this.loginByToken(token);
|
||||
} else {
|
||||
// Timeout if it is not actually auto login
|
||||
setTimeout(() => {
|
||||
if (! this.loggedIn) {
|
||||
this.allowLoginDialog = true;
|
||||
this.storage().removeItem("token");
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
this.allowLoginDialog = true;
|
||||
}
|
||||
|
||||
this.socketIO.firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect");
|
||||
this.socketIO.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
this.socketIO.connected = false;
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("reconnecting...")}`;
|
||||
this.socketIO.showReverseProxyGuide = true;
|
||||
this.socketIO.connected = false;
|
||||
this.socketIO.firstConnect = false;
|
||||
this.socketIO.connecting = false;
|
||||
});
|
||||
|
||||
// Custom Events
|
||||
|
||||
socket.on("info", (info) => {
|
||||
this.info = info;
|
||||
});
|
||||
|
||||
socket.on("autoLogin", () => {
|
||||
this.loggedIn = true;
|
||||
this.storage().token = "autoLogin";
|
||||
this.socketIO.token = "autoLogin";
|
||||
this.allowLoginDialog = false;
|
||||
this.afterLogin();
|
||||
});
|
||||
|
||||
socket.on("setup", () => {
|
||||
console.log("setup");
|
||||
this.$router.push("/setup");
|
||||
});
|
||||
|
||||
agentSocket.on("terminalWrite", (terminalName, data) => {
|
||||
const terminal = terminalMap.get(terminalName);
|
||||
if (!terminal) {
|
||||
//console.error("Terminal not found: " + terminalName);
|
||||
return;
|
||||
}
|
||||
terminal.write(data);
|
||||
});
|
||||
|
||||
agentSocket.on("stackList", (res) => {
|
||||
if (res.ok) {
|
||||
if (!res.endpoint) {
|
||||
this.stackList = res.stackList;
|
||||
} else {
|
||||
if (!this.allAgentStackList[res.endpoint]) {
|
||||
this.allAgentStackList[res.endpoint] = {
|
||||
stackList: {},
|
||||
};
|
||||
}
|
||||
this.allAgentStackList[res.endpoint].stackList = res.stackList;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("stackStatusList", (res) => {
|
||||
if (res.ok) {
|
||||
for (let stackName in res.stackStatusList) {
|
||||
const stackObj = this.stackList[stackName];
|
||||
if (stackObj) {
|
||||
stackObj.status = res.stackStatusList[stackName];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("agentStatus", (res) => {
|
||||
this.agentStatusList[res.endpoint] = res.status;
|
||||
|
||||
if (res.msg) {
|
||||
this.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("agentList", (res) => {
|
||||
console.log(res);
|
||||
if (res.ok) {
|
||||
this.agentList = res.agentList;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("refresh", () => {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The storage currently in use
|
||||
* @returns Current storage
|
||||
*/
|
||||
storage() : Storage {
|
||||
return (this.remember) ? localStorage : sessionStorage;
|
||||
},
|
||||
|
||||
getSocket() : Socket {
|
||||
return socket;
|
||||
},
|
||||
|
||||
emitAgent(endpoint : string, eventName : string, ...args : unknown[]) {
|
||||
this.getSocket().emit("agent", endpoint, eventName, ...args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get payload of JWT cookie
|
||||
* @returns {(object | undefined)} JWT payload
|
||||
*/
|
||||
getJWTPayload() {
|
||||
const jwtToken = this.storage().token;
|
||||
|
||||
if (jwtToken && jwtToken !== "autoLogin") {
|
||||
return jwtDecode(jwtToken);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send request to log user in
|
||||
* @param {string} username Username to log in with
|
||||
* @param {string} password Password to log in with
|
||||
* @param {string} token User token
|
||||
* @param {loginCB} callback Callback to call with result
|
||||
* @returns {void}
|
||||
*/
|
||||
login(username : string, password : string, token : string, callback) {
|
||||
this.getSocket().emit("login", {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
}, (res) => {
|
||||
if (res.tokenRequired) {
|
||||
callback(res);
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
this.storage().token = res.token;
|
||||
this.socketIO.token = res.token;
|
||||
this.loggedIn = true;
|
||||
this.username = this.getJWTPayload()?.username;
|
||||
|
||||
this.afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
||||
callback(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Log in using a token
|
||||
* @param {string} token Token to log in with
|
||||
* @returns {void}
|
||||
*/
|
||||
loginByToken(token : string) {
|
||||
socket.emit("loginByToken", token, (res) => {
|
||||
this.allowLoginDialog = true;
|
||||
|
||||
if (! res.ok) {
|
||||
this.logout();
|
||||
} else {
|
||||
this.loggedIn = true;
|
||||
this.username = this.getJWTPayload()?.username;
|
||||
this.afterLogin();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Log out of the web application
|
||||
* @returns {void}
|
||||
*/
|
||||
logout() {
|
||||
socket.emit("logout", () => { });
|
||||
this.storage().removeItem("token");
|
||||
this.socketIO.token = null;
|
||||
this.loggedIn = false;
|
||||
this.username = null;
|
||||
this.clearData();
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearData() {
|
||||
|
||||
},
|
||||
|
||||
afterLogin() {
|
||||
|
||||
},
|
||||
|
||||
bindTerminal(endpoint : string, terminalName : string, terminal : Terminal) {
|
||||
// Load terminal, get terminal screen
|
||||
this.emitAgent(endpoint, "terminalJoin", terminalName, (res) => {
|
||||
if (res.ok) {
|
||||
terminal.write(res.buffer);
|
||||
terminalMap.set(terminalName, terminal);
|
||||
} else {
|
||||
this.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unbindTerminal(terminalName : string) {
|
||||
terminalMap.delete(terminalName);
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
// import { io } from "socket.io-client";
|
||||
// import { Socket } from "socket.io-client";
|
||||
// import {defineComponent, ref} from "vue";
|
||||
// import jwtDecode from "jwt-decode";
|
||||
// import { Terminal } from "@xterm/xterm";
|
||||
// import { AgentSocket } from "../../../common/agent-socket";
|
||||
//
|
||||
// let socket : Socket;
|
||||
//
|
||||
// let terminalMap : Map<string, Terminal> = new Map();
|
||||
//
|
||||
// export default defineComponent({
|
||||
// data() {
|
||||
// return {
|
||||
// socketIO: {
|
||||
// token: null,
|
||||
// firstConnect: true,
|
||||
// connected: false,
|
||||
// connectCount: 0,
|
||||
// initedSocketIO: false,
|
||||
// connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
|
||||
// showReverseProxyGuide: true,
|
||||
// connecting: false,
|
||||
// },
|
||||
// info: {
|
||||
//
|
||||
// },
|
||||
// remember: (localStorage.remember !== "0"),
|
||||
// loggedIn: false,
|
||||
// allowLoginDialog: false,
|
||||
// username: null,
|
||||
// composeTemplate: "",
|
||||
//
|
||||
// stackList: {},
|
||||
//
|
||||
// // All stack list from all agents
|
||||
// allAgentStackList: {} as Record<string, object>,
|
||||
//
|
||||
// // online / offline / connecting
|
||||
// agentStatusList: {
|
||||
//
|
||||
// },
|
||||
//
|
||||
// // Agent List
|
||||
// agentList: {
|
||||
//
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// computed: {
|
||||
//
|
||||
// agentCount() {
|
||||
// return Object.keys(this.agentList).length;
|
||||
// },
|
||||
//
|
||||
// completeStackList() {
|
||||
// let list : Record<string, object> = {};
|
||||
//
|
||||
// for (let stackName in this.stackList) {
|
||||
// list[stackName + "_"] = this.stackList[stackName];
|
||||
// }
|
||||
//
|
||||
// for (let endpoint in this.allAgentStackList) {
|
||||
// let instance = this.allAgentStackList[endpoint];
|
||||
// for (let stackName in instance.stackList) {
|
||||
// list[stackName + "_" + endpoint] = instance.stackList[stackName];
|
||||
// }
|
||||
// }
|
||||
// return list;
|
||||
// },
|
||||
//
|
||||
// usernameFirstChar() {
|
||||
// if (typeof this.username == "string" && this.username.length >= 1) {
|
||||
// return this.username.charAt(0).toUpperCase();
|
||||
// } else {
|
||||
// return "🐬";
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Frontend Version
|
||||
// * It should be compiled to a static value while building the frontend.
|
||||
// * Please see ./frontend/vite.config.ts, it is defined via vite.js
|
||||
// * @returns {string}
|
||||
// */
|
||||
// frontendVersion() {
|
||||
// // eslint-disable-next-line no-undef
|
||||
// return FRONTEND_VERSION;
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Are both frontend and backend in the same version?
|
||||
// * @returns {boolean}
|
||||
// */
|
||||
// isFrontendBackendVersionMatched() {
|
||||
// if (!this.info.version) {
|
||||
// return true;
|
||||
// }
|
||||
// return this.info.version === this.frontendVersion;
|
||||
// },
|
||||
//
|
||||
// },
|
||||
// watch: {
|
||||
//
|
||||
// "socketIO.connected"() {
|
||||
// if (this.socketIO.connected) {
|
||||
// this.agentStatusList[""] = "online";
|
||||
// } else {
|
||||
// this.agentStatusList[""] = "offline";
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// remember() {
|
||||
// localStorage.remember = (this.remember) ? "1" : "0";
|
||||
// },
|
||||
//
|
||||
// // Reload the SPA if the server version is changed.
|
||||
// "info.version"(to, from) {
|
||||
// if (from && from !== to) {
|
||||
// window.location.reload();
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
// created() {
|
||||
// this.initSocketIO();
|
||||
// },
|
||||
// mounted() {
|
||||
// return;
|
||||
//
|
||||
// },
|
||||
// methods: {
|
||||
//
|
||||
// endpointDisplayFunction(endpoint : string) {
|
||||
// if (endpoint) {
|
||||
// return endpoint;
|
||||
// } else {
|
||||
// return this.$t("currentEndpoint");
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Initialize connection to socket server
|
||||
// * @param bypass Should the check for if we
|
||||
// * are on a status page be bypassed?
|
||||
// */
|
||||
// initSocketIO(bypass = false) {
|
||||
// console.log("INIT");
|
||||
// // No need to re-init
|
||||
// if (this.socketIO.initedSocketIO) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// this.socketIO.initedSocketIO = true;
|
||||
// let url : string;
|
||||
// const env = process.env.NODE_ENV || "production";
|
||||
// if (env === "development" || localStorage.dev === "dev") {
|
||||
// url = location.protocol + "//" + location.hostname + ":5001";
|
||||
// } else {
|
||||
// url = location.protocol + "//" + location.host;
|
||||
// }
|
||||
//
|
||||
// let connectingMsgTimeout = setTimeout(() => {
|
||||
// this.socketIO.connecting = true;
|
||||
// }, 1500);
|
||||
//
|
||||
// socket = io(url);
|
||||
//
|
||||
// // Handling events from agents
|
||||
// let agentSocket = new AgentSocket();
|
||||
// socket.on("agent", (eventName : unknown, ...args : unknown[]) => {
|
||||
// agentSocket.call(eventName, ...args);
|
||||
// });
|
||||
//
|
||||
// socket.on("connect", () => {
|
||||
// console.log("Connected to the socket server");
|
||||
//
|
||||
// clearTimeout(connectingMsgTimeout);
|
||||
// this.socketIO.connecting = false;
|
||||
//
|
||||
// this.socketIO.connectCount++;
|
||||
// this.socketIO.connected = true;
|
||||
// this.socketIO.showReverseProxyGuide = false;
|
||||
// const token = this.storage().token;
|
||||
//
|
||||
// if (token) {
|
||||
// if (token !== "autoLogin") {
|
||||
// console.log("Logging in by token");
|
||||
// this.loginByToken(token);
|
||||
// } else {
|
||||
// // Timeout if it is not actually auto login
|
||||
// setTimeout(() => {
|
||||
// if (! this.loggedIn) {
|
||||
// this.allowLoginDialog = true;
|
||||
// this.storage().removeItem("token");
|
||||
// }
|
||||
// }, 5000);
|
||||
// }
|
||||
// } else {
|
||||
// this.allowLoginDialog = true;
|
||||
// }
|
||||
//
|
||||
// this.socketIO.firstConnect = false;
|
||||
// });
|
||||
//
|
||||
// socket.on("disconnect", () => {
|
||||
// console.log("disconnect");
|
||||
// this.socketIO.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
// this.socketIO.connected = false;
|
||||
// });
|
||||
//
|
||||
// socket.on("connect_error", (err) => {
|
||||
// console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
// this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("reconnecting...")}`;
|
||||
// this.socketIO.showReverseProxyGuide = true;
|
||||
// this.socketIO.connected = false;
|
||||
// this.socketIO.firstConnect = false;
|
||||
// this.socketIO.connecting = false;
|
||||
// });
|
||||
//
|
||||
// // Custom Events
|
||||
//
|
||||
// socket.on("info", (info) => {
|
||||
// this.info = info;
|
||||
// });
|
||||
//
|
||||
// socket.on("autoLogin", () => {
|
||||
// this.loggedIn = true;
|
||||
// this.storage().token = "autoLogin";
|
||||
// this.socketIO.token = "autoLogin";
|
||||
// this.allowLoginDialog = false;
|
||||
// this.afterLogin();
|
||||
// });
|
||||
//
|
||||
// socket.on("setup", () => {
|
||||
// console.log("setup");
|
||||
// this.$router.push("/setup");
|
||||
// });
|
||||
//
|
||||
// agentSocket.on("terminalWrite", (terminalName, data) => {
|
||||
// const terminal = terminalMap.get(terminalName);
|
||||
// if (!terminal) {
|
||||
// //console.error("Terminal not found: " + terminalName);
|
||||
// return;
|
||||
// }
|
||||
// terminal.write(data);
|
||||
// });
|
||||
//
|
||||
// agentSocket.on("stackList", (res) => {
|
||||
// if (res.ok) {
|
||||
// if (!res.endpoint) {
|
||||
// this.stackList = res.stackList;
|
||||
// } else {
|
||||
// if (!this.allAgentStackList[res.endpoint]) {
|
||||
// this.allAgentStackList[res.endpoint] = {
|
||||
// stackList: {},
|
||||
// };
|
||||
// }
|
||||
// this.allAgentStackList[res.endpoint].stackList = res.stackList;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// socket.on("stackStatusList", (res) => {
|
||||
// if (res.ok) {
|
||||
// for (let stackName in res.stackStatusList) {
|
||||
// const stackObj = this.stackList[stackName];
|
||||
// if (stackObj) {
|
||||
// stackObj.status = res.stackStatusList[stackName];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// socket.on("agentStatus", (res) => {
|
||||
// this.agentStatusList[res.endpoint] = res.status;
|
||||
//
|
||||
// if (res.msg) {
|
||||
// this.toastError(res.msg);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// socket.on("agentList", (res) => {
|
||||
// console.log(res);
|
||||
// if (res.ok) {
|
||||
// this.agentList = res.agentList;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// socket.on("refresh", () => {
|
||||
// location.reload();
|
||||
// });
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * The storage currently in use
|
||||
// * @returns Current storage
|
||||
// */
|
||||
// storage() : Storage {
|
||||
// return (this.remember) ? localStorage : sessionStorage;
|
||||
// },
|
||||
//
|
||||
// getSocket() : Socket {
|
||||
// return socket;
|
||||
// },
|
||||
//
|
||||
// emitAgent(endpoint : string, eventName : string, ...args : unknown[]) {
|
||||
// this.getSocket().emit("agent", endpoint, eventName, ...args);
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Get payload of JWT cookie
|
||||
// * @returns {(object | undefined)} JWT payload
|
||||
// */
|
||||
// getJWTPayload() {
|
||||
// const jwtToken = this.storage().token;
|
||||
//
|
||||
// if (jwtToken && jwtToken !== "autoLogin") {
|
||||
// return jwtDecode(jwtToken);
|
||||
// }
|
||||
// return undefined;
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Send request to log user in
|
||||
// * @param {string} username Username to log in with
|
||||
// * @param {string} password Password to log in with
|
||||
// * @param {string} token User token
|
||||
// * @param {loginCB} callback Callback to call with result
|
||||
// * @returns {void}
|
||||
// */
|
||||
// login(username : string, password : string, token : string, callback) {
|
||||
// this.getSocket().emit("login", {
|
||||
// username,
|
||||
// password,
|
||||
// token,
|
||||
// }, (res) => {
|
||||
// if (res.tokenRequired) {
|
||||
// callback(res);
|
||||
// }
|
||||
//
|
||||
// if (res.ok) {
|
||||
// this.storage().token = res.token;
|
||||
// this.socketIO.token = res.token;
|
||||
// this.loggedIn = true;
|
||||
// this.username = this.getJWTPayload()?.username;
|
||||
//
|
||||
// this.afterLogin();
|
||||
//
|
||||
// // Trigger Chrome Save Password
|
||||
// history.pushState({}, "");
|
||||
// }
|
||||
//
|
||||
// callback(res);
|
||||
// });
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Log in using a token
|
||||
// * @param {string} token Token to log in with
|
||||
// * @returns {void}
|
||||
// */
|
||||
// loginByToken(token : string) {
|
||||
// socket.emit("loginByToken", token, (res) => {
|
||||
// this.allowLoginDialog = true;
|
||||
//
|
||||
// if (! res.ok) {
|
||||
// this.logout();
|
||||
// } else {
|
||||
// this.loggedIn = true;
|
||||
// this.username = this.getJWTPayload()?.username;
|
||||
// this.afterLogin();
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * Log out of the web application
|
||||
// * @returns {void}
|
||||
// */
|
||||
// logout() {
|
||||
// socket.emit("logout", () => { });
|
||||
// this.storage().removeItem("token");
|
||||
// this.socketIO.token = null;
|
||||
// this.loggedIn = false;
|
||||
// this.username = null;
|
||||
// this.clearData();
|
||||
// },
|
||||
//
|
||||
// /**
|
||||
// * @returns {void}
|
||||
// */
|
||||
// clearData() {
|
||||
//
|
||||
// },
|
||||
//
|
||||
// afterLogin() {
|
||||
//
|
||||
// },
|
||||
//
|
||||
// bindTerminal(endpoint : string, terminalName : string, terminal : Terminal) {
|
||||
// // Load terminal, get terminal screen
|
||||
// this.emitAgent(endpoint, "terminalJoin", terminalName, (res) => {
|
||||
// if (res.ok) {
|
||||
// terminal.write(res.buffer);
|
||||
// terminalMap.set(terminalName, terminal);
|
||||
// } else {
|
||||
// this.toastRes(res);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
//
|
||||
// unbindTerminal(terminalName : string) {
|
||||
// terminalMap.delete(terminalName);
|
||||
// },
|
||||
//
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// export function useSocket() {
|
||||
// const socketIO = ref({
|
||||
// token: null,
|
||||
// firstConnect: true,
|
||||
// connected: false,
|
||||
// connectCount: 0,
|
||||
// initedSocketIO: false,
|
||||
// connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
|
||||
// showReverseProxyGuide: true,
|
||||
// connecting: false,
|
||||
// });
|
||||
// const info = ref({});
|
||||
// const remember = ref((localStorage.remember !== "0"));
|
||||
// const loggedIn = ref(false);
|
||||
// const allowLoginDialog = ref(false);
|
||||
// const username = ref(null);
|
||||
// const composeTemplate = ref("");
|
||||
// }
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
|||
<transition ref="tableContainer" name="slide-fade" appear>
|
||||
<div v-if="$route.name === 'DashboardHome'">
|
||||
<h1 class="mb-3">
|
||||
{{ $t("home") }}
|
||||
{{ t("home") }}
|
||||
</h1>
|
||||
|
||||
<div class="row first-row">
|
||||
|
@ -12,78 +12,78 @@
|
|||
<div class="shadow-box big-padding text-center mb-4">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ $t("active") }}</h3>
|
||||
<h3>{{ t("active") }}</h3>
|
||||
<span class="num active">{{ activeNum }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("exited") }}</h3>
|
||||
<h3>{{ t("exited") }}</h3>
|
||||
<span class="num exited">{{ exitedNum }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("inactive") }}</h3>
|
||||
<h3>{{ t("inactive") }}</h3>
|
||||
<span class="num inactive">{{ inactiveNum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Run -->
|
||||
<h2 class="mb-3">{{ $t("Docker Run") }}</h2>
|
||||
<h2 class="mb-3">{{ t("Docker Run") }}</h2>
|
||||
<div class="mb-3">
|
||||
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn-normal btn mb-4" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
|
||||
<button class="btn-normal btn mb-4" @click="convertDockerRun">{{ t("Convert to Compose") }}</button>
|
||||
</div>
|
||||
<!-- Right -->
|
||||
<div class="col-md-5">
|
||||
<!-- Agent List -->
|
||||
<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">{{ t("dockgeAgent", 2) }} <span class="badge bg-warning" style="font-size: 12px;">beta</span></h4>
|
||||
|
||||
<div v-for="(agent, endpoint) in $root.agentList" :key="endpoint" class="mb-3 agent">
|
||||
<div v-for="(agent, endpoint) in socket.agentList.value" :key="endpoint" class="mb-3 agent">
|
||||
<!-- Agent Status -->
|
||||
<template v-if="$root.agentStatusList[endpoint]">
|
||||
<span v-if="$root.agentStatusList[endpoint] === 'online'" class="badge bg-primary me-2">{{ $t("agentOnline") }}</span>
|
||||
<span v-else-if="$root.agentStatusList[endpoint] === 'offline'" class="badge bg-danger me-2">{{ $t("agentOffline") }}</span>
|
||||
<span v-else class="badge bg-secondary me-2">{{ $t($root.agentStatusList[endpoint]) }}</span>
|
||||
<template v-if="socket.agentStatusList.value[endpoint]">
|
||||
<span v-if="socket.agentStatusList.value[endpoint] === 'online'" class="badge bg-primary me-2">{{ t("agentOnline") }}</span>
|
||||
<span v-else-if="socket.agentStatusList.value[endpoint] === 'offline'" class="badge bg-danger me-2">{{ t("agentOffline") }}</span>
|
||||
<span v-else class="badge bg-secondary me-2">{{ t(socket.agentStatusList.value[endpoint]) }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Agent Display Name -->
|
||||
<span v-if="endpoint === ''">{{ $t("currentEndpoint") }}</span>
|
||||
<span v-if="endpoint === ''">{{ t("currentEndpoint") }}</span>
|
||||
<a v-else :href="agent.url" target="_blank">{{ endpoint }}</a>
|
||||
|
||||
<!-- Remove Button -->
|
||||
<font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agent.url] = !showRemoveAgentDialog[agent.url]" />
|
||||
|
||||
<!-- Remoe Agent Dialog -->
|
||||
<BModal v-model="showRemoveAgentDialog[agent.url]" :okTitle="$t('removeAgent')" okVariant="danger" @ok="removeAgent(agent.url)">
|
||||
<BModal v-model="showRemoveAgentDialog[agent.url]" :okTitle="t('removeAgent')" okVariant="danger" @ok="removeAgent(agent.url)">
|
||||
<p>{{ agent.url }}</p>
|
||||
{{ $t("removeAgentMsg") }}
|
||||
{{ t("removeAgentMsg") }}
|
||||
</BModal>
|
||||
</div>
|
||||
|
||||
<button v-if="!showAgentForm" class="btn btn-normal" @click="showAgentForm = !showAgentForm">{{ $t("addAgent") }}</button>
|
||||
<button v-if="!showAgentForm" class="btn btn-normal" @click="showAgentForm = !showAgentForm">{{ t("addAgent") }}</button>
|
||||
|
||||
<!-- Add Agent Form -->
|
||||
<form v-if="showAgentForm" @submit.prevent="addAgent">
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">{{ $t("dockgeURL") }}</label>
|
||||
<label for="url" class="form-label">{{ t("dockgeURL") }}</label>
|
||||
<input id="url" v-model="agent.url" type="url" class="form-control" required placeholder="http://">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||
<label for="username" class="form-label">{{ t("Username") }}</label>
|
||||
<input id="username" v-model="agent.username" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||
<label for="password" class="form-label">{{ t("Password") }}</label>
|
||||
<input id="password" v-model="agent.password" type="password" class="form-control" required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" :disabled="connectingAgent">
|
||||
<template v-if="connectingAgent">{{ $t("connecting") }}</template>
|
||||
<template v-else>{{ $t("connect") }}</template>
|
||||
<template v-if="connectingAgent">{{ t("connecting") }}</template>
|
||||
<template v-else>{{ t("connect") }}</template>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -94,199 +94,191 @@
|
|||
<router-view ref="child" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { statusNameShort } from "../../../common/util-common";
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from "vue";
|
||||
import {useSocket} from "../sockets";
|
||||
import {toastRes} from "../toast";
|
||||
import {useRouter} from "vue-router";
|
||||
import {statusNameShort} from "../../../common/util-common";
|
||||
const { t } = useI18n();
|
||||
const socket = useSocket();
|
||||
const router = useRouter();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
withDefaults(defineProps<{
|
||||
type: number
|
||||
}>(), {
|
||||
type: 0
|
||||
});
|
||||
|
||||
},
|
||||
props: {
|
||||
calculatedHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
initialPerPage: 25,
|
||||
paginationConfig: {
|
||||
hideCount: true,
|
||||
chunksNavigation: "scroll",
|
||||
},
|
||||
importantHeartBeatListLength: 0,
|
||||
displayedRecords: [],
|
||||
dockerRunCommand: "",
|
||||
showAgentForm: false,
|
||||
showRemoveAgentDialog: {},
|
||||
connectingAgent: false,
|
||||
agent: {
|
||||
const tableContainer = ref();
|
||||
|
||||
const page = ref<number>(1);
|
||||
const perPage = ref<number>(25);
|
||||
const initialPerPage = ref<number>(25);
|
||||
const paginationConfig = ref({
|
||||
hideCount: true,
|
||||
chunksNavigation: "scroll",
|
||||
});
|
||||
const importantHeartBeatListLength = ref(0);
|
||||
const displayedRecords = ref([]);
|
||||
const dockerRunCommand = ref("");
|
||||
const showAgentForm = ref(false);
|
||||
const showRemoveAgentDialog = ref({});
|
||||
const connectingAgent = ref(false);
|
||||
const agent = ref({
|
||||
url: "http://",
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const activeNum = computed(() => {
|
||||
return getStatusNum("active");
|
||||
});
|
||||
|
||||
const inactiveNum = computed(() => {
|
||||
return getStatusNum("inactive");
|
||||
});
|
||||
|
||||
const exitedNum = computed(() => {
|
||||
return getStatusNum("exited");
|
||||
});
|
||||
|
||||
watch(perPage, () => {
|
||||
nextTick(() => {
|
||||
getImportantHeartbeatListPaged();
|
||||
});
|
||||
});
|
||||
|
||||
watch(page, () => {
|
||||
getImportantHeartbeatListPaged();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initialPerPage.value = perPage.value;
|
||||
|
||||
window.addEventListener("resize", updatePerPage);
|
||||
updatePerPage();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updatePerPage);
|
||||
});
|
||||
|
||||
const addAgent = () => {
|
||||
connectingAgent.value = true;
|
||||
socket.getSocket().emit("addAgent", agent.value, (res) => {
|
||||
toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
showAgentForm.value = false;
|
||||
agent.value = {
|
||||
url: "http://",
|
||||
username: "",
|
||||
password: "",
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computed: {
|
||||
activeNum() {
|
||||
return this.getStatusNum("active");
|
||||
},
|
||||
inactiveNum() {
|
||||
return this.getStatusNum("inactive");
|
||||
},
|
||||
exitedNum() {
|
||||
return this.getStatusNum("exited");
|
||||
},
|
||||
},
|
||||
connectingAgent.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch: {
|
||||
perPage() {
|
||||
this.$nextTick(() => {
|
||||
this.getImportantHeartbeatListPaged();
|
||||
});
|
||||
},
|
||||
const removeAgent = (url: string) => {
|
||||
socket.getSocket().emit("removeAgent", url, (res) => {
|
||||
if (res.ok) {
|
||||
toastRes(res);
|
||||
|
||||
page() {
|
||||
this.getImportantHeartbeatListPaged();
|
||||
},
|
||||
},
|
||||
let urlObj = new URL(url);
|
||||
let endpoint = urlObj.host;
|
||||
|
||||
mounted() {
|
||||
this.initialPerPage = this.perPage;
|
||||
// Remove the stack list and status list of the removed agent
|
||||
delete socket.allAgentStackList.value[endpoint];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", this.updatePerPage);
|
||||
this.updatePerPage();
|
||||
},
|
||||
const getStatusNum = (statusName: string) => {
|
||||
let num = 0;
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.updatePerPage);
|
||||
},
|
||||
for (let stackName in socket.completeStackList.value) {
|
||||
const stack = socket.completeStackList.value[stackName];
|
||||
if (statusNameShort(stack.status) === statusName) {
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
methods: {
|
||||
const convertDockerRun = () => {
|
||||
if (dockerRunCommand.value.trim() === "docker run") {
|
||||
throw new Error("Please enter a docker run command");
|
||||
}
|
||||
|
||||
addAgent() {
|
||||
this.connectingAgent = true;
|
||||
this.$root.getSocket().emit("addAgent", this.agent, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
// composerize is working in dev, but after "vite build", it is not working
|
||||
// So pass to backend to do the conversion
|
||||
socket.getSocket().emit("composerize", dockerRunCommand.value, (res) => {
|
||||
if (res.ok) {
|
||||
socket.composeTemplate = res.composeTemplate;
|
||||
router.push("/compose");
|
||||
} else {
|
||||
toastRes(res);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (res.ok) {
|
||||
this.showAgentForm = false;
|
||||
this.agent = {
|
||||
url: "http://",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Updates the displayed records when a new important heartbeat arrives.
|
||||
* @param {object} heartbeat - The heartbeat object received.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onNewImportantHeartbeat = (heartbeat: object): void => {
|
||||
if (page.value === 1) {
|
||||
displayedRecords.value.unshift(heartbeat);
|
||||
if (displayedRecords.value.length > perPage.value) {
|
||||
displayedRecords.value.pop();
|
||||
}
|
||||
importantHeartBeatListLength.value += 1;
|
||||
}
|
||||
};
|
||||
|
||||
this.connectingAgent = false;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Retrieves the length of the important heartbeat list for all monitors.
|
||||
* @returns {void}
|
||||
*/
|
||||
const getImportantHeartbeatListLength = (): void => {
|
||||
socket.getSocket().emit("monitorImportantHeartbeatListCount", null, (res) => {
|
||||
if (res.ok) {
|
||||
importantHeartBeatListLength.value = res.count;
|
||||
getImportantHeartbeatListPaged();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
removeAgent(url) {
|
||||
this.$root.getSocket().emit("removeAgent", url, (res) => {
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res);
|
||||
/**
|
||||
* Retrieves the important heartbeat list for the current page.
|
||||
* @returns {void}
|
||||
*/
|
||||
const getImportantHeartbeatListPaged = (): void => {
|
||||
const offset = (page.value - 1) * perPage.value;
|
||||
socket.getSocket().emit("monitorImportantHeartbeatListPaged", null, offset, perPage.value, (res) => {
|
||||
if (res.ok) {
|
||||
displayedRecords.value = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let urlObj = new URL(url);
|
||||
let endpoint = urlObj.host;
|
||||
/**
|
||||
* Updates the number of items shown per page based on the available height.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updatePerPage = (): void => {
|
||||
const tableContainerHeight = tableContainer.value?.offsetHeight;
|
||||
const availableHeight = window.innerHeight - tableContainerHeight;
|
||||
const additionalPerPage = Math.floor(availableHeight / 58);
|
||||
|
||||
// Remove the stack list and status list of the removed agent
|
||||
delete this.$root.allAgentStackList[endpoint];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getStatusNum(statusName) {
|
||||
let num = 0;
|
||||
|
||||
for (let stackName in this.$root.completeStackList) {
|
||||
const stack = this.$root.completeStackList[stackName];
|
||||
if (statusNameShort(stack.status) === statusName) {
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
},
|
||||
|
||||
convertDockerRun() {
|
||||
if (this.dockerRunCommand.trim() === "docker run") {
|
||||
throw new Error("Please enter a docker run command");
|
||||
}
|
||||
|
||||
// composerize is working in dev, but after "vite build", it is not working
|
||||
// So pass to backend to do the conversion
|
||||
this.$root.getSocket().emit("composerize", this.dockerRunCommand, (res) => {
|
||||
if (res.ok) {
|
||||
this.$root.composeTemplate = res.composeTemplate;
|
||||
this.$router.push("/compose");
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the displayed records when a new important heartbeat arrives.
|
||||
* @param {object} heartbeat - The heartbeat object received.
|
||||
* @returns {void}
|
||||
*/
|
||||
onNewImportantHeartbeat(heartbeat) {
|
||||
if (this.page === 1) {
|
||||
this.displayedRecords.unshift(heartbeat);
|
||||
if (this.displayedRecords.length > this.perPage) {
|
||||
this.displayedRecords.pop();
|
||||
}
|
||||
this.importantHeartBeatListLength += 1;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the length of the important heartbeat list for all monitors.
|
||||
* @returns {void}
|
||||
*/
|
||||
getImportantHeartbeatListLength() {
|
||||
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", null, (res) => {
|
||||
if (res.ok) {
|
||||
this.importantHeartBeatListLength = res.count;
|
||||
this.getImportantHeartbeatListPaged();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the important heartbeat list for the current page.
|
||||
* @returns {void}
|
||||
*/
|
||||
getImportantHeartbeatListPaged() {
|
||||
const offset = (this.page - 1) * this.perPage;
|
||||
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", null, offset, this.perPage, (res) => {
|
||||
if (res.ok) {
|
||||
this.displayedRecords = res.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the number of items shown per page based on the available height.
|
||||
* @returns {void}
|
||||
*/
|
||||
updatePerPage() {
|
||||
const tableContainer = this.$refs.tableContainer;
|
||||
const tableContainerHeight = tableContainer.offsetHeight;
|
||||
const availableHeight = window.innerHeight - tableContainerHeight;
|
||||
const additionalPerPage = Math.floor(availableHeight / 58);
|
||||
|
||||
if (additionalPerPage > 0) {
|
||||
this.perPage = Math.max(this.initialPerPage, this.perPage + additionalPerPage);
|
||||
} else {
|
||||
this.perPage = this.initialPerPage;
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
if (additionalPerPage > 0) {
|
||||
perPage.value = Math.max(initialPerPage.value, perPage.value + additionalPerPage);
|
||||
} else {
|
||||
perPage.value = initialPerPage.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,121 +1,128 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { log } from "../../../backend/log";
|
||||
<script setup lang="ts">
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
let lastSelectTime = 0;
|
||||
|
||||
export default defineComponent({
|
||||
name: "Files",
|
||||
components: { FontAwesomeIcon },
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
selected: [],
|
||||
currentPath: "/",
|
||||
newFileData: {
|
||||
isFolder: false,
|
||||
fileName: "",
|
||||
},
|
||||
stack: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stackName() {
|
||||
return this.$route.params.stackName;
|
||||
},
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import {useSocket} from "../sockets";
|
||||
import {toastError, toastRes, toastSuccess} from "../toast";
|
||||
import FileEditor from "../components/FileEditor.vue";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
endpoint() {
|
||||
return this.stack.endpoint || this.$route.params.endpoint || "";
|
||||
},
|
||||
// Use
|
||||
const route = useRoute();
|
||||
const socket = useSocket();
|
||||
|
||||
newFileLabel() {
|
||||
return this.newFileData.isFolder ? {
|
||||
title: "newFolder",
|
||||
placeholder: "folderName",
|
||||
} : {
|
||||
title: "newFile",
|
||||
placeholder: "fileName",
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDir("/");
|
||||
},
|
||||
methods: {
|
||||
select(ev, file) {
|
||||
const isMac = window.navigator.userAgent.toLowerCase().includes("mac");
|
||||
if (isMac ? ev.metaKey : ev.ctrlKey) {
|
||||
// 多选
|
||||
if (this.selected.includes(file.name)) {
|
||||
this.selected = this.selected.filter((f) => f !== file.name);
|
||||
} else {
|
||||
this.selected.push(file.name);
|
||||
}
|
||||
} else {
|
||||
// 单选
|
||||
if (this.selected.length === 1 && file.name === this.selected[0] && Date.now() - lastSelectTime < 1000) {
|
||||
console.log("Double Click");
|
||||
this.open(file);
|
||||
}
|
||||
this.selected = [ file.name ];
|
||||
lastSelectTime = Date.now();
|
||||
}
|
||||
},
|
||||
// Refs
|
||||
const fileEditor = ref<FileEditor>();
|
||||
const createFileModal = ref<Confirm>();
|
||||
|
||||
open(file) {
|
||||
console.log(file);
|
||||
if (file.folder) {
|
||||
// Open Folder
|
||||
this.loadDir(file.path);
|
||||
} else {
|
||||
// Open File
|
||||
this.$refs.fileEditor.open(file);
|
||||
}
|
||||
},
|
||||
// Reactive variables
|
||||
const files = ref([]);
|
||||
const selected = ref<string[]>([]);
|
||||
const currentPath = ref<string>("/");
|
||||
const newFileData = ref<{ isFolder: boolean; fileName: string }>({
|
||||
isFolder: false,
|
||||
fileName: "",
|
||||
});
|
||||
const stack = ref<Record<string, object>>({});
|
||||
|
||||
loadDir(dir: string) {
|
||||
this.$root.emitAgent(this.endpoint, "listDir", this.stackName, dir, (res) => {
|
||||
console.log(res.files);
|
||||
this.currentPath = dir;
|
||||
this.files = res.files;
|
||||
});
|
||||
},
|
||||
// Computed properties
|
||||
const stackName = computed(() => {
|
||||
return route.params.stackName;
|
||||
});
|
||||
|
||||
openNewFileModel(isFolder: boolean = false) {
|
||||
this.newFileData.isFolder = isFolder;
|
||||
this.newFileData.fileName = "";
|
||||
this.newFileData.fileName = "";
|
||||
this.$refs.createFileModal.show();
|
||||
},
|
||||
const endpoint = computed(() => {
|
||||
return stack.value.endpoint || route.params.endpoint || "";
|
||||
});
|
||||
|
||||
newFile() {
|
||||
// Check file
|
||||
const filter = this.files.filter(f => f.name === this.newFileData.fileName);
|
||||
if (filter.length > 0) {
|
||||
this.$root.toastError("File Exist");
|
||||
return;
|
||||
}
|
||||
this.$root.emitAgent(this.endpoint, "createFile", this.stackName, this.currentPath, this.newFileData.isFolder
|
||||
, this.newFileData.fileName, (res) => {
|
||||
if (res.ok) {
|
||||
this.$root.toastSuccess("Created");
|
||||
this.loadDir(this.currentPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadStack() {
|
||||
this.processing = true;
|
||||
this.$root.emitAgent(this.endpoint, "getStack", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.stack = res.stack;
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
})
|
||||
const newFileLabel = computed(() => {
|
||||
return newFileData.value.isFolder ? {
|
||||
title: "newFolder",
|
||||
placeholder: "folderName",
|
||||
} : {
|
||||
title: "newFile",
|
||||
placeholder: "fileName",
|
||||
};
|
||||
});
|
||||
|
||||
// Methods
|
||||
const select = (ev: MouseEvent, file) => {
|
||||
const isMac = window.navigator.userAgent.toLowerCase().includes("mac");
|
||||
if (isMac ? ev.metaKey : ev.ctrlKey) {
|
||||
// Multiple selection
|
||||
if (selected.value.includes(file.name)) {
|
||||
selected.value = selected.value.filter((f) => f !== file.name);
|
||||
} else {
|
||||
selected.value.push(file.name);
|
||||
}
|
||||
},
|
||||
} else {
|
||||
// Single selection
|
||||
if (selected.value.length === 1 && file.name === selected.value[0] && Date.now() - lastSelectTime < 1000) {
|
||||
console.log("Double Click");
|
||||
open(file);
|
||||
}
|
||||
selected.value = [file.name];
|
||||
lastSelectTime = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
const open = (file) => {
|
||||
console.log(file);
|
||||
if (file.folder) {
|
||||
// Open Folder
|
||||
loadDir(file.path);
|
||||
} else {
|
||||
// Open File
|
||||
fileEditor.value?.open(file);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDir = (dir: string) => {
|
||||
socket.emitAgent(endpoint.value, "listDir", stackName.value, dir, (res: { files: any[] }) => {
|
||||
console.log(res.files);
|
||||
currentPath.value = dir;
|
||||
files.value = res.files;
|
||||
});
|
||||
};
|
||||
|
||||
const openNewFileModel = (isFolder = false) => {
|
||||
newFileData.value.isFolder = isFolder;
|
||||
newFileData.value.fileName = "";
|
||||
newFileData.value.fileName = "";
|
||||
createFileModal.value?.show();
|
||||
};
|
||||
|
||||
const newFile = () => {
|
||||
// Check file existence
|
||||
const filter = files.value.filter(f => f.name === newFileData.value.fileName);
|
||||
if (filter.length > 0) {
|
||||
toastError("File Exist");
|
||||
return;
|
||||
}
|
||||
socket.emitAgent(endpoint.value, "createFile", stackName.value, currentPath.value, newFileData.value.isFolder
|
||||
, newFileData.value.fileName, (res: { ok: boolean }) => {
|
||||
if (res.ok) {
|
||||
toastSuccess("Created");
|
||||
loadDir(currentPath.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadStack = () => {
|
||||
processing.value = true;
|
||||
socket.emitAgent(endpoint.value, "getStack", stack.value.name, (res: { ok: boolean; stack: Record<string, any> }) => {
|
||||
if (res.ok) {
|
||||
stack.value = res.stack;
|
||||
} else {
|
||||
toastRes(res);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Lifecycle hook
|
||||
onMounted(() => {
|
||||
loadDir("/");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
|
||||
import Layout from "./layouts/Layout.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
|
|
|
@ -0,0 +1,417 @@
|
|||
import { App, computed, ComputedRef, getCurrentInstance, inject, Ref, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { io } from "socket.io-client";
|
||||
import { AgentSocket } from "../../common/agent-socket";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { useRouter } from "vue-router";
|
||||
import { toastError, toastRes } from "./toast";
|
||||
import { i18n } from "./i18n";
|
||||
|
||||
let socket : Socket;
|
||||
|
||||
let terminalMap : Map<string, Terminal> = new Map();
|
||||
|
||||
export const SocketPlugin = {
|
||||
install(app: App) {
|
||||
app.provide("socket", Socket());
|
||||
}
|
||||
};
|
||||
|
||||
export const Socket = () => {
|
||||
const { t } = i18n.global;
|
||||
const socketIO = ref({
|
||||
token: null,
|
||||
firstConnect: true,
|
||||
connected: false,
|
||||
connectCount: 0,
|
||||
initedSocketIO: false,
|
||||
connectionErrorMsg: `${t("Cannot connect to the socket server.")} ${t("Reconnecting...")}`,
|
||||
showReverseProxyGuide: true,
|
||||
connecting: false,
|
||||
});
|
||||
const info = ref({});
|
||||
const remember = ref(localStorage.remember !== "0");
|
||||
const loggedIn = ref(false);
|
||||
const allowLoginDialog = ref(false);
|
||||
const username = ref(null);
|
||||
const composeTemplate = ref("");
|
||||
const stackList = ref<Record<string, object>>({});
|
||||
// All stack list from all agents
|
||||
const allAgentStackList = ref<Record<string, object>>({});
|
||||
// online / offline / connecting
|
||||
const agentStatusList = ref<Record<string, string>>({});
|
||||
// Agent List
|
||||
const agentList = ref<Record<string, object>>({});
|
||||
|
||||
/** COMPUTE **/
|
||||
const agentCount = computed(() => {
|
||||
return Object.keys(agentList.value).length;
|
||||
});
|
||||
|
||||
const completeStackList = computed(() => {
|
||||
let list: Record<string, object> = {};
|
||||
|
||||
for (let stackName in stackList.value) {
|
||||
list[stackName + "_"] = stackList.value[stackName];
|
||||
}
|
||||
|
||||
for (let endpoint in allAgentStackList.value) {
|
||||
let instance = allAgentStackList.value[endpoint];
|
||||
for (let stackName in instance.stackList) {
|
||||
list[stackName + "_" + endpoint] = instance.stackList[stackName];
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const usernameFirstChar = computed(() => {
|
||||
if (typeof username.value == "string" && username.value.length >= 1) {
|
||||
return username.value.charAt(0).toUpperCase();
|
||||
} else {
|
||||
return "🐬";
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Frontend Version
|
||||
* It should be compiled to a static value while building the frontend.
|
||||
* Please see ./frontend/vite.config.ts, it is defined via vite.js
|
||||
* @returns {string}
|
||||
*/
|
||||
const frontendVersion = computed(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
return FRONTEND_VERSION;
|
||||
});
|
||||
|
||||
/**
|
||||
* Are both frontend and backend in the same version?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isFrontendBackendVersionMatched = computed(() => {
|
||||
if (!info.value.version) {
|
||||
return true;
|
||||
}
|
||||
return info.value.version === frontendVersion.value;
|
||||
});
|
||||
|
||||
// WATCH
|
||||
|
||||
watch(() => socketIO.value.connected, () => {
|
||||
if (socketIO.value.connected) {
|
||||
agentStatusList.value[""] = "online";
|
||||
} else {
|
||||
agentStatusList.value[""] = "offline";
|
||||
}
|
||||
});
|
||||
|
||||
watch(remember, () => {
|
||||
localStorage.remember = (remember.value) ? "1" : "0";
|
||||
});
|
||||
|
||||
watch(() => info.value.version, (to, from) => {
|
||||
if (from && from !== to) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
const endpointDisplayFunction = (endpoint : string) => {
|
||||
if (endpoint) {
|
||||
return endpoint;
|
||||
} else {
|
||||
return t("currentEndpoint");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize connection to socket server
|
||||
* @param bypass Should the check for if we
|
||||
* are on a status page be bypassed?
|
||||
*/
|
||||
const initSocketIO = (bypass = false) => {
|
||||
// No need to re-init
|
||||
if (socketIO.value.initedSocketIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
socketIO.value.initedSocketIO = true;
|
||||
let url : string;
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
url = location.protocol + "//" + location.hostname + ":5001";
|
||||
} else {
|
||||
url = location.protocol + "//" + location.host;
|
||||
}
|
||||
|
||||
let connectingMsgTimeout = setTimeout(() => {
|
||||
socketIO.value.connecting = true;
|
||||
}, 1500);
|
||||
|
||||
socket = io(url);
|
||||
|
||||
// Handling events from agents
|
||||
let agentSocket = new AgentSocket();
|
||||
socket.on("agent", (eventName : unknown, ...args : unknown[]) => {
|
||||
agentSocket.call(eventName, ...args);
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to the socket server");
|
||||
|
||||
clearTimeout(connectingMsgTimeout);
|
||||
socketIO.value.connecting = false;
|
||||
socketIO.value.connectCount++;
|
||||
socketIO.value.connected = true;
|
||||
socketIO.value.showReverseProxyGuide = false;
|
||||
const token = storage().token;
|
||||
|
||||
if (token) {
|
||||
if (token !== "autoLogin") {
|
||||
console.log("Logging in by token");
|
||||
loginByToken(token);
|
||||
} else {
|
||||
// Timeout if it is not actually auto login
|
||||
setTimeout(() => {
|
||||
if (! loggedIn.value) {
|
||||
allowLoginDialog.value = true;
|
||||
storage().removeItem("token");
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
allowLoginDialog.value = true;
|
||||
}
|
||||
|
||||
socketIO.value.firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect");
|
||||
socketIO.value.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
socketIO.value.connected = false;
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
socketIO.value.connectionErrorMsg = `${t("Cannot connect to the socket server.")} [${err}] ${t("reconnecting...")}`;
|
||||
socketIO.value.showReverseProxyGuide = true;
|
||||
socketIO.value.connected = false;
|
||||
socketIO.value.firstConnect = false;
|
||||
socketIO.value.connecting = false;
|
||||
});
|
||||
|
||||
// Custom Events
|
||||
|
||||
socket.on("info", (i) => {
|
||||
info.value = i;
|
||||
});
|
||||
|
||||
socket.on("autoLogin", () => {
|
||||
loggedIn.value = true;
|
||||
storage().token = "autoLogin";
|
||||
socketIO.value.token = "autoLogin";
|
||||
allowLoginDialog.value = false;
|
||||
afterLogin();
|
||||
});
|
||||
|
||||
socket.on("setup", () => {
|
||||
console.log("setup");
|
||||
useRouter().push("/setup");
|
||||
});
|
||||
|
||||
agentSocket.on("terminalWrite", (terminalName, data) => {
|
||||
const terminal = terminalMap.get(terminalName);
|
||||
if (!terminal) {
|
||||
//console.error("Terminal not found: " + terminalName);
|
||||
return;
|
||||
}
|
||||
terminal.write(data);
|
||||
});
|
||||
|
||||
agentSocket.on("stackList", (res) => {
|
||||
if (res.ok) {
|
||||
if (!res.endpoint) {
|
||||
stackList.value = res.stackList;
|
||||
} else {
|
||||
if (!allAgentStackList.value[res.endpoint]) {
|
||||
allAgentStackList.value[res.endpoint] = {
|
||||
stackList: {},
|
||||
};
|
||||
}
|
||||
allAgentStackList.value[res.endpoint].stackList = res.stackList;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("stackStatusList", (res) => {
|
||||
if (res.ok) {
|
||||
for (let stackName in res.stackStatusList) {
|
||||
const stackObj = stackList.value[stackName];
|
||||
if (stackObj) {
|
||||
stackObj.status = res.stackStatusList[stackName];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("agentStatus", (res) => {
|
||||
agentStatusList.value[res.endpoint] = res.status;
|
||||
|
||||
if (res.msg) {
|
||||
toastError(res.msg);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("agentList", (res) => {
|
||||
console.log(res);
|
||||
if (res.ok) {
|
||||
agentList.value = res.agentList;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("refresh", () => {
|
||||
location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The storage currently in use
|
||||
* @returns Current storage
|
||||
*/
|
||||
const storage = () : Storage => {
|
||||
return (remember.value) ? localStorage : sessionStorage;
|
||||
};
|
||||
|
||||
const getSocket = () : Socket => {
|
||||
return socket;
|
||||
};
|
||||
|
||||
const emitAgent = (endpoint : string | undefined, eventName : string, ...args : unknown[]) => {
|
||||
if (!endpoint) {
|
||||
return;
|
||||
}
|
||||
getSocket().emit("agent", endpoint, eventName, ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get payload of JWT cookie
|
||||
* @returns {(object | undefined)} JWT payload
|
||||
*/
|
||||
const getJWTPayload = () => {
|
||||
const jwtToken = storage().token;
|
||||
|
||||
if (jwtToken && jwtToken !== "autoLogin") {
|
||||
return jwtDecode(jwtToken);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send request to log user in
|
||||
* @param {string} username Username to log in with
|
||||
* @param {string} password Password to log in with
|
||||
* @param {string} token User token
|
||||
* @param {loginCB} callback Callback to call with result
|
||||
* @returns {void}
|
||||
*/
|
||||
const login = (username : string, password : string, token : string, callback) => {
|
||||
getSocket().emit("login", {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
}, (res) => {
|
||||
if (res.tokenRequired) {
|
||||
callback(res);
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
storage().token = res.token;
|
||||
socketIO.value.token = res.token;
|
||||
loggedIn.value = true;
|
||||
username = getJWTPayload()?.username;
|
||||
afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
||||
callback(res);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Log in using a token
|
||||
* @param {string} token Token to log in with
|
||||
* @returns {void}
|
||||
*/
|
||||
const loginByToken = (token : string) => {
|
||||
socket.emit("loginByToken", token, (res) => {
|
||||
allowLoginDialog.value = true;
|
||||
|
||||
if (!res.ok) {
|
||||
logout();
|
||||
} else {
|
||||
loggedIn.value = true;
|
||||
username.value = getJWTPayload()?.username;
|
||||
afterLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Log out of the web application
|
||||
*/
|
||||
const logout = () => {
|
||||
socket.emit("logout", () => { });
|
||||
storage().removeItem("token");
|
||||
socketIO.value.token = null;
|
||||
loggedIn.value = false;
|
||||
username.value = null;
|
||||
clearData();
|
||||
};
|
||||
|
||||
const clearData = () => {};
|
||||
|
||||
const afterLogin = () => {};
|
||||
|
||||
const bindTerminal = (endpoint : string, terminalName : string, terminal : Terminal) => {
|
||||
// Load terminal, get terminal screen
|
||||
emitAgent(endpoint, "terminalJoin", terminalName, (res) => {
|
||||
if (res.ok) {
|
||||
terminal.write(res.buffer);
|
||||
terminalMap.set(terminalName, terminal);
|
||||
} else {
|
||||
toastRes(res);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const unbindTerminal = (terminalName : string) => {
|
||||
terminalMap.delete(terminalName);
|
||||
};
|
||||
|
||||
initSocketIO();
|
||||
|
||||
return {
|
||||
socketIO,
|
||||
loggedIn,
|
||||
username,
|
||||
composeTemplate,
|
||||
allowLoginDialog,
|
||||
usernameFirstChar,
|
||||
completeStackList,
|
||||
stackList,
|
||||
agentCount,
|
||||
allAgentStackList,
|
||||
agentStatusList,
|
||||
agentList,
|
||||
info,
|
||||
endpointDisplayFunction,
|
||||
getSocket,
|
||||
logout,
|
||||
emitAgent
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export const useSocket = (): ReturnType<typeof Socket> => inject("socket") as ReturnType<typeof Socket>;
|
|
@ -0,0 +1,69 @@
|
|||
import {computed, inject, onMounted, ref, watch} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export declare type SystemTheme = "dark" | "light" | "auto"
|
||||
|
||||
export const THEME_INJECT_KEY = "theme";
|
||||
|
||||
export const theme = () => {
|
||||
const system = ref<SystemTheme>((window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light");
|
||||
const userTheme = ref<SystemTheme>(localStorage.theme);
|
||||
const statusPageTheme = ref<SystemTheme>("light");
|
||||
const forceStatusPageTheme = ref<boolean>(false);
|
||||
const path = ref<string>("");
|
||||
|
||||
const theme = computed(() => {
|
||||
if (userTheme.value === "auto") {
|
||||
return system.value;
|
||||
}
|
||||
return userTheme.value;
|
||||
});
|
||||
|
||||
const isDark = computed(() => {
|
||||
return theme.value === "dark";
|
||||
});
|
||||
|
||||
const updateThemeColorMeta = () => {
|
||||
if (theme.value === "dark") {
|
||||
document.querySelector("#theme-color")?.setAttribute("content", "#161B22");
|
||||
} else {
|
||||
document.querySelector("#theme-color")?.setAttribute("content", "#5cdd8b");
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => useRoute()?.fullPath, (routePath) => {
|
||||
path.value = routePath;
|
||||
});
|
||||
|
||||
watch(userTheme, (val) => {
|
||||
localStorage.theme = val;
|
||||
});
|
||||
|
||||
watch(theme, (to, from) => {
|
||||
document.body.classList.remove(from);
|
||||
document.body.classList.add(theme.value);
|
||||
updateThemeColorMeta();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (! userTheme.value) {
|
||||
userTheme.value = "dark";
|
||||
}
|
||||
|
||||
document.body.classList.add(theme.value);
|
||||
updateThemeColorMeta();
|
||||
});
|
||||
|
||||
return {
|
||||
system,
|
||||
userTheme,
|
||||
statusPageTheme,
|
||||
forceStatusPageTheme,
|
||||
path,
|
||||
theme,
|
||||
isDark,
|
||||
updateThemeColorMeta
|
||||
};
|
||||
};
|
||||
|
||||
export const useTheme = (): ReturnType<typeof theme> => inject(THEME_INJECT_KEY) as ReturnType<typeof theme>;
|
|
@ -0,0 +1,38 @@
|
|||
import {useToast} from "vue-toastification";
|
||||
import {i18n} from "./i18n";
|
||||
|
||||
const toast = useToast();
|
||||
const {t} = i18n.global;
|
||||
|
||||
export const toastRes = (res: unknown) => {
|
||||
if (typeof res !== "object") {
|
||||
return;
|
||||
}
|
||||
let msg = res.msg;
|
||||
if (res.msgi18n) {
|
||||
if (msg != null && typeof msg === "object") {
|
||||
msg = t(msg.key, msg.values);
|
||||
} else {
|
||||
msg = t(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(msg);
|
||||
} else {
|
||||
toast.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
export const toastSuccess = (msg: string) => {
|
||||
toast.success(t(msg));
|
||||
};
|
||||
|
||||
/**
|
||||
* Show an error toast
|
||||
* @param {string} msg Message to show
|
||||
* @returns {void}
|
||||
*/
|
||||
export const toastError = (msg: string) => {
|
||||
toast.error(t(msg));
|
||||
};
|
|
@ -1,13 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"backend/**/*",
|
||||
"common/**/*"
|
||||
"common/**/*",
|
||||
"frontend/**/*"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue