fix28: полный production-фикс графиков, machine-auth, i18n и peer import/export
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
title="Managed by hy2xs-orchestrator reconfigure"
|
||||
:title="$t('config.orchestratorManaged')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px"
|
||||
@@ -205,7 +205,7 @@ const dataFormRules = {
|
||||
},
|
||||
{
|
||||
pattern: /^\d+$/,
|
||||
message: "field must be a integer",
|
||||
message: t("config.mustBeInteger"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
@@ -217,7 +217,7 @@ const dataFormRules = {
|
||||
},
|
||||
{
|
||||
pattern: /^\/([a-z0-9]+(\/[a-z0-9]+)*)?$/,
|
||||
message: "field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
|
||||
message: t("config.invalidWebContext"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
@@ -229,7 +229,7 @@ const dataFormRules = {
|
||||
},
|
||||
{
|
||||
pattern: /^\d+(\.\d)?$/,
|
||||
message: "field must be a number with up to one decimal place",
|
||||
message: t("config.invalidTrafficTime"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -160,11 +160,16 @@ const loadDashboard = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const xAxisLabels = computed(() => {
|
||||
const trafficXAxisLabels = computed(() => {
|
||||
const allTs = new Set<number>();
|
||||
for (const item of timeseries.value.traffic || []) {
|
||||
allTs.add(item.ts);
|
||||
}
|
||||
return Array.from(allTs).sort((a, b) => a - b);
|
||||
});
|
||||
|
||||
const systemXAxisLabels = computed(() => {
|
||||
const allTs = new Set<number>();
|
||||
for (const item of timeseries.value.system || []) {
|
||||
allTs.add(item.ts);
|
||||
}
|
||||
@@ -199,7 +204,7 @@ const trafficChartOption = computed(() => ({
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||
data: trafficXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
@@ -213,14 +218,14 @@ const trafficChartOption = computed(() => ({
|
||||
type: "line",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
|
||||
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
|
||||
},
|
||||
{
|
||||
name: t("dashboard.upload"),
|
||||
type: "line",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
|
||||
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -237,7 +242,7 @@ const systemChartOption = computed(() => ({
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||
data: systemXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
@@ -251,14 +256,14 @@ const systemChartOption = computed(() => ({
|
||||
type: "line",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
|
||||
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
|
||||
},
|
||||
{
|
||||
name: t("dashboard.ram"),
|
||||
type: "line",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
|
||||
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -8,6 +8,9 @@ export default {
|
||||
<script setup lang="ts">
|
||||
import { reactive, toRefs } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
|
||||
@@ -24,15 +27,15 @@ function back() {
|
||||
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-button class="pan-back-btn" @click="back">Back</el-button>
|
||||
<el-button class="pan-back-btn" @click="back">{{ $t("errorPage.back") }}</el-button>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">Oops!</h1>
|
||||
<h2>You do not have permission to access this page</h2>
|
||||
<h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1>
|
||||
<h2>{{ $t("errorPage.forbiddenTitle") }}</h2>
|
||||
<ul class="list-unstyled">
|
||||
<li>Or you can go:</li>
|
||||
<li>{{ $t("errorPage.forbiddenGo") }}</li>
|
||||
<li class="link-type">
|
||||
<router-link to="/">Back to Home</router-link>
|
||||
<router-link to="/">{{ $t("errorPage.backHome") }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</el-col>
|
||||
@@ -41,7 +44,7 @@ function back() {
|
||||
:src="errGif"
|
||||
width="313"
|
||||
height="428"
|
||||
alt="Girl has dropped her ice cream."
|
||||
:alt="t('errorPage.forbiddenImageAlt')"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -6,8 +6,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function message() {
|
||||
return "The webmaster said that you can not enter this page...";
|
||||
return t("errorPage.notFoundHeadline");
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,13 +41,12 @@ function message() {
|
||||
/>
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">OOPS!</div>
|
||||
<div class="bullshit__oops">{{ $t("errorPage.oops") }}</div>
|
||||
<div class="bullshit__headline">{{ message() }}</div>
|
||||
<div class="bullshit__info">
|
||||
Please check that the URL you entered is correct, or click the button
|
||||
below to return to the homepage.
|
||||
{{ $t("errorPage.notFoundInfo") }}
|
||||
</div>
|
||||
<a href="" class="bullshit__return-home">Back to home</a>
|
||||
<a href="/#/" class="bullshit__return-home">{{ $t("errorPage.notFoundBackHome") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,18 @@
|
||||
<el-card shadow="never">
|
||||
<div class="mb-3">
|
||||
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
||||
<el-upload
|
||||
v-model:file-list="importFileList"
|
||||
:http-request="handleImport"
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
:limit="1"
|
||||
:before-upload="beforeImport"
|
||||
class="inline-block ml-2"
|
||||
>
|
||||
<el-button>{{ $t("common.import") }}</el-button>
|
||||
</el-upload>
|
||||
<el-button class="ml-2" @click="handleExport">{{ $t("common.export") }}</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="records">
|
||||
<el-table-column :label="$t('peer.name')" min-width="220">
|
||||
@@ -88,7 +100,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="qrDialog" title="QR" width="420px">
|
||||
<el-dialog v-model="qrDialog" :title="$t('peer.clientQr')" width="420px">
|
||||
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
||||
</el-dialog>
|
||||
|
||||
@@ -127,21 +139,24 @@
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
||||
import {
|
||||
deletePeerApi,
|
||||
getPeerApi,
|
||||
getPeerClientConfigApi,
|
||||
importPeerApi,
|
||||
kickPeerApi,
|
||||
pagePeerApi,
|
||||
releaseKickPeerApi,
|
||||
resetPeerTrafficApi,
|
||||
savePeerApi,
|
||||
updatePeerApi,
|
||||
exportPeerApi,
|
||||
} from "@/api/peer";
|
||||
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types";
|
||||
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus/lib/components";
|
||||
|
||||
const { t } = useI18n();
|
||||
const loading = ref(false);
|
||||
@@ -149,6 +164,7 @@ const total = ref(0);
|
||||
const records = ref<PeerVo[]>([]);
|
||||
const qrDialog = ref(false);
|
||||
const qrSrc = ref("");
|
||||
const importFileList = ref<UploadFile[]>([]);
|
||||
const overviewQrSrc = ref("");
|
||||
const overviewClientUrl = ref("");
|
||||
const formRef = ref();
|
||||
@@ -281,6 +297,47 @@ async function showQr(row: PeerVo) {
|
||||
qrDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleImport(params: UploadRequestOptions) {
|
||||
if (importFileList.value.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
await importPeerApi(formData);
|
||||
importFileList.value = [];
|
||||
await handleQuery();
|
||||
}
|
||||
|
||||
function beforeImport(file: UploadRawFile) {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
ElMessage.error(t("common.fileFormatUnsupported"));
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
ElMessage.error(t("common.fileTooLarge"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
try {
|
||||
const response = await exportPeerApi();
|
||||
const blob = new Blob([response.data], { type: "application/octet-stream" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.href = url;
|
||||
const dis = response.headers["content-disposition"] || "";
|
||||
a.download = dis.split("attachment; filename=")[1] || "peers-export.json";
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
ElMessage.success(t("common.downloadSuccess"));
|
||||
} catch {
|
||||
ElMessage.error(t("common.invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(handleQuery);
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user