diff --git a/agent/app/api/v2/database_redis.go b/agent/app/api/v2/database_redis.go index 34ba7b62eea2..f3717e9c7307 100644 --- a/agent/app/api/v2/database_redis.go +++ b/agent/app/api/v2/database_redis.go @@ -86,17 +86,38 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) { // @Tags Database Redis // @Summary Install redis-cli -// @Success 200 +// @Accept json +// @Param request body dto.RedisCliInstall true "request" +// @Success 200 {object} dto.RedisCliStatus // @Security ApiKeyAuth // @Security Timestamp // @Router /databases/redis/install/cli [post] func (b *BaseApi) InstallCli(c *gin.Context) { - if err := redisService.InstallCli(); err != nil { + var req dto.RedisCliInstall + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := redisService.InstallCli(req) + if err != nil { helper.InternalServer(c, err) return } + helper.SuccessWithData(c, data) +} - helper.Success(c) +// @Tags Database Redis +// @Summary Load redis-cli installation status +// @Success 200 {object} dto.RedisCliStatus +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /databases/redis/cli/status [get] +func (b *BaseApi) LoadRedisCliStatus(c *gin.Context) { + data, err := redisService.LoadCliStatus() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) } // @Tags Database Redis diff --git a/agent/app/dto/database.go b/agent/app/dto/database.go index e28cf87dc849..e9f5984e8158 100644 --- a/agent/app/dto/database.go +++ b/agent/app/dto/database.go @@ -22,6 +22,17 @@ type DBBaseInfo struct { Port int64 `json:"port"` } +type RedisCliInstall struct { + TaskID string `json:"taskID" validate:"omitempty,uuid"` +} + +type RedisCliStatus struct { + Installed bool `json:"installed"` + TaskID string `json:"taskID"` + Status string `json:"status"` + ErrorMsg string `json:"errorMsg"` +} + // mysql type MysqlDBSearch struct { PageInfo diff --git a/agent/app/service/container.go b/agent/app/service/container.go index d65ef4f1fac4..87ce07f15f2c 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -484,6 +484,10 @@ func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) { } func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bool) error { + return u.containerCreate(req, inThread, "") +} + +func (u *ContainerService) containerCreate(req dto.ContainerOperate, inThread bool, taskName string) error { client, err := docker.NewDockerClient() if err != nil { return err @@ -497,7 +501,10 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo return buserr.New("ErrContainerName") } - taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) + if taskName == "" { + taskName = task.GetTaskName(req.Name, task.TaskCreate, task.TaskScopeContainer) + } + taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) if err != nil { unlock() _ = client.Close() @@ -558,6 +565,11 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo }, nil) if inThread { + if err := taskItem.Prepare(); err != nil { + unlock() + _ = client.Close() + return err + } go func() { defer unlock() defer client.Close() diff --git a/agent/app/service/database_redis.go b/agent/app/service/database_redis.go index c895cfab7c34..ac3064b47426 100644 --- a/agent/app/service/database_redis.go +++ b/agent/app/service/database_redis.go @@ -8,6 +8,12 @@ import ( "os" "os/exec" "strings" + "sync" + + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/google/uuid" + "gorm.io/gorm" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/global" @@ -23,6 +29,11 @@ import ( type RedisService struct{} +const redisCliTaskName = "RedisCliEnable" + +// The CLI container is shared by all remote Redis databases on this node. +var redisCliInstallMutex sync.Mutex + type IRedisService interface { UpdateConf(req dto.RedisConfUpdate) error UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error @@ -33,7 +44,8 @@ type IRedisService interface { LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) CheckHasCli() bool - InstallCli() error + InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) + LoadCliStatus() (*dto.RedisCliStatus, error) } func NewIRedisService() IRedisService { @@ -71,20 +83,62 @@ func (u *RedisService) CheckHasCli() bool { return false } for _, item := range containerLists { - if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" { + if len(item.Names) > 0 && strings.TrimPrefix(item.Names[0], "/") == "1Panel-redis-cli-tools" { return true } } return false } -func (u *RedisService) InstallCli() error { +func (u *RedisService) LoadCliStatus() (*dto.RedisCliStatus, error) { + result := &dto.RedisCliStatus{} + latest, err := taskRepo.GetFirst(repo.WithByName(redisCliTaskName), repo.WithByType(task.TaskScopeContainer), repo.WithOrderDesc("created_at")) + if errors.Is(err, gorm.ErrRecordNotFound) { + result.Installed = u.CheckHasCli() + return result, nil + } + if err != nil { + return nil, err + } + result.TaskID = latest.ID + result.Status = latest.Status + result.ErrorMsg = latest.ErrorMsg + result.Installed = u.CheckHasCli() + return result, nil +} + +func (u *RedisService) InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) { + if !redisCliInstallMutex.TryLock() { + return nil, buserr.New("TaskIsExecuting") + } + defer redisCliInstallMutex.Unlock() + status, err := u.LoadCliStatus() + if err != nil { + return nil, err + } + if status.Status == constant.StatusExecuting || status.Installed { + return status, nil + } + if req.TaskID == "" { + req.TaskID = uuid.NewString() + } + // Never reuse an existing task ID: doing so would truncate its log. + if _, err := taskRepo.GetFirst(taskRepo.WithByID(req.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) { + if err != nil { + return nil, err + } + return nil, buserr.New("TaskIsExecuting") + } item := dto.ContainerOperate{ + TaskID: req.TaskID, Name: "1Panel-redis-cli-tools", Image: "redis:7.4.4", Networks: []dto.ContainerNetwork{{Network: "1panel-network"}}, } - return NewIContainerService().ContainerCreate(item, false) + if err := (&ContainerService{}).containerCreate(item, true, redisCliTaskName); err != nil { + return nil, err + } + return &dto.RedisCliStatus{TaskID: req.TaskID, Status: constant.StatusExecuting}, nil } func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { diff --git a/agent/app/service/image.go b/agent/app/service/image.go index b8f7e65875ee..635815f2830d 100644 --- a/agent/app/service/image.go +++ b/agent/app/service/image.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "os" @@ -325,18 +324,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error { } go func() { - client, err := docker.NewDockerClient() - if err != nil { - taskItem.Log("Failed to create Docker client: " + err.Error()) - return - } - defer client.Close() - for _, itemPath := range req.Paths { currentPath := itemPath itemName := path.Base(currentPath) taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error { taskItem.Logf("----------------- %s -----------------", itemName) + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() file, err := os.Open(currentPath) if err != nil { return err @@ -347,14 +344,9 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error { return err } defer res.Body.Close() - content, err := io.ReadAll(res.Body) - if err != nil { - return err - } - if strings.Contains(string(content), "Error") { - return errors.New(string(content)) - } - return nil + return consumeImageLoadResponse(res.Body, func(message string) { + taskItem.Log(message) + }) }, nil) } _ = taskItem.Execute() diff --git a/agent/app/service/image_load_response.go b/agent/app/service/image_load_response.go new file mode 100644 index 000000000000..c900f22321d4 --- /dev/null +++ b/agent/app/service/image_load_response.go @@ -0,0 +1,37 @@ +package service + +import ( + "encoding/json" + "errors" + "io" + "strings" +) + +// Docker may report load failures in a successful HTTP response's JSON stream. +func consumeImageLoadResponse(reader io.Reader, log func(string)) error { + decoder := json.NewDecoder(reader) + for { + var message struct { + Stream string `json:"stream"` + Error string `json:"error"` + ErrorDetail struct { + Message string `json:"message"` + } `json:"errorDetail"` + } + if err := decoder.Decode(&message); err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + if message.Error != "" { + return errors.New(message.Error) + } + if message.ErrorDetail.Message != "" { + return errors.New(message.ErrorDetail.Message) + } + if text := strings.TrimSpace(message.Stream); text != "" { + log(text) + } + } +} diff --git a/agent/app/task/task.go b/agent/app/task/task.go index 2425d80c7729..e30441cb1775 100644 --- a/agent/app/task/task.go +++ b/agent/app/task/task.go @@ -283,8 +283,18 @@ func (t *Task) updateTask(task *model.Task) { _ = t.taskRepo.Update(context.Background(), task) } -func (t *Task) Execute() error { +// Prepare makes a task visible before dispatching it to a background worker. +func (t *Task) Prepare() error { if err := t.taskRepo.Save(context.Background(), t.Task); err != nil { + _ = t.logFile.Close() + global.RemoveTaskCancel(t.TaskID) + return err + } + return nil +} + +func (t *Task) Execute() error { + if err := t.Prepare(); err != nil { return err } var err error diff --git a/agent/router/ro_database.go b/agent/router/ro_database.go index 95f57223bb91..13d58c7cbe84 100644 --- a/agent/router/ro_database.go +++ b/agent/router/ro_database.go @@ -43,6 +43,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) { cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus) cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf) cmdRouter.GET("/redis/check", baseApi.CheckHasCli) + cmdRouter.GET("/redis/cli/status", baseApi.LoadRedisCliStatus) cmdRouter.POST("/redis/install/cli", baseApi.InstallCli) cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf) diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 3dad4da3bf53..90bb696b9466 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -15010,6 +15010,30 @@ const docTemplate = `{ ] } }, + "/databases/redis/cli/status": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisCliStatus" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load redis-cli installation status", + "tags": [ + "Database Redis" + ] + } + }, "/databases/redis/conf": { "post": { "consumes": [ @@ -15092,9 +15116,26 @@ const docTemplate = `{ }, "/databases/redis/install/cli": { "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisCliInstall" + } + } + ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisCliStatus" + } } }, "security": [ @@ -39731,6 +39772,31 @@ const docTemplate = `{ ], "type": "object" }, + "dto.RedisCliInstall": { + "type": "object", + "properties": { + "taskID": { + "type": "string" + } + } + }, + "dto.RedisCliStatus": { + "type": "object", + "properties": { + "errorMsg": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "taskID": { + "type": "string" + } + } + }, "dto.RedisConf": { "properties": { "containerName": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 5ebf2ce42810..f4fac9b42c9e 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -15006,6 +15006,30 @@ ] } }, + "/databases/redis/cli/status": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisCliStatus" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load redis-cli installation status", + "tags": [ + "Database Redis" + ] + } + }, "/databases/redis/conf": { "post": { "consumes": [ @@ -15088,9 +15112,26 @@ }, "/databases/redis/install/cli": { "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisCliInstall" + } + } + ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisCliStatus" + } } }, "security": [ @@ -39727,6 +39768,31 @@ ], "type": "object" }, + "dto.RedisCliInstall": { + "type": "object", + "properties": { + "taskID": { + "type": "string" + } + } + }, + "dto.RedisCliStatus": { + "type": "object", + "properties": { + "errorMsg": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "taskID": { + "type": "string" + } + } + }, "dto.RedisConf": { "properties": { "containerName": { diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index fe29bd9f8fd7..33e4ec370056 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -1,6 +1,13 @@ import { ReqPage } from '.'; export namespace Database { + export interface RedisCliStatus { + installed: boolean; + taskID: string; + status: string; + errorMsg: string; + } + export interface SearchDBWithPage { info: string; database: string; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index e5176cfa515a..731b5f4bf056 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -144,8 +144,13 @@ export const imagePull = (params: Container.ImagePull) => { export const imagePush = (params: Container.ImagePush) => { return http.post(`/containers/image/push`, params); }; -export const imageLoad = (params: Container.ImageLoad) => { - return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M); +export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => { + return http.post( + `/containers/image/load`, + params, + TimeoutEnum.T_10M, + currentNode ? { CurrentNode: currentNode } : undefined, + ); }; export const imageSave = (params: Container.ImageSave) => { return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M); diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index 08910a1002a3..2840b7a8e552 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -193,8 +193,13 @@ export const redisPersistenceConf = (type: string, database: string) => { export const checkRedisCli = () => { return http.get(`/databases/redis/check`); }; -export const installRedisCli = () => { - return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M); +export const loadRedisCliStatus = (node: string) => { + return http.get(`/databases/redis/cli/status?operateNode=${encodeURIComponent(node)}`); +}; +export const installRedisCli = (taskID: string, node: string) => { + return http.post(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, { + taskID, + }); }; export const changeRedisPassword = (database: string, password: string) => { if (password) { diff --git a/frontend/src/components/table/TableSearch.vue b/frontend/src/components/table/TableSearch.vue index f965fb385536..f53ac9fb41d6 100644 --- a/frontend/src/components/table/TableSearch.vue +++ b/frontend/src/components/table/TableSearch.vue @@ -16,10 +16,12 @@ diff --git a/frontend/src/views/database/redis/index.vue b/frontend/src/views/database/redis/index.vue index fbdc193660f9..e333edf64d85 100644 --- a/frontend/src/views/database/redis/index.vue +++ b/frontend/src/views/database/redis/index.vue @@ -81,8 +81,23 @@ :style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }" :description="loadErrMsg()" > - - {{ $t('commons.button.enable') }} + + {{ cliInstalling ? $t('database.redisCliEnabling') : $t('commons.button.enable') }} + + + {{ $t('commons.button.log') }}
@@ -134,6 +149,7 @@ +
@@ -143,17 +159,21 @@ import Conn from '@/views/database/redis/conn/index.vue'; import Terminal from '@/components/terminal/index.vue'; import AppStatus from '@/components/app-status/index.vue'; import QuickCmd from '@/views/database/redis/command/index.vue'; -import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; +import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { checkAppInstalled } from '@/api/modules/app'; import { useGlobalStore } from '@/composables/useGlobalStore'; -import { listDatabases, checkRedisCli, installRedisCli } from '@/api/modules/database'; +import { listDatabases, loadRedisCliStatus, installRedisCli } from '@/api/modules/database'; import { Database } from '@/api/interface/database'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; import i18n from '@/lang'; import { getCommandList } from '@/api/modules/command'; import { routerToName, routerToNameWithQuery } from '@/utils/router'; import { useOperateNodeContext } from '@/composables/useOperateNodeContext'; +import TaskLog from '@/components/log/task/index.vue'; +import bus from '@/global/bus'; +import { newUUID } from '@/utils/id'; + const { currentNode, currentRedisDB, openMenuTabs } = useGlobalStore(); useOperateNodeContext(currentNode); @@ -170,7 +190,21 @@ const appStatusRef = ref(); const open = ref(false); -const redisCliExist = ref(); +const redisCliExist = ref(false); +const cliSubmitting = ref(false); +const cliStatusLoaded = ref(false); +const cliTaskID = ref(''); +const cliTaskStatus = ref(''); +const cliTaskLogRef = ref>(); +const cliInstalling = computed(() => cliTaskStatus.value === 'Executing'); +const cliStatusText = computed(() => { + if (cliInstalling.value) return i18n.global.t('database.redisCliEnabling'); + if (cliTaskStatus.value === 'Failed') return i18n.global.t('database.redisCliEnableFailed'); + return redisCliExist.value ? i18n.global.t('database.redisCliEnabled') : i18n.global.t('database.redisCliHelper'); +}); +let cliPollTimer: ReturnType; +let cliGeneration = 0; +let disposed = false; const appKey = ref('redis'); const appName = ref(); @@ -248,8 +282,10 @@ const changeDatabase = async () => { }; const loadDBOptions = async () => { + const generation = cliGeneration; try { const res = await listDatabases('redis,redis-cluster'); + if (disposed || generation !== cliGeneration) return; let datas = res.data || []; dbOptionsLocal.value = []; dbOptionsRemote.value = []; @@ -286,14 +322,14 @@ const loadDBOptions = async () => { reOpenTerminal(); } } finally { - isLoaded.value = true; + if (!disposed && generation === cliGeneration) isLoaded.value = true; } }; const loadErrMsg = () => { return currentDB.value.from === 'local' ? i18n.global.t('commons.service.serviceNotStarted', ['Redis']) - : i18n.global.t('database.redisCliHelper'); + : cliStatusText.value; }; const reOpenTerminal = async () => { closeTerminal(false); @@ -301,6 +337,10 @@ const reOpenTerminal = async () => { }; const initTerminal = async () => { + if (disposed || !currentDB.value) return; + const generation = cliGeneration; + const database = currentDBName.value; + const isCurrent = () => !disposed && generation === cliGeneration && database === currentDBName.value; loading.value = true; if (currentDB.value.from === 'remote') { if (!redisCliExist.value) { @@ -311,9 +351,10 @@ const initTerminal = async () => { loading.value = false; redisIsExist.value = true; nextTick(() => { + if (!isCurrent()) return; terminalShow.value = true; redisStatus.value = 'Running'; - terminalRef.value.acceptParams({ + terminalRef.value?.acceptParams({ endpoint: '/api/v2/hosts/terminal/container', args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`, error: '', @@ -325,13 +366,15 @@ const initTerminal = async () => { } await checkAppInstalled(currentDB.value.type, currentDBName.value) .then((res) => { + if (!isCurrent()) return; redisIsExist.value = res.data.isExist; redisStatus.value = res.data.status; loading.value = false; nextTick(() => { + if (!isCurrent()) return; if (res.data.status === 'Running') { terminalShow.value = true; - terminalRef.value.acceptParams({ + terminalRef.value?.acceptParams({ endpoint: '/api/v2/hosts/terminal/container', args: `source=${currentDB.value.type}&name=${currentDBName.value}&from=${currentDB.value.from}`, error: '', @@ -342,6 +385,7 @@ const initTerminal = async () => { isRefresh.value = !isRefresh.value; }) .catch(() => { + if (!isCurrent()) return; closeTerminal(false); loading.value = false; }); @@ -352,28 +396,78 @@ const closeTerminal = async (isKeepShow: boolean) => { terminalShow.value = isKeepShow; }; +const openCliTaskLog = () => { + if (cliTaskID.value) cliTaskLogRef.value?.openWithTaskID(cliTaskID.value, cliInstalling.value, currentNode.value); +}; + +const applyCliStatus = (status: Database.RedisCliStatus) => { + const wasInstalling = cliInstalling.value; + const wasInstalled = redisCliExist.value; + redisCliExist.value = status.installed; + cliTaskID.value = status.taskID; + cliTaskStatus.value = status.status; + cliStatusLoaded.value = true; + if (!wasInstalled && status.installed && !cliInstalling.value && currentDB.value?.from === 'remote') { + reOpenTerminal(); + } + if (wasInstalling && !cliInstalling.value) { + bus.emit('refreshTask', true); + if (status.status === 'Success' && status.installed) { + MsgSuccess(i18n.global.t('database.redisCliEnabled')); + } else if (status.status === 'Failed') { + MsgError(i18n.global.t('database.redisCliEnableFailed')); + } + } +}; + +const scheduleCliPoll = (generation: number) => { + clearTimeout(cliPollTimer); + if (disposed || generation !== cliGeneration) return; + cliPollTimer = setTimeout(() => refreshCliStatus(generation), 2000); +}; + +const refreshCliStatus = async (generation: number) => { + try { + const res = await loadRedisCliStatus(currentNode.value); + if (disposed || generation !== cliGeneration) return; + applyCliStatus(res.data); + if (cliInstalling.value) scheduleCliPoll(generation); + } catch { + // Keep tracking a running task after a transient request failure. + if (!disposed && generation === cliGeneration) scheduleCliPoll(generation); + } +}; + const checkCliValid = async () => { - await checkRedisCli() - .then((res) => { - redisCliExist.value = res.data; - loadDBOptions(); - }) - .catch(() => { - loadDBOptions(); - }); + const generation = cliGeneration; + await refreshCliStatus(generation); + if (!disposed && generation === cliGeneration) await loadDBOptions(); }; + const installCli = async () => { - loading.value = true; - await installRedisCli() - .then(() => { - loading.value = false; - redisCliExist.value = true; - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + if (cliSubmitting.value || cliInstalling.value) return; + const generation = cliGeneration; + const node = currentNode.value; + cliSubmitting.value = true; + clearTimeout(cliPollTimer); + try { + const res = await installRedisCli(newUUID(), node); + if (disposed || generation !== cliGeneration) return; + applyCliStatus(res.data); + bus.emit('refreshTask', true); + if (cliInstalling.value) { + MsgSuccess(i18n.global.t('database.redisCliTaskSubmitted')); + openCliTaskLog(); + scheduleCliPoll(generation); + } else if (res.data.installed && currentDB.value?.from === 'remote') { reOpenTerminal(); - }) - .catch(() => { - loading.value = false; - }); + } + } catch { + // The server may have accepted the task even if the response was lost. + if (!disposed && generation === cliGeneration) await refreshCliStatus(generation); + } finally { + if (!disposed && generation === cliGeneration) cliSubmitting.value = false; + } }; const loadQuickCmd = async () => { @@ -392,6 +486,25 @@ const onSetQuickCmd = () => { dialogQuickCmdRef.value.acceptParams(); }; +watch(currentNode, () => { + cliGeneration++; + clearTimeout(cliPollTimer); + cliTaskLogRef.value?.handleClose(); + closeTerminal(false); + redisCliExist.value = false; + cliSubmitting.value = false; + cliStatusLoaded.value = false; + cliTaskID.value = ''; + cliTaskStatus.value = ''; + currentDB.value = undefined; + redisStatus.value = undefined; + dbOptionsLocal.value = []; + dbOptionsRemote.value = []; + isLoaded.value = false; + loading.value = false; + checkCliValid(); +}); + onMounted(() => { loadQuickCmd(); checkCliValid(); @@ -403,6 +516,9 @@ const onAfter = () => { initTerminal(); }; onBeforeUnmount(() => { + disposed = true; + cliGeneration++; + clearTimeout(cliPollTimer); closeTerminal(false); }); diff --git a/frontend/src/views/database/redis/setting/persistence/index.vue b/frontend/src/views/database/redis/setting/persistence/index.vue index a94bb001e85e..55b0c2567037 100644 --- a/frontend/src/views/database/redis/setting/persistence/index.vue +++ b/frontend/src/views/database/redis/setting/persistence/index.vue @@ -44,38 +44,40 @@ - - - - - - - - - - + + + + + + + + + + + +
- - - {{ $t('database.rdbHelper1') }} - - - - {{ $t('database.rdbHelper2') }} - - - {{ $t('commons.button.delete') }} - -
- - {{ $t('commons.button.add') }} - -
+ + + {{ $t('database.rdbHelper1') }} + + + + {{ $t('database.rdbHelper2') }} + + + {{ $t('commons.button.delete') }} + +
+ + {{ $t('commons.button.add') }} + +
{{ $t('database.rdbHelper3') }}