mirror of https://github.com/halo-dev/halo
				
				
				
			feat: add the ability to install plugins remotely via URI (#3963)
#### What type of PR is this? /kind feature /area core /area console /milestone 2.6.x /kind api-change #### What this PR does / why we need it: 支持通过 URI 远程安装和升级插件 how to test it? 1. 测试插件安装 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-KhIVw/releases/app-release-canxF/download/app-release-canxF-znFre" }' ``` 2. 测试插件升级 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/plugins/PluginFeed/upgrade-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-KhIVw/releases/app-release-canxF/download/app-release-canxF-znFre" }' ``` #### Which issue(s) this PR fixes: Fixes #2292 #### Does this PR introduce a user-facing change? ```release-note 支持通过 URI 远程安装和升级插件 ```pull/4005/head
							parent
							
								
									f5493a6d86
								
							
						
					
					
						commit
						710261b035
					
				| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
package run.halo.app.core.extension.endpoint;
 | 
			
		||||
 | 
			
		||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
 | 
			
		||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
 | 
			
		||||
import static java.util.Comparator.comparing;
 | 
			
		||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
 | 
			
		||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +18,8 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
 | 
			
		|||
import io.swagger.v3.oas.annotations.media.ArraySchema;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.nio.file.Paths;
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +41,7 @@ import org.springframework.http.codec.multipart.FilePart;
 | 
			
		|||
import org.springframework.http.codec.multipart.FormFieldPart;
 | 
			
		||||
import org.springframework.http.codec.multipart.Part;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.springframework.util.FileCopyUtils;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
import org.springframework.util.StringUtils;
 | 
			
		||||
import org.springframework.web.reactive.function.server.RouterFunction;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +60,10 @@ import run.halo.app.extension.Comparators;
 | 
			
		|||
import run.halo.app.extension.ConfigMap;
 | 
			
		||||
import run.halo.app.extension.ReactiveExtensionClient;
 | 
			
		||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
 | 
			
		||||
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
 | 
			
		||||
import run.halo.app.infra.exception.ThemeInstallationException;
 | 
			
		||||
import run.halo.app.infra.exception.ThemeUpgradeException;
 | 
			
		||||
import run.halo.app.infra.utils.DataBufferUtils;
 | 
			
		||||
import run.halo.app.plugin.PluginNotFoundException;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +75,8 @@ public class PluginEndpoint implements CustomEndpoint {
 | 
			
		|||
 | 
			
		||||
    private final PluginService pluginService;
 | 
			
		||||
 | 
			
		||||
    private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RouterFunction<ServerResponse> endpoint() {
 | 
			
		||||
        final var tag = "api.console.halo.run/v1alpha1/Plugin";
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +93,39 @@ public class PluginEndpoint implements CustomEndpoint {
 | 
			
		|||
                        ))
 | 
			
		||||
                    .response(responseBuilder().implementation(Plugin.class))
 | 
			
		||||
            )
 | 
			
		||||
            .POST("plugins/-/install-from-uri", this::installFromUri,
 | 
			
		||||
                builder -> builder.operationId("InstallPluginFromUri")
 | 
			
		||||
                    .description("Install a plugin from uri.")
 | 
			
		||||
                    .tag(tag)
 | 
			
		||||
                    .requestBody(requestBodyBuilder()
 | 
			
		||||
                        .required(true)
 | 
			
		||||
                        .content(contentBuilder()
 | 
			
		||||
                            .mediaType(MediaType.APPLICATION_JSON_VALUE)
 | 
			
		||||
                            .schema(schemaBuilder()
 | 
			
		||||
                                .implementation(InstallFromUriRequest.class))
 | 
			
		||||
                        ))
 | 
			
		||||
                    .response(responseBuilder()
 | 
			
		||||
                        .implementation(Plugin.class))
 | 
			
		||||
            )
 | 
			
		||||
            .POST("plugins/{name}/upgrade-from-uri", this::upgradeFromUri,
 | 
			
		||||
                builder -> builder.operationId("UpgradePluginFromUri")
 | 
			
		||||
                    .description("Upgrade a plugin from uri.")
 | 
			
		||||
                    .tag(tag)
 | 
			
		||||
                    .parameter(parameterBuilder()
 | 
			
		||||
                        .in(ParameterIn.PATH)
 | 
			
		||||
                        .name("name")
 | 
			
		||||
                        .required(true)
 | 
			
		||||
                    )
 | 
			
		||||
                    .requestBody(requestBodyBuilder()
 | 
			
		||||
                        .required(true)
 | 
			
		||||
                        .content(contentBuilder()
 | 
			
		||||
                            .mediaType(MediaType.APPLICATION_JSON_VALUE)
 | 
			
		||||
                            .schema(schemaBuilder()
 | 
			
		||||
                                .implementation(UpgradeFromUriRequest.class))
 | 
			
		||||
                        ))
 | 
			
		||||
                    .response(responseBuilder()
 | 
			
		||||
                        .implementation(Plugin.class))
 | 
			
		||||
            )
 | 
			
		||||
            .POST("plugins/{name}/upgrade", contentType(MediaType.MULTIPART_FORM_DATA),
 | 
			
		||||
                this::upgrade, builder -> builder.operationId("UpgradePlugin")
 | 
			
		||||
                    .description("Upgrade a plugin by uploading a Jar file")
 | 
			
		||||
| 
						 | 
				
			
			@ -177,6 +220,57 @@ public class PluginEndpoint implements CustomEndpoint {
 | 
			
		|||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {
 | 
			
		||||
        final var name = request.pathVariable("name");
 | 
			
		||||
        return request.bodyToMono(UpgradeFromUriRequest.class)
 | 
			
		||||
            .flatMap(upgradeRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
 | 
			
		||||
                reactiveUrlDataBufferFetcher.fetch(upgradeRequest.uri())))
 | 
			
		||||
            )
 | 
			
		||||
            .subscribeOn(Schedulers.boundedElastic())
 | 
			
		||||
            .onErrorMap(throwable -> {
 | 
			
		||||
                log.error("Failed to fetch plugin file from uri.", throwable);
 | 
			
		||||
                return new ThemeUpgradeException("Failed to fetch plugin file from uri.", null,
 | 
			
		||||
                    null);
 | 
			
		||||
            })
 | 
			
		||||
            .flatMap(inputStream -> Mono.usingWhen(
 | 
			
		||||
                transferToTemp(inputStream),
 | 
			
		||||
                (path) -> pluginService.upgrade(name, path),
 | 
			
		||||
                this::deleteFileIfExists)
 | 
			
		||||
            )
 | 
			
		||||
            .flatMap(theme -> ServerResponse.ok()
 | 
			
		||||
                .contentType(MediaType.APPLICATION_JSON)
 | 
			
		||||
                .bodyValue(theme)
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Mono<ServerResponse> installFromUri(ServerRequest request) {
 | 
			
		||||
        return request.bodyToMono(InstallFromUriRequest.class)
 | 
			
		||||
            .flatMap(installRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
 | 
			
		||||
                reactiveUrlDataBufferFetcher.fetch(installRequest.uri())))
 | 
			
		||||
            )
 | 
			
		||||
            .subscribeOn(Schedulers.boundedElastic())
 | 
			
		||||
            .doOnError(throwable -> {
 | 
			
		||||
                log.error("Failed to fetch plugin file from uri.", throwable);
 | 
			
		||||
                throw new ThemeInstallationException("Failed to fetch plugin file from uri.", null,
 | 
			
		||||
                    null);
 | 
			
		||||
            })
 | 
			
		||||
            .flatMap(inputStream -> Mono.usingWhen(
 | 
			
		||||
                transferToTemp(inputStream),
 | 
			
		||||
                pluginService::install,
 | 
			
		||||
                this::deleteFileIfExists)
 | 
			
		||||
            )
 | 
			
		||||
            .flatMap(theme -> ServerResponse.ok()
 | 
			
		||||
                .contentType(MediaType.APPLICATION_JSON)
 | 
			
		||||
                .bodyValue(theme)
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Mono<ServerResponse> reload(ServerRequest serverRequest) {
 | 
			
		||||
        var name = serverRequest.pathVariable("name");
 | 
			
		||||
        return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);
 | 
			
		||||
| 
						 | 
				
			
			@ -508,4 +602,11 @@ public class PluginEndpoint implements CustomEndpoint {
 | 
			
		|||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Mono<Path> transferToTemp(InputStream inputStream) {
 | 
			
		||||
        return Mono.fromCallable(() -> {
 | 
			
		||||
            Path tempFile = Files.createTempFile("halo-plugins", ".jar");
 | 
			
		||||
            FileCopyUtils.copy(inputStream, Files.newOutputStream(tempFile));
 | 
			
		||||
            return tempFile;
 | 
			
		||||
        }).subscribeOn(Schedulers.boundedElastic());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
 | 
			
		|||
import org.springframework.web.server.ServerWebInputException;
 | 
			
		||||
import reactor.core.Exceptions;
 | 
			
		||||
import reactor.core.publisher.Mono;
 | 
			
		||||
import reactor.core.scheduler.Schedulers;
 | 
			
		||||
import reactor.util.retry.Retry;
 | 
			
		||||
import run.halo.app.core.extension.Setting;
 | 
			
		||||
import run.halo.app.core.extension.Theme;
 | 
			
		||||
| 
						 | 
				
			
			@ -247,6 +248,7 @@ public class ThemeEndpoint implements CustomEndpoint {
 | 
			
		|||
            .flatMap(upgradeRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
 | 
			
		||||
                reactiveUrlDataBufferFetcher.fetch(upgradeRequest.uri())))
 | 
			
		||||
            )
 | 
			
		||||
            .subscribeOn(Schedulers.boundedElastic())
 | 
			
		||||
            .doOnError(throwable -> {
 | 
			
		||||
                log.error("Failed to fetch zip file from uri.", throwable);
 | 
			
		||||
                throw new ThemeUpgradeException("Failed to fetch zip file from uri.", null,
 | 
			
		||||
| 
						 | 
				
			
			@ -268,6 +270,7 @@ public class ThemeEndpoint implements CustomEndpoint {
 | 
			
		|||
            .flatMap(installRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
 | 
			
		||||
                reactiveUrlDataBufferFetcher.fetch(installRequest.uri())))
 | 
			
		||||
            )
 | 
			
		||||
            .subscribeOn(Schedulers.boundedElastic())
 | 
			
		||||
            .doOnError(throwable -> {
 | 
			
		||||
                log.error("Failed to fetch zip file from uri.", throwable);
 | 
			
		||||
                throw new ThemeInstallationException("Failed to fetch zip file from uri.", null,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,8 @@ rules:
 | 
			
		|||
    resources: [ "plugins" ]
 | 
			
		||||
    verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
 | 
			
		||||
  - apiGroups: [ "api.console.halo.run" ]
 | 
			
		||||
    resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload" ]
 | 
			
		||||
    resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload",
 | 
			
		||||
                 "plugins/install-from-uri", "plugins/upgrade-from-uri" ]
 | 
			
		||||
    verbs: [ "*" ]
 | 
			
		||||
  - apiGroups: [ "api.console.halo.run" ]
 | 
			
		||||
    resources: [ "plugin-presets" ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,11 +40,15 @@ import {
 | 
			
		|||
// @ts-ignore
 | 
			
		||||
import { ConfigMap } from "../models";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { InstallFromUriRequest } from "../models";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { Plugin } from "../models";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { PluginList } from "../models";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { Setting } from "../models";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { UpgradeFromUriRequest } from "../models";
 | 
			
		||||
/**
 | 
			
		||||
 * ApiConsoleHaloRunV1alpha1PluginApi - axios parameter creator
 | 
			
		||||
 * @export
 | 
			
		||||
| 
						 | 
				
			
			@ -231,6 +235,67 @@ export const ApiConsoleHaloRunV1alpha1PluginApiAxiosParamCreator = function (
 | 
			
		|||
        options: localVarRequestOptions,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Install a plugin from uri.
 | 
			
		||||
     * @param {InstallFromUriRequest} installFromUriRequest
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    installPluginFromUri: async (
 | 
			
		||||
      installFromUriRequest: InstallFromUriRequest,
 | 
			
		||||
      options: AxiosRequestConfig = {}
 | 
			
		||||
    ): Promise<RequestArgs> => {
 | 
			
		||||
      // verify required parameter 'installFromUriRequest' is not null or undefined
 | 
			
		||||
      assertParamExists(
 | 
			
		||||
        "installPluginFromUri",
 | 
			
		||||
        "installFromUriRequest",
 | 
			
		||||
        installFromUriRequest
 | 
			
		||||
      );
 | 
			
		||||
      const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri`;
 | 
			
		||||
      // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
      const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
      let baseOptions;
 | 
			
		||||
      if (configuration) {
 | 
			
		||||
        baseOptions = configuration.baseOptions;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const localVarRequestOptions = {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        ...baseOptions,
 | 
			
		||||
        ...options,
 | 
			
		||||
      };
 | 
			
		||||
      const localVarHeaderParameter = {} as any;
 | 
			
		||||
      const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
      // authentication BasicAuth required
 | 
			
		||||
      // http basic authentication required
 | 
			
		||||
      setBasicAuthToObject(localVarRequestOptions, configuration);
 | 
			
		||||
 | 
			
		||||
      // authentication BearerAuth required
 | 
			
		||||
      // http bearer authentication required
 | 
			
		||||
      await setBearerAuthToObject(localVarHeaderParameter, configuration);
 | 
			
		||||
 | 
			
		||||
      localVarHeaderParameter["Content-Type"] = "application/json";
 | 
			
		||||
 | 
			
		||||
      setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
      let headersFromBaseOptions =
 | 
			
		||||
        baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
      localVarRequestOptions.headers = {
 | 
			
		||||
        ...localVarHeaderParameter,
 | 
			
		||||
        ...headersFromBaseOptions,
 | 
			
		||||
        ...options.headers,
 | 
			
		||||
      };
 | 
			
		||||
      localVarRequestOptions.data = serializeDataIfNeeded(
 | 
			
		||||
        installFromUriRequest,
 | 
			
		||||
        localVarRequestOptions,
 | 
			
		||||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        url: toPathString(localVarUrlObj),
 | 
			
		||||
        options: localVarRequestOptions,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * List all plugin presets in the system.
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
| 
						 | 
				
			
			@ -611,6 +676,75 @@ export const ApiConsoleHaloRunV1alpha1PluginApiAxiosParamCreator = function (
 | 
			
		|||
      };
 | 
			
		||||
      localVarRequestOptions.data = localVarFormParams;
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        url: toPathString(localVarUrlObj),
 | 
			
		||||
        options: localVarRequestOptions,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Upgrade a plugin from uri.
 | 
			
		||||
     * @param {string} name
 | 
			
		||||
     * @param {UpgradeFromUriRequest} upgradeFromUriRequest
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    upgradePluginFromUri: async (
 | 
			
		||||
      name: string,
 | 
			
		||||
      upgradeFromUriRequest: UpgradeFromUriRequest,
 | 
			
		||||
      options: AxiosRequestConfig = {}
 | 
			
		||||
    ): Promise<RequestArgs> => {
 | 
			
		||||
      // verify required parameter 'name' is not null or undefined
 | 
			
		||||
      assertParamExists("upgradePluginFromUri", "name", name);
 | 
			
		||||
      // verify required parameter 'upgradeFromUriRequest' is not null or undefined
 | 
			
		||||
      assertParamExists(
 | 
			
		||||
        "upgradePluginFromUri",
 | 
			
		||||
        "upgradeFromUriRequest",
 | 
			
		||||
        upgradeFromUriRequest
 | 
			
		||||
      );
 | 
			
		||||
      const localVarPath =
 | 
			
		||||
        `/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade-from-uri`.replace(
 | 
			
		||||
          `{${"name"}}`,
 | 
			
		||||
          encodeURIComponent(String(name))
 | 
			
		||||
        );
 | 
			
		||||
      // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
      const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
      let baseOptions;
 | 
			
		||||
      if (configuration) {
 | 
			
		||||
        baseOptions = configuration.baseOptions;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const localVarRequestOptions = {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        ...baseOptions,
 | 
			
		||||
        ...options,
 | 
			
		||||
      };
 | 
			
		||||
      const localVarHeaderParameter = {} as any;
 | 
			
		||||
      const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
      // authentication BasicAuth required
 | 
			
		||||
      // http basic authentication required
 | 
			
		||||
      setBasicAuthToObject(localVarRequestOptions, configuration);
 | 
			
		||||
 | 
			
		||||
      // authentication BearerAuth required
 | 
			
		||||
      // http bearer authentication required
 | 
			
		||||
      await setBearerAuthToObject(localVarHeaderParameter, configuration);
 | 
			
		||||
 | 
			
		||||
      localVarHeaderParameter["Content-Type"] = "application/json";
 | 
			
		||||
 | 
			
		||||
      setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
      let headersFromBaseOptions =
 | 
			
		||||
        baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
      localVarRequestOptions.headers = {
 | 
			
		||||
        ...localVarHeaderParameter,
 | 
			
		||||
        ...headersFromBaseOptions,
 | 
			
		||||
        ...options.headers,
 | 
			
		||||
      };
 | 
			
		||||
      localVarRequestOptions.data = serializeDataIfNeeded(
 | 
			
		||||
        upgradeFromUriRequest,
 | 
			
		||||
        localVarRequestOptions,
 | 
			
		||||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        url: toPathString(localVarUrlObj),
 | 
			
		||||
        options: localVarRequestOptions,
 | 
			
		||||
| 
						 | 
				
			
			@ -700,6 +834,30 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFp = function (
 | 
			
		|||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Install a plugin from uri.
 | 
			
		||||
     * @param {InstallFromUriRequest} installFromUriRequest
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    async installPluginFromUri(
 | 
			
		||||
      installFromUriRequest: InstallFromUriRequest,
 | 
			
		||||
      options?: AxiosRequestConfig
 | 
			
		||||
    ): Promise<
 | 
			
		||||
      (axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>
 | 
			
		||||
    > {
 | 
			
		||||
      const localVarAxiosArgs =
 | 
			
		||||
        await localVarAxiosParamCreator.installPluginFromUri(
 | 
			
		||||
          installFromUriRequest,
 | 
			
		||||
          options
 | 
			
		||||
        );
 | 
			
		||||
      return createRequestFunction(
 | 
			
		||||
        localVarAxiosArgs,
 | 
			
		||||
        globalAxios,
 | 
			
		||||
        BASE_PATH,
 | 
			
		||||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * List all plugin presets in the system.
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
| 
						 | 
				
			
			@ -863,6 +1021,33 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFp = function (
 | 
			
		|||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Upgrade a plugin from uri.
 | 
			
		||||
     * @param {string} name
 | 
			
		||||
     * @param {UpgradeFromUriRequest} upgradeFromUriRequest
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    async upgradePluginFromUri(
 | 
			
		||||
      name: string,
 | 
			
		||||
      upgradeFromUriRequest: UpgradeFromUriRequest,
 | 
			
		||||
      options?: AxiosRequestConfig
 | 
			
		||||
    ): Promise<
 | 
			
		||||
      (axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>
 | 
			
		||||
    > {
 | 
			
		||||
      const localVarAxiosArgs =
 | 
			
		||||
        await localVarAxiosParamCreator.upgradePluginFromUri(
 | 
			
		||||
          name,
 | 
			
		||||
          upgradeFromUriRequest,
 | 
			
		||||
          options
 | 
			
		||||
        );
 | 
			
		||||
      return createRequestFunction(
 | 
			
		||||
        localVarAxiosArgs,
 | 
			
		||||
        globalAxios,
 | 
			
		||||
        BASE_PATH,
 | 
			
		||||
        configuration
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -924,6 +1109,20 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFactory = function (
 | 
			
		|||
        )
 | 
			
		||||
        .then((request) => request(axios, basePath));
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Install a plugin from uri.
 | 
			
		||||
     * @param {ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest} requestParameters Request parameters.
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    installPluginFromUri(
 | 
			
		||||
      requestParameters: ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest,
 | 
			
		||||
      options?: AxiosRequestConfig
 | 
			
		||||
    ): AxiosPromise<Plugin> {
 | 
			
		||||
      return localVarFp
 | 
			
		||||
        .installPluginFromUri(requestParameters.installFromUriRequest, options)
 | 
			
		||||
        .then((request) => request(axios, basePath));
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * List all plugin presets in the system.
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
| 
						 | 
				
			
			@ -1025,6 +1224,24 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFactory = function (
 | 
			
		|||
        )
 | 
			
		||||
        .then((request) => request(axios, basePath));
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Upgrade a plugin from uri.
 | 
			
		||||
     * @param {ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest} requestParameters Request parameters.
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     */
 | 
			
		||||
    upgradePluginFromUri(
 | 
			
		||||
      requestParameters: ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest,
 | 
			
		||||
      options?: AxiosRequestConfig
 | 
			
		||||
    ): AxiosPromise<Plugin> {
 | 
			
		||||
      return localVarFp
 | 
			
		||||
        .upgradePluginFromUri(
 | 
			
		||||
          requestParameters.name,
 | 
			
		||||
          requestParameters.upgradeFromUriRequest,
 | 
			
		||||
          options
 | 
			
		||||
        )
 | 
			
		||||
        .then((request) => request(axios, basePath));
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1084,6 +1301,20 @@ export interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginRequest {
 | 
			
		|||
  readonly source?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Request parameters for installPluginFromUri operation in ApiConsoleHaloRunV1alpha1PluginApi.
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest
 | 
			
		||||
 */
 | 
			
		||||
export interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest {
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @type {InstallFromUriRequest}
 | 
			
		||||
   * @memberof ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUri
 | 
			
		||||
   */
 | 
			
		||||
  readonly installFromUriRequest: InstallFromUriRequest;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Request parameters for listPlugins operation in ApiConsoleHaloRunV1alpha1PluginApi.
 | 
			
		||||
 * @export
 | 
			
		||||
| 
						 | 
				
			
			@ -1224,6 +1455,27 @@ export interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginRequest {
 | 
			
		|||
  readonly source?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Request parameters for upgradePluginFromUri operation in ApiConsoleHaloRunV1alpha1PluginApi.
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest
 | 
			
		||||
 */
 | 
			
		||||
export interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest {
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @type {string}
 | 
			
		||||
   * @memberof ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUri
 | 
			
		||||
   */
 | 
			
		||||
  readonly name: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @type {UpgradeFromUriRequest}
 | 
			
		||||
   * @memberof ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUri
 | 
			
		||||
   */
 | 
			
		||||
  readonly upgradeFromUriRequest: UpgradeFromUriRequest;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ApiConsoleHaloRunV1alpha1PluginApi - object-oriented interface
 | 
			
		||||
 * @export
 | 
			
		||||
| 
						 | 
				
			
			@ -1284,6 +1536,22 @@ export class ApiConsoleHaloRunV1alpha1PluginApi extends BaseAPI {
 | 
			
		|||
      .then((request) => request(this.axios, this.basePath));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Install a plugin from uri.
 | 
			
		||||
   * @param {ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest} requestParameters Request parameters.
 | 
			
		||||
   * @param {*} [options] Override http request option.
 | 
			
		||||
   * @throws {RequiredError}
 | 
			
		||||
   * @memberof ApiConsoleHaloRunV1alpha1PluginApi
 | 
			
		||||
   */
 | 
			
		||||
  public installPluginFromUri(
 | 
			
		||||
    requestParameters: ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest,
 | 
			
		||||
    options?: AxiosRequestConfig
 | 
			
		||||
  ) {
 | 
			
		||||
    return ApiConsoleHaloRunV1alpha1PluginApiFp(this.configuration)
 | 
			
		||||
      .installPluginFromUri(requestParameters.installFromUriRequest, options)
 | 
			
		||||
      .then((request) => request(this.axios, this.basePath));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * List all plugin presets in the system.
 | 
			
		||||
   * @param {*} [options] Override http request option.
 | 
			
		||||
| 
						 | 
				
			
			@ -1394,4 +1662,24 @@ export class ApiConsoleHaloRunV1alpha1PluginApi extends BaseAPI {
 | 
			
		|||
      )
 | 
			
		||||
      .then((request) => request(this.axios, this.basePath));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Upgrade a plugin from uri.
 | 
			
		||||
   * @param {ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest} requestParameters Request parameters.
 | 
			
		||||
   * @param {*} [options] Override http request option.
 | 
			
		||||
   * @throws {RequiredError}
 | 
			
		||||
   * @memberof ApiConsoleHaloRunV1alpha1PluginApi
 | 
			
		||||
   */
 | 
			
		||||
  public upgradePluginFromUri(
 | 
			
		||||
    requestParameters: ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest,
 | 
			
		||||
    options?: AxiosRequestConfig
 | 
			
		||||
  ) {
 | 
			
		||||
    return ApiConsoleHaloRunV1alpha1PluginApiFp(this.configuration)
 | 
			
		||||
      .upgradePluginFromUri(
 | 
			
		||||
        requestParameters.name,
 | 
			
		||||
        requestParameters.upgradeFromUriRequest,
 | 
			
		||||
        options
 | 
			
		||||
      )
 | 
			
		||||
      .then((request) => request(this.axios, this.basePath));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -733,6 +733,9 @@ core:
 | 
			
		|||
      change_status:
 | 
			
		||||
        active_title: Are you sure you want to active this plugin?
 | 
			
		||||
        inactive_title: Are you sure you want to inactive this plugin?
 | 
			
		||||
      remote_download:
 | 
			
		||||
        title: Remote download address detected, do you want to download?
 | 
			
		||||
        description: "Please carefully verify whether this address can be trusted: {url}"
 | 
			
		||||
    filters:
 | 
			
		||||
      status:
 | 
			
		||||
        items:
 | 
			
		||||
| 
						 | 
				
			
			@ -750,6 +753,12 @@ core:
 | 
			
		|||
      titles:
 | 
			
		||||
        install: Install plugin
 | 
			
		||||
        upgrade: Upgrade plugin ({display_name})
 | 
			
		||||
      tabs:
 | 
			
		||||
        local: Local
 | 
			
		||||
        remote:
 | 
			
		||||
          title: Remote
 | 
			
		||||
          fields:
 | 
			
		||||
            url: Remote URL
 | 
			
		||||
      operations:
 | 
			
		||||
        active_after_install:
 | 
			
		||||
          title: Install successful
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -733,6 +733,9 @@ core:
 | 
			
		|||
      change_status:
 | 
			
		||||
        active_title: 确定要启用该插件吗?
 | 
			
		||||
        inactive_title: 确定要停用该插件吗?
 | 
			
		||||
      remote_download:
 | 
			
		||||
        title: 检测到了远程下载地址,是否需要下载?
 | 
			
		||||
        description: 请仔细鉴别此地址是否可信:{url}
 | 
			
		||||
    filters:
 | 
			
		||||
      status:
 | 
			
		||||
        items:
 | 
			
		||||
| 
						 | 
				
			
			@ -750,6 +753,12 @@ core:
 | 
			
		|||
      titles:
 | 
			
		||||
        install: 安装插件
 | 
			
		||||
        upgrade: 升级插件({display_name})
 | 
			
		||||
      tabs:
 | 
			
		||||
        local: 本地上传
 | 
			
		||||
        remote:
 | 
			
		||||
          title: 远程下载
 | 
			
		||||
          fields:
 | 
			
		||||
            url: 下载地址
 | 
			
		||||
      operations:
 | 
			
		||||
        active_after_install:
 | 
			
		||||
          title: 安装成功
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -733,6 +733,9 @@ core:
 | 
			
		|||
      change_status:
 | 
			
		||||
        active_title: 確定要啟用該插件嗎?
 | 
			
		||||
        inactive_title: 確定要停用該插件嗎?
 | 
			
		||||
      remote_download:
 | 
			
		||||
        title: 偵測到遠端下載地址,是否需要下載?
 | 
			
		||||
        description: 請仔細鑑別此地址是否可信:{url}
 | 
			
		||||
    filters:
 | 
			
		||||
      status:
 | 
			
		||||
        items:
 | 
			
		||||
| 
						 | 
				
			
			@ -750,6 +753,12 @@ core:
 | 
			
		|||
      titles:
 | 
			
		||||
        install: 安裝插件
 | 
			
		||||
        upgrade: 升級插件({display_name})
 | 
			
		||||
      tabs:
 | 
			
		||||
        local: 本地上傳
 | 
			
		||||
        remote:
 | 
			
		||||
          title: 遠端下載
 | 
			
		||||
          fields:
 | 
			
		||||
            url: 下載地址
 | 
			
		||||
      operations:
 | 
			
		||||
        active_after_install:
 | 
			
		||||
          title: 安裝成功
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,10 +13,11 @@ import {
 | 
			
		|||
  VLoading,
 | 
			
		||||
  VDropdown,
 | 
			
		||||
  VDropdownItem,
 | 
			
		||||
  Dialog,
 | 
			
		||||
} from "@halo-dev/components";
 | 
			
		||||
import PluginListItem from "./components/PluginListItem.vue";
 | 
			
		||||
import PluginUploadModal from "./components/PluginUploadModal.vue";
 | 
			
		||||
import { computed, ref } from "vue";
 | 
			
		||||
import { computed, ref, onMounted } from "vue";
 | 
			
		||||
import { apiClient } from "@/utils/api-client";
 | 
			
		||||
import { usePermission } from "@/utils/permission";
 | 
			
		||||
import FilterTag from "@/components/filter/FilterTag.vue";
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +26,7 @@ import { getNode } from "@formkit/core";
 | 
			
		|||
import { useQuery } from "@tanstack/vue-query";
 | 
			
		||||
import type { Plugin } from "@halo-dev/api-client";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import { useRouteQuery } from "@vueuse/router";
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -146,12 +148,34 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
 | 
			
		|||
    return deletingPlugins?.length ? 3000 : false;
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// handle remote download url from route
 | 
			
		||||
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
 | 
			
		||||
  "remote-download-url"
 | 
			
		||||
);
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (routeRemoteDownloadUrl.value) {
 | 
			
		||||
    Dialog.warning({
 | 
			
		||||
      title: t("core.plugin.operations.remote_download.title"),
 | 
			
		||||
      description: t("core.plugin.operations.remote_download.description", {
 | 
			
		||||
        url: routeRemoteDownloadUrl.value,
 | 
			
		||||
      }),
 | 
			
		||||
      confirmText: t("core.common.buttons.download"),
 | 
			
		||||
      cancelText: t("core.common.buttons.cancel"),
 | 
			
		||||
      onConfirm() {
 | 
			
		||||
        pluginInstall.value = true;
 | 
			
		||||
      },
 | 
			
		||||
      onCancel() {
 | 
			
		||||
        routeRemoteDownloadUrl.value = null;
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <PluginUploadModal
 | 
			
		||||
    v-if="currentUserHasPermission(['system:plugins:manage'])"
 | 
			
		||||
    v-model:visible="pluginInstall"
 | 
			
		||||
    @close="refetch()"
 | 
			
		||||
  />
 | 
			
		||||
 | 
			
		||||
  <VPageHeader :title="$t('core.plugin.title')">
 | 
			
		||||
| 
						 | 
				
			
			@ -324,7 +348,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
 | 
			
		|||
          role="list"
 | 
			
		||||
        >
 | 
			
		||||
          <li v-for="plugin in data" :key="plugin.metadata.name">
 | 
			
		||||
            <PluginListItem :plugin="plugin" @reload="refetch()" />
 | 
			
		||||
            <PluginListItem :plugin="plugin" />
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </Transition>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,10 +32,6 @@ const props = withDefaults(
 | 
			
		|||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (event: "reload"): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const { plugin } = toRefs(props);
 | 
			
		||||
 | 
			
		||||
const upgradeModal = ref(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -43,10 +39,6 @@ const upgradeModal = ref(false);
 | 
			
		|||
const { getFailedMessage, changeStatus, uninstall } =
 | 
			
		||||
  usePluginLifeCycle(plugin);
 | 
			
		||||
 | 
			
		||||
const onUpgradeModalClose = () => {
 | 
			
		||||
  emit("reload");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleResetSettingConfig = async () => {
 | 
			
		||||
  Dialog.warning({
 | 
			
		||||
    title: t("core.plugin.operations.reset.title"),
 | 
			
		||||
| 
						 | 
				
			
			@ -73,11 +65,7 @@ const handleResetSettingConfig = async () => {
 | 
			
		|||
};
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <PluginUploadModal
 | 
			
		||||
    v-model:visible="upgradeModal"
 | 
			
		||||
    :upgrade-plugin="plugin"
 | 
			
		||||
    @close="onUpgradeModalClose"
 | 
			
		||||
  />
 | 
			
		||||
  <PluginUploadModal v-model:visible="upgradeModal" :upgrade-plugin="plugin" />
 | 
			
		||||
  <VEntity>
 | 
			
		||||
    <template #start>
 | 
			
		||||
      <VEntityField>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,25 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { VModal, Dialog, Toast } from "@halo-dev/components";
 | 
			
		||||
import {
 | 
			
		||||
  VModal,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  Toast,
 | 
			
		||||
  VTabs,
 | 
			
		||||
  VTabItem,
 | 
			
		||||
  VButton,
 | 
			
		||||
} from "@halo-dev/components";
 | 
			
		||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
 | 
			
		||||
import { apiClient } from "@/utils/api-client";
 | 
			
		||||
import type { Plugin } from "@halo-dev/api-client";
 | 
			
		||||
import { computed, ref, watch } from "vue";
 | 
			
		||||
import { computed, ref, watch, nextTick } from "vue";
 | 
			
		||||
import type { SuccessResponse, ErrorResponse } from "@uppy/core";
 | 
			
		||||
import type { UppyFile } from "@uppy/utils";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import { useQueryClient } from "@tanstack/vue-query";
 | 
			
		||||
import { useRouteQuery } from "@vueuse/router";
 | 
			
		||||
import { submitForm } from "@formkit/core";
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const queryClient = useQueryClient();
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
| 
						 | 
				
			
			@ -57,8 +68,13 @@ const onUploaded = async (response: SuccessResponse) => {
 | 
			
		|||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const plugin = response.body as Plugin;
 | 
			
		||||
  handleVisibleChange(false);
 | 
			
		||||
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
 | 
			
		||||
 | 
			
		||||
  handleShowActiveModalAfterInstall(response.body as Plugin);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleShowActiveModalAfterInstall = (plugin: Plugin) => {
 | 
			
		||||
  Dialog.success({
 | 
			
		||||
    title: t("core.plugin.upload_modal.operations.active_after_install.title"),
 | 
			
		||||
    description: t(
 | 
			
		||||
| 
						 | 
				
			
			@ -103,30 +119,49 @@ const PLUGIN_ALREADY_EXISTS_TYPE =
 | 
			
		|||
 | 
			
		||||
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
 | 
			
		||||
  const body = response.body as PluginInstallationErrorResponse;
 | 
			
		||||
 | 
			
		||||
  if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
 | 
			
		||||
    Dialog.info({
 | 
			
		||||
      title: t(
 | 
			
		||||
        "core.plugin.upload_modal.operations.existed_during_installation.title"
 | 
			
		||||
      ),
 | 
			
		||||
      description: t(
 | 
			
		||||
        "core.plugin.upload_modal.operations.existed_during_installation.description"
 | 
			
		||||
      ),
 | 
			
		||||
      confirmText: t("core.common.buttons.confirm"),
 | 
			
		||||
      cancelText: t("core.common.buttons.cancel"),
 | 
			
		||||
      onConfirm: async () => {
 | 
			
		||||
        await apiClient.plugin.upgradePlugin({
 | 
			
		||||
          name: body.pluginName,
 | 
			
		||||
          file: file.data as File,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Toast.success(t("core.common.toast.upgrade_success"));
 | 
			
		||||
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    handleCatchExistsException(body, file.data as File);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCatchExistsException = async (
 | 
			
		||||
  error: PluginInstallationErrorResponse,
 | 
			
		||||
  file?: File
 | 
			
		||||
) => {
 | 
			
		||||
  Dialog.info({
 | 
			
		||||
    title: t(
 | 
			
		||||
      "core.plugin.upload_modal.operations.existed_during_installation.title"
 | 
			
		||||
    ),
 | 
			
		||||
    description: t(
 | 
			
		||||
      "core.plugin.upload_modal.operations.existed_during_installation.description"
 | 
			
		||||
    ),
 | 
			
		||||
    confirmText: t("core.common.buttons.confirm"),
 | 
			
		||||
    cancelText: t("core.common.buttons.cancel"),
 | 
			
		||||
    onConfirm: async () => {
 | 
			
		||||
      if (activeTabId.value === "local") {
 | 
			
		||||
        await apiClient.plugin.upgradePlugin({
 | 
			
		||||
          name: error.pluginName,
 | 
			
		||||
          file: file,
 | 
			
		||||
        });
 | 
			
		||||
      } else if (activeTabId.value === "remote") {
 | 
			
		||||
        await apiClient.plugin.upgradePluginFromUri({
 | 
			
		||||
          name: error.pluginName,
 | 
			
		||||
          upgradeFromUriRequest: {
 | 
			
		||||
            uri: remoteDownloadUrl.value,
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new Error("Unknown tab id");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      Toast.success(t("core.common.toast.upgrade_success"));
 | 
			
		||||
 | 
			
		||||
      window.location.reload();
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.visible,
 | 
			
		||||
  (newValue) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,24 +175,120 @@ watch(
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// remote download
 | 
			
		||||
const activeTabId = ref("local");
 | 
			
		||||
const remoteDownloadUrl = ref("");
 | 
			
		||||
const downloading = ref(false);
 | 
			
		||||
 | 
			
		||||
const handleDownloadPlugin = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    downloading.value = true;
 | 
			
		||||
    if (props.upgradePlugin) {
 | 
			
		||||
      await apiClient.plugin.upgradePluginFromUri({
 | 
			
		||||
        name: props.upgradePlugin.metadata.name,
 | 
			
		||||
        upgradeFromUriRequest: {
 | 
			
		||||
          uri: remoteDownloadUrl.value,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      Toast.success(t("core.common.toast.upgrade_success"));
 | 
			
		||||
      window.location.reload();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { data: plugin } = await apiClient.plugin.installPluginFromUri({
 | 
			
		||||
      installFromUriRequest: {
 | 
			
		||||
        uri: remoteDownloadUrl.value,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    handleVisibleChange(false);
 | 
			
		||||
    queryClient.invalidateQueries({ queryKey: ["plugins"] });
 | 
			
		||||
 | 
			
		||||
    handleShowActiveModalAfterInstall(plugin);
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    const data = error?.response.data as PluginInstallationErrorResponse;
 | 
			
		||||
    if (data?.type === PLUGIN_ALREADY_EXISTS_TYPE) {
 | 
			
		||||
      handleCatchExistsException(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.error("Failed to download plugin", error);
 | 
			
		||||
  } finally {
 | 
			
		||||
    routeRemoteDownloadUrl.value = null;
 | 
			
		||||
    downloading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// handle remote download url from route
 | 
			
		||||
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
 | 
			
		||||
  "remote-download-url"
 | 
			
		||||
);
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.visible,
 | 
			
		||||
  (visible) => {
 | 
			
		||||
    if (routeRemoteDownloadUrl.value && visible) {
 | 
			
		||||
      activeTabId.value = "remote";
 | 
			
		||||
      remoteDownloadUrl.value = routeRemoteDownloadUrl.value;
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
        submitForm("plugin-remote-download-form");
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <VModal
 | 
			
		||||
    :visible="visible"
 | 
			
		||||
    :width="600"
 | 
			
		||||
    :title="modalTitle"
 | 
			
		||||
    :centered="false"
 | 
			
		||||
    @update:visible="handleVisibleChange"
 | 
			
		||||
  >
 | 
			
		||||
    <UppyUpload
 | 
			
		||||
      v-if="uploadVisible"
 | 
			
		||||
      :restrictions="{
 | 
			
		||||
        maxNumberOfFiles: 1,
 | 
			
		||||
        allowedFileTypes: ['.jar'],
 | 
			
		||||
      }"
 | 
			
		||||
      :endpoint="endpoint"
 | 
			
		||||
      auto-proceed
 | 
			
		||||
      @uploaded="onUploaded"
 | 
			
		||||
      @error="onError"
 | 
			
		||||
    />
 | 
			
		||||
    <VTabs v-model:active-id="activeTabId" type="outline" class="!rounded-none">
 | 
			
		||||
      <VTabItem id="local" :label="$t('core.plugin.upload_modal.tabs.local')">
 | 
			
		||||
        <UppyUpload
 | 
			
		||||
          v-if="uploadVisible"
 | 
			
		||||
          :restrictions="{
 | 
			
		||||
            maxNumberOfFiles: 1,
 | 
			
		||||
            allowedFileTypes: ['.jar'],
 | 
			
		||||
          }"
 | 
			
		||||
          :endpoint="endpoint"
 | 
			
		||||
          auto-proceed
 | 
			
		||||
          @uploaded="onUploaded"
 | 
			
		||||
          @error="onError"
 | 
			
		||||
        />
 | 
			
		||||
      </VTabItem>
 | 
			
		||||
      <VTabItem
 | 
			
		||||
        id="remote"
 | 
			
		||||
        :label="$t('core.plugin.upload_modal.tabs.remote.title')"
 | 
			
		||||
      >
 | 
			
		||||
        <FormKit
 | 
			
		||||
          id="plugin-remote-download-form"
 | 
			
		||||
          name="plugin-remote-download-form"
 | 
			
		||||
          type="form"
 | 
			
		||||
          :preserve="true"
 | 
			
		||||
          @submit="handleDownloadPlugin"
 | 
			
		||||
        >
 | 
			
		||||
          <FormKit
 | 
			
		||||
            v-model="remoteDownloadUrl"
 | 
			
		||||
            :label="$t('core.plugin.upload_modal.tabs.remote.fields.url')"
 | 
			
		||||
            type="text"
 | 
			
		||||
          ></FormKit>
 | 
			
		||||
        </FormKit>
 | 
			
		||||
 | 
			
		||||
        <div class="pt-5">
 | 
			
		||||
          <VButton
 | 
			
		||||
            :loading="downloading"
 | 
			
		||||
            type="secondary"
 | 
			
		||||
            @click="$formkit.submit('plugin-remote-download-form')"
 | 
			
		||||
          >
 | 
			
		||||
            {{ $t("core.common.buttons.download") }}
 | 
			
		||||
          </VButton>
 | 
			
		||||
        </div>
 | 
			
		||||
      </VTabItem>
 | 
			
		||||
    </VTabs>
 | 
			
		||||
  </VModal>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue