Add support for configuring external URL online (#7459)

* Add support for configuring external URL online

* Fix the problem of timeout on external URL initialization

* Add external URL editing capability to overview page

Signed-off-by: Ryan Wang <i@ryanc.cc>

---------

Signed-off-by: Ryan Wang <i@ryanc.cc>
Co-authored-by: Ryan Wang <i@ryanc.cc>
pull/7466/head
John Niang 2025-05-22 15:23:43 +08:00 committed by GitHub
commit fb6b6f632d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 490 additions and 260 deletions

View File

@ -71,6 +71,7 @@ public class SystemSetting {
String logo; String logo;
String favicon; String favicon;
String language; String language;
String externalUrl;
@JsonIgnore @JsonIgnore
public Optional<Locale> useSystemLocale() { public Optional<Locale> useSystemLocale() {

View File

@ -89,10 +89,9 @@ public class ExtensionResourceInitializer implements SmartLifecycle {
extension.getMetadata().getName()); extension.getMetadata().getName());
} }
}) })
.then(Mono.fromRunnable( .then()
() -> eventPublisher.publishEvent(new ExtensionInitializedEvent(this)))
)
.block(Duration.ofMinutes(1)); .block(Duration.ofMinutes(1));
eventPublisher.publishEvent(new ExtensionInitializedEvent(this));
} }
@Override @Override

View File

@ -1,71 +0,0 @@
package run.halo.app.infra;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.http.HttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import run.halo.app.infra.properties.HaloProperties;
/**
* Default implementation for getting external url from halo properties.
*
* @author johnniang
*/
@Component
public class HaloPropertiesExternalUrlSupplier implements ExternalUrlSupplier {
private final HaloProperties haloProperties;
private final WebFluxProperties webFluxProperties;
public HaloPropertiesExternalUrlSupplier(HaloProperties haloProperties,
WebFluxProperties webFluxProperties) {
this.haloProperties = haloProperties;
this.webFluxProperties = webFluxProperties;
}
@Override
public URI get() {
if (!haloProperties.isUseAbsolutePermalink()) {
return URI.create(getBasePath());
}
try {
return haloProperties.getExternalUrl().toURI();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public URL getURL(HttpRequest request) {
var externalUrl = haloProperties.getExternalUrl();
if (externalUrl == null) {
try {
externalUrl = request.getURI().resolve(getBasePath()).toURL();
} catch (MalformedURLException e) {
throw new RuntimeException("Cannot parse request URI to URL.", e);
}
}
return externalUrl;
}
@Nullable
@Override
public URL getRaw() {
return haloProperties.getExternalUrl();
}
private String getBasePath() {
var basePath = webFluxProperties.getBasePath();
if (!StringUtils.hasText(basePath)) {
basePath = "/";
}
return basePath;
}
}

View File

@ -0,0 +1,109 @@
package run.halo.app.infra;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.Exceptions;
import run.halo.app.infra.properties.HaloProperties;
/**
* Default implementation for getting external url from system config first, halo properties second.
*
* @author johnniang
*/
@Slf4j
@Component
class SystemConfigFirstExternalUrlSupplier implements ExternalUrlSupplier {
private final HaloProperties haloProperties;
private final WebFluxProperties webFluxProperties;
private final SystemConfigurableEnvironmentFetcher systemConfigFetcher;
@Nullable
private URL externalUrl;
public SystemConfigFirstExternalUrlSupplier(HaloProperties haloProperties,
WebFluxProperties webFluxProperties,
SystemConfigurableEnvironmentFetcher systemConfigFetcher) {
this.haloProperties = haloProperties;
this.webFluxProperties = webFluxProperties;
this.systemConfigFetcher = systemConfigFetcher;
}
@EventListener
void onExtensionInitialized(ExtensionInitializedEvent ignored) {
systemConfigFetcher.getBasic()
.mapNotNull(SystemSetting.Basic::getExternalUrl)
.filter(StringUtils::hasText)
.doOnNext(externalUrlString -> {
try {
this.externalUrl = URI.create(externalUrlString).toURL();
} catch (MalformedURLException e) {
log.error("""
Cannot parse external URL {} from system config. Fallback to default \
external URL supplier from properties.\
""", externalUrlString, e);
// For continuing the application startup, we need to return null here.
}
})
.blockOptional(Duration.ofSeconds(10));
}
@Override
public URI get() {
try {
if (externalUrl != null) {
return externalUrl.toURI();
}
if (!haloProperties.isUseAbsolutePermalink()) {
return URI.create(getBasePath());
}
return haloProperties.getExternalUrl().toURI();
} catch (URISyntaxException e) {
throw Exceptions.propagate(e);
}
}
@Override
public URL getURL(HttpRequest request) {
if (this.externalUrl != null) {
return this.externalUrl;
}
var externalUrl = haloProperties.getExternalUrl();
if (externalUrl != null) {
return externalUrl;
}
try {
externalUrl = request.getURI().resolve(getBasePath()).toURL();
} catch (MalformedURLException e) {
throw new RuntimeException("Cannot parse request URI to URL.", e);
}
return externalUrl;
}
@Nullable
@Override
public URL getRaw() {
return externalUrl != null ? externalUrl : haloProperties.getExternalUrl();
}
private String getBasePath() {
var basePath = webFluxProperties.getBasePath();
if (!StringUtils.hasText(basePath)) {
basePath = "/";
}
return basePath;
}
}

View File

@ -61,17 +61,17 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler<Reconcil
public Mono<SystemSetting.Basic> getBasic() { public Mono<SystemSetting.Basic> getBasic() {
return fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class) return fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class)
.switchIfEmpty(Mono.just(new SystemSetting.Basic())); .switchIfEmpty(Mono.fromSupplier(SystemSetting.Basic::new));
} }
public Mono<SystemSetting.Comment> fetchComment() { public Mono<SystemSetting.Comment> fetchComment() {
return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class) return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class)
.switchIfEmpty(Mono.just(new SystemSetting.Comment())); .switchIfEmpty(Mono.fromSupplier(SystemSetting.Comment::new));
} }
public Mono<SystemSetting.Post> fetchPost() { public Mono<SystemSetting.Post> fetchPost() {
return fetch(SystemSetting.Post.GROUP, SystemSetting.Post.class) return fetch(SystemSetting.Post.GROUP, SystemSetting.Post.class)
.switchIfEmpty(Mono.just(new SystemSetting.Post())); .switchIfEmpty(Mono.fromSupplier(SystemSetting.Post::new));
} }
public Mono<SystemSetting.ThemeRouteRules> fetchRouteRules() { public Mono<SystemSetting.ThemeRouteRules> fetchRouteRules() {
@ -123,15 +123,17 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler<Reconcil
data.forEach((group, dataValue) -> { data.forEach((group, dataValue) -> {
// https://www.rfc-editor.org/rfc/rfc7386 // https://www.rfc-editor.org/rfc/rfc7386
String defaultV = copiedDefault.get(group); String defaultV = copiedDefault.get(group);
String newValue; String newValue = null;
if (dataValue == null) { if (dataValue == null) {
if (copiedDefault.containsKey(group)) { if (!copiedDefault.containsKey(group)) {
newValue = null;
} else {
newValue = defaultV; newValue = defaultV;
} }
} else { } else {
newValue = mergeRemappingFunction(dataValue, defaultV); if (copiedDefault.containsKey(group)) {
newValue = mergeRemappingFunction(dataValue, defaultV);
} else {
newValue = dataValue;
}
} }
if (newValue == null) { if (newValue == null) {
@ -195,20 +197,18 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler<Reconcil
* @return a new {@link ConfigMap} named <code>system</code> by json merge patch. * @return a new {@link ConfigMap} named <code>system</code> by json merge patch.
*/ */
private Mono<ConfigMap> loadConfigMapInternal() { private Mono<ConfigMap> loadConfigMapInternal() {
Mono<ConfigMap> mapMono = var defaultConfigMono =
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT); extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT);
if (mapMono == null) { var configMono = extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);
return Mono.empty(); return defaultConfigMono.flatMap(defaultConfig -> configMono.map(
} config -> {
return mapMono.flatMap(systemDefault -> Map<String, String> defaultData = defaultConfig.getData();
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG) Map<String, String> data = config.getData();
.map(system -> {
Map<String, String> defaultData = systemDefault.getData();
Map<String, String> data = system.getData();
Map<String, String> mergedData = mergeData(defaultData, data); Map<String, String> mergedData = mergeData(defaultData, data);
system.setData(mergedData); config.setData(mergedData);
return system; return config;
}) })
.switchIfEmpty(Mono.just(systemDefault))); .defaultIfEmpty(defaultConfig)
);
} }
} }

View File

@ -12,6 +12,7 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ConfigMap;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.InitializationStateGetter; import run.halo.app.infra.InitializationStateGetter;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
@ -36,20 +37,24 @@ public class GlobalInfoServiceImpl implements GlobalInfoService {
private final ObjectProvider<SystemConfigurableEnvironmentFetcher> private final ObjectProvider<SystemConfigurableEnvironmentFetcher>
systemConfigFetcher; systemConfigFetcher;
private final ExternalUrlSupplier externalUrl;
public GlobalInfoServiceImpl(HaloProperties haloProperties, public GlobalInfoServiceImpl(HaloProperties haloProperties,
AuthProviderService authProviderService, AuthProviderService authProviderService,
InitializationStateGetter initializationStateGetter, InitializationStateGetter initializationStateGetter,
ObjectProvider<SystemConfigurableEnvironmentFetcher> systemConfigFetcher) { ObjectProvider<SystemConfigurableEnvironmentFetcher> systemConfigFetcher,
ExternalUrlSupplier externalUrl) {
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
this.authProviderService = authProviderService; this.authProviderService = authProviderService;
this.initializationStateGetter = initializationStateGetter; this.initializationStateGetter = initializationStateGetter;
this.systemConfigFetcher = systemConfigFetcher; this.systemConfigFetcher = systemConfigFetcher;
this.externalUrl = externalUrl;
} }
@Override @Override
public Mono<GlobalInfo> getGlobalInfo() { public Mono<GlobalInfo> getGlobalInfo() {
final var info = new GlobalInfo(); final var info = new GlobalInfo();
info.setExternalUrl(haloProperties.getExternalUrl()); info.setExternalUrl(externalUrl.getRaw());
info.setUseAbsolutePermalink(haloProperties.isUseAbsolutePermalink()); info.setUseAbsolutePermalink(haloProperties.isUseAbsolutePermalink());
info.setLocale(Locale.getDefault()); info.setLocale(Locale.getDefault());
info.setTimeZone(TimeZone.getDefault()); info.setTimeZone(TimeZone.getDefault());

View File

@ -1,119 +0,0 @@
package run.halo.app.infra;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.http.HttpRequest;
import run.halo.app.infra.properties.HaloProperties;
@ExtendWith(MockitoExtension.class)
class HaloPropertiesExternalUrlSupplierTest {
@Mock
HaloProperties haloProperties;
@Mock
WebFluxProperties webFluxProperties;
@InjectMocks
HaloPropertiesExternalUrlSupplier externalUrl;
@Test
void getURIWhenUsingAbsolutePermalink() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);
assertEquals(fakeUri, externalUrl.get());
}
@Test
void getURIWhenBasePathSetAndNotUsingAbsolutePermalink() throws MalformedURLException {
when(webFluxProperties.getBasePath()).thenReturn("/blog");
when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);
assertEquals(URI.create("/blog"), externalUrl.get());
}
@Test
void getURIWhenBasePathSetAndUsingAbsolutePermalink() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog");
when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);
assertEquals(URI.create("https://halo.run/fake"), externalUrl.get());
}
@Test
void getURIWhenUsingRelativePermalink() throws MalformedURLException {
when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);
assertEquals(URI.create("/"), externalUrl.get());
}
@Test
void getURLWhenExternalURLProvided() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
var mockRequest = mock(HttpRequest.class);
var url = externalUrl.getURL(mockRequest);
assertEquals(fakeUri.toURL(), url);
}
@Test
void getURLWhenExternalURLAbsent() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(null);
var mockRequest = mock(HttpRequest.class);
when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/"), url);
}
@Test
void getURLWhenBasePathSetAndExternalURLProvided() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog");
var mockRequest = mock(HttpRequest.class);
lenient().when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/fake"), url);
}
@Test
void getURLWhenBasePathSetAndExternalURLAbsent() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(null);
when(webFluxProperties.getBasePath()).thenReturn("/blog");
var mockRequest = mock(HttpRequest.class);
when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/blog"), url);
}
@Test
void getRaw() throws MalformedURLException {
var fakeUri = URI.create("http://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
assertEquals(fakeUri.toURL(), externalUrl.getRaw());
when(haloProperties.getExternalUrl()).thenReturn(null);
assertNull(externalUrl.getRaw());
}
}

View File

@ -0,0 +1,155 @@
package run.halo.app.infra;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.http.HttpRequest;
import reactor.core.publisher.Mono;
import run.halo.app.infra.properties.HaloProperties;
@ExtendWith(MockitoExtension.class)
class SystemConfigFirstExternalUrlSupplierTest {
@Mock
HaloProperties haloProperties;
@Mock
WebFluxProperties webFluxProperties;
@Mock
SystemConfigurableEnvironmentFetcher systemConfigFetcher;
@InjectMocks
SystemConfigFirstExternalUrlSupplier externalUrl;
@Nested
class HaloPropertiesSupplier {
@BeforeEach
void setUp() {
when(systemConfigFetcher.getBasic()).thenReturn(Mono.empty());
externalUrl.onExtensionInitialized(null);
}
@Test
void getURIWhenUsingAbsolutePermalink() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);
assertEquals(fakeUri, externalUrl.get());
}
@Test
void getURIWhenBasePathSetAndNotUsingAbsolutePermalink() throws MalformedURLException {
when(webFluxProperties.getBasePath()).thenReturn("/blog");
when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);
assertEquals(URI.create("/blog"), externalUrl.get());
}
@Test
void getURIWhenBasePathSetAndUsingAbsolutePermalink() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog");
when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);
assertEquals(URI.create("https://halo.run/fake"), externalUrl.get());
}
@Test
void getURIWhenUsingRelativePermalink() throws MalformedURLException {
when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);
assertEquals(URI.create("/"), externalUrl.get());
}
@Test
void getURLWhenExternalURLProvided() throws MalformedURLException {
var fakeUri = URI.create("https://halo.run/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
var mockRequest = mock(HttpRequest.class);
var url = externalUrl.getURL(mockRequest);
assertEquals(fakeUri.toURL(), url);
}
@Test
void getURLWhenExternalURLAbsent() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(null);
var mockRequest = mock(HttpRequest.class);
when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/"), url);
}
@Test
void getURLWhenBasePathSetAndExternalURLProvided() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog");
var mockRequest = mock(HttpRequest.class);
lenient().when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/fake"), url);
}
@Test
void getURLWhenBasePathSetAndExternalURLAbsent() throws MalformedURLException {
var fakeUri = URI.create("https://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(null);
when(webFluxProperties.getBasePath()).thenReturn("/blog");
var mockRequest = mock(HttpRequest.class);
when(mockRequest.getURI()).thenReturn(fakeUri);
var url = externalUrl.getURL(mockRequest);
assertEquals(new URL("https://localhost/blog"), url);
}
@Test
void getRaw() throws MalformedURLException {
var fakeUri = URI.create("http://localhost/fake");
when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());
assertEquals(fakeUri.toURL(), externalUrl.getRaw());
when(haloProperties.getExternalUrl()).thenReturn(null);
assertNull(externalUrl.getRaw());
}
}
@Nested
class SystemConfigSupplier {
@Test
void shouldGetUrlCorrectly() throws Exception {
var basic = new SystemSetting.Basic();
basic.setExternalUrl("https://www.halo.run");
when(systemConfigFetcher.getBasic()).thenReturn(Mono.just(basic));
externalUrl.onExtensionInitialized(null);
assertEquals(URI.create("https://www.halo.run").toURL(), externalUrl.getRaw());
assertEquals(URI.create("https://www.halo.run"), externalUrl.get());
var mockRequest = mock(HttpRequest.class);
assertEquals(URI.create("https://www.halo.run").toURL(),
externalUrl.getURL(mockRequest));
}
}
}

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: