Add external URL editing capability to overview page

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/7459/head
Ryan Wang 2025-05-22 14:56:57 +08:00
parent 254bb9b225
commit 42c374dba5
6 changed files with 196 additions and 45 deletions

View File

@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import H2WarningAlert from "@/components/alerts/H2WarningAlert.vue"; import H2WarningAlert from "@/components/alerts/H2WarningAlert.vue";
import type { GlobalInfo, Info, Startup } from "@/types"; import type { Info, Startup } from "@/types";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
import { useThemeStore } from "@console/stores/theme"; import { useThemeStore } from "@console/stores/theme";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
import { consoleApiClient } from "@halo-dev/api-client"; import { consoleApiClient } from "@halo-dev/api-client";
@ -10,7 +11,6 @@ import {
IconClipboardLine, IconClipboardLine,
IconTerminalBoxLine, IconTerminalBoxLine,
Toast, Toast,
VAlert,
VButton, VButton,
VCard, VCard,
VDescription, VDescription,
@ -24,6 +24,7 @@ import { useClipboard } from "@vueuse/core";
import axios from "axios"; import axios from "axios";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import ExternalUrlItem from "./components/ExternalUrlItem.vue";
const { t } = useI18n(); const { t } = useI18n();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -40,16 +41,7 @@ const { data: info } = useQuery<Info>({
retry: 0, retry: 0,
}); });
const { data: globalInfo } = useQuery<GlobalInfo>({ const { globalInfo } = useGlobalInfoFetch();
queryKey: ["system-global-info"],
queryFn: async () => {
const { data } = await axios.get<GlobalInfo>(`/actuator/globalinfo`, {
withCredentials: true,
});
return data;
},
retry: 0,
});
const { data: startup } = useQuery<Startup>({ const { data: startup } = useQuery<Startup>({
queryKey: ["system-startup-info"], queryKey: ["system-startup-info"],
@ -76,20 +68,6 @@ const { data: plugins, isLoading: isPluginsLoading } = useQuery<Plugin[]>({
enabled: computed(() => currentUserHasPermission(["system:plugins:view"])), enabled: computed(() => currentUserHasPermission(["system:plugins:view"])),
}); });
const isExternalUrlValid = computed(() => {
if (!globalInfo.value?.useAbsolutePermalink) {
return true;
}
if (!globalInfo.value?.externalUrl) {
return true;
}
const url = new URL(globalInfo.value.externalUrl);
const { host: currentHost, protocol: currentProtocol } = window.location;
return url.host === currentHost && url.protocol === currentProtocol;
});
// copy system information to clipboard // copy system information to clipboard
const { copy, isSupported } = useClipboard({ legacy: true }); const { copy, isSupported } = useClipboard({ legacy: true });
@ -243,25 +221,7 @@ const handleDownloadLogfile = () => {
</div> </div>
<div class="border-t border-gray-200"> <div class="border-t border-gray-200">
<VDescription> <VDescription>
<VDescriptionItem :label="$t('core.overview.fields.external_url')"> <ExternalUrlItem />
<span v-if="globalInfo?.externalUrl">
{{ globalInfo?.externalUrl }}
</span>
<span v-else>
{{ $t("core.overview.fields_values.external_url.not_setup") }}
</span>
<VAlert
v-if="!isExternalUrlValid"
class="mt-3"
type="warning"
:title="$t('core.common.text.warning')"
:closable="false"
>
<template #description>
{{ $t("core.overview.alert.external_url_invalid") }}
</template>
</VAlert>
</VDescriptionItem>
<VDescriptionItem <VDescriptionItem
v-if="startup?.timeline.startTime" v-if="startup?.timeline.startTime"
:label="$t('core.overview.fields.start_time')" :label="$t('core.overview.fields.start_time')"

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import { setFocus } from "@/formkit/utils/focus";
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
import { coreApiClient } from "@halo-dev/api-client";
import { Dialog, Toast, VButton, VLoading, VSpace } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import axios from "axios";
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { globalInfo } = useGlobalInfoFetch();
const emit = defineEmits<{
(event: "close"): void;
}>();
const isRestarting = ref(false);
function onSubmit({ externalUrl }: { externalUrl: string }) {
Dialog.warning({
title: t("core.overview.external_url_form.operations.save.title"),
description: t(
"core.overview.external_url_form.operations.save.description"
),
confirmType: "danger",
confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => {
const { data: configMap } = await coreApiClient.configMap.getConfigMap({
name: "system",
});
const basicConfig = JSON.parse(configMap.data?.["basic"] || "{}");
basicConfig.externalUrl = externalUrl.trim();
await coreApiClient.configMap.patchConfigMap({
name: "system",
jsonPatchInner: [
{
op: "add",
path: "/data/basic",
value: JSON.stringify(basicConfig),
},
],
});
await axios.post(`/actuator/restart`);
isRestarting.value = true;
Toast.success(t("core.common.toast.save_success"));
},
});
}
onMounted(() => {
setFocus("externalUrl");
});
useQuery({
queryKey: ["check-health"],
queryFn: async () => {
const { data } = await axios.get("/actuator/health");
return data;
},
onSuccess(data) {
if (data.status === "UP") {
window.location.reload();
}
},
retry: true,
retryDelay: 2000,
enabled: computed(() => isRestarting.value),
});
</script>
<template>
<template v-if="!isRestarting">
<FormKit
id="external-url-form"
type="form"
name="external-url-form"
@submit="onSubmit"
>
<FormKit
id="externalUrl"
:model-value="globalInfo?.externalUrl"
type="url"
name="externalUrl"
validation="url|required"
:validation-label="$t('core.overview.fields.external_url')"
:classes="{ outer: '!pb-0' }"
></FormKit>
</FormKit>
<VSpace class="mt-4">
<VButton type="secondary" @click="$formkit.submit('external-url-form')">
{{ $t("core.common.buttons.save") }}
</VButton>
<VButton @click="emit('close')">
{{ $t("core.common.buttons.cancel") }}
</VButton>
</VSpace>
</template>
<template v-else>
<div class="flex items-center gap-2">
<VLoading class="!inline-flex !py-0" />
<div class="text-xs text-gray-600">
{{ $t("core.overview.external_url_form.tips.restarting") }}
</div>
</div>
</template>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
import {
IconRiPencilFill,
VAlert,
VDescriptionItem,
} from "@halo-dev/components";
import { computed, ref } from "vue";
import ExternalUrlForm from "./ExternalUrlForm.vue";
const { globalInfo } = useGlobalInfoFetch();
const isExternalUrlValid = computed(() => {
if (!globalInfo.value?.externalUrl) {
return false;
}
const url = new URL(globalInfo.value.externalUrl);
const { host: currentHost, protocol: currentProtocol } = window.location;
return url.host === currentHost && url.protocol === currentProtocol;
});
const showExternalUrlForm = ref(false);
</script>
<template>
<VDescriptionItem :label="$t('core.overview.fields.external_url')">
<div v-if="!showExternalUrlForm" class="flex items-center gap-3">
<span v-if="globalInfo?.externalUrl">
{{ globalInfo?.externalUrl }}
</span>
<span v-else>
{{ $t("core.overview.fields_values.external_url.not_setup") }}
</span>
<IconRiPencilFill
class="cursor-pointer text-sm text-gray-600 hover:text-gray-900"
@click="showExternalUrlForm = true"
/>
</div>
<ExternalUrlForm v-else @close="showExternalUrlForm = false" />
<VAlert
v-if="!isExternalUrlValid && !showExternalUrlForm"
class="mt-3"
type="warning"
:title="$t('core.common.text.warning')"
:closable="false"
>
<template #description>
{{ $t("core.overview.alert.external_url_invalid") }}
</template>
</VAlert>
</VDescriptionItem>
</template>

View File

@ -1458,6 +1458,16 @@ core:
The external access url detected is inconsistent with the current access The external access url detected is inconsistent with the current access
url, which may cause some links to fail to redirect properly. Please url, which may cause some links to fail to redirect properly. Please
check the external access url settings. check the external access url settings.
external_url_form:
operations:
save:
title: Modify external url
description: >-
Modifying the external access address requires restarting the Halo
service. It will automatically restart after the modification is
completed. Do you want to continue?
tips:
restarting: Modification completed, waiting for restart...
backup: backup:
title: Backup and Restore title: Backup and Restore
tabs: tabs:

View File

@ -1356,6 +1356,13 @@ core:
os: 操作系统:{os} os: 操作系统:{os}
alert: alert:
external_url_invalid: 检测到外部访问地址与当前访问地址不一致,可能会导致部分链接无法正常跳转,请检查外部访问地址设置。 external_url_invalid: 检测到外部访问地址与当前访问地址不一致,可能会导致部分链接无法正常跳转,请检查外部访问地址设置。
external_url_form:
operations:
save:
title: 修改外部访问地址
description: 修改外部访问地址需要重启 Halo 服务,修改完成之后会自动进行重启,是否继续?
tips:
restarting: 修改完成,等待重启...
backup: backup:
title: 备份与恢复 title: 备份与恢复
tabs: tabs:

View File

@ -1341,6 +1341,13 @@ core:
os: 操作系統:{os} os: 操作系統:{os}
alert: alert:
external_url_invalid: 檢測到外部訪問地址與當前訪問地址不一致,可能會導致部分連結無法正常跳轉,請檢查外部訪問地址設置。 external_url_invalid: 檢測到外部訪問地址與當前訪問地址不一致,可能會導致部分連結無法正常跳轉,請檢查外部訪問地址設置。
external_url_form:
operations:
save:
title: 修改外部訪問地址
description: 修改外部訪問地址需要重啟 Halo 服務,修改完成之後會自動進行重啟,是否繼續?
tips:
restarting: 修改完成,等待重啟...
backup: backup:
title: 備份與還原 title: 備份與還原
tabs: tabs: