1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: Node.js 环境增加启动、停止、重启操作 (#2394)

This commit is contained in:
zhengkunwang 2023-09-26 22:50:16 +08:00 committed by GitHub
parent 1130a70052
commit 4ca25d51d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 440 additions and 388 deletions

View File

@ -93,14 +93,14 @@ func (b *BaseApi) GetApp(c *gin.Context) {
// @Security ApiKeyAuth
// @Router /apps/detail/:appId/:version/:type [get]
func (b *BaseApi) GetAppDetail(c *gin.Context) {
appId, err := helper.GetIntParamByKey(c, "appId")
appID, err := helper.GetIntParamByKey(c, "appId")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
return
}
version := c.Param("version")
appType := c.Param("type")
appDetailDTO, err := appService.GetAppDetail(appId, version, appType)
appDetailDTO, err := appService.GetAppDetail(appID, version, appType)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -143,3 +143,26 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
}
helper.SuccessWithData(c, res)
}
// @Tags Runtime
// @Summary Operate runtime
// @Description 操作运行环境
// @Accept json
// @Param request body request.RuntimeOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/operate [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
func (b *BaseApi) OperateRuntime(c *gin.Context) {
var req request.RuntimeOperate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := runtimeService.OperateRuntime(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -11,6 +11,7 @@ type AppSearch struct {
Tags []string `json:"tags"`
Type string `json:"type"`
Recommend bool `json:"recommend"`
Resource string `json:"resource"`
}
type AppInstallCreate struct {

View File

@ -47,3 +47,8 @@ type RuntimeUpdate struct {
type NodePackageReq struct {
CodeDir string `json:"codeDir"`
}
type RuntimeOperate struct {
Operate string `json:"operate"`
ID uint `json:"ID"`
}

View File

@ -61,6 +61,9 @@ func (a AppService) PageApp(req request.AppSearch) (interface{}, error) {
if req.Recommend {
opts = append(opts, appRepo.GetRecommend())
}
if req.Resource != "" {
opts = append(opts, appRepo.WithResource(req.Resource))
}
if len(req.Tags) != 0 {
tags, err := tagRepo.GetByKeys(req.Tags)
if err != nil {
@ -151,12 +154,12 @@ func (a AppService) GetApp(key string) (*response.AppDTO, error) {
return &appDTO, nil
}
func (a AppService) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) {
func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) {
var (
appDetailDTO response.AppDetailDTO
opts []repo.DBOption
)
opts = append(opts, appDetailRepo.WithAppId(appId), appDetailRepo.WithVersion(version))
opts = append(opts, appDetailRepo.WithAppId(appID), appDetailRepo.WithVersion(version))
detail, err := appDetailRepo.GetFirst(opts...)
if err != nil {
return appDetailDTO, err
@ -165,7 +168,7 @@ func (a AppService) GetAppDetail(appId uint, version, appType string) (response.
appDetailDTO.Enable = true
if appType == "runtime" {
app, err := appRepo.GetFirst(commonRepo.WithByID(appId))
app, err := appRepo.GetFirst(commonRepo.WithByID(appID))
if err != nil {
return appDetailDTO, err
}

View File

@ -31,6 +31,7 @@ type IRuntimeService interface {
Update(req request.RuntimeUpdate) error
Get(id uint) (res *response.RuntimeDTO, err error)
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
OperateRuntime(req request.RuntimeOperate) error
}
func NewRuntimeService() IRuntimeService {
@ -357,3 +358,42 @@ func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]
}
return packageScripts, nil
}
func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
defer func() {
if err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
}
}()
switch req.Operate {
case constant.RuntimeUp:
if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return err
}
if err = SyncRuntimeContainerStatus(runtime); err != nil {
return err
}
case constant.RuntimeDown:
if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return err
}
runtime.Status = constant.RuntimeStopped
case constant.RuntimeRestart:
if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return err
}
if err = runComposeCmdWithLog(constant.RuntimeUp, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return err
}
if err = SyncRuntimeContainerStatus(runtime); err != nil {
return err
}
}
return runtimeRepo.Save(runtime)
}

View File

@ -42,7 +42,7 @@ func handleNode(create request.RuntimeCreate, runtime *model.Runtime, fileOp fil
}
runtime.DockerCompose = string(composeContent)
runtime.Env = string(envContent)
runtime.Status = constant.RuntimeStarting
runtime.Status = constant.RuntimeCreating
runtime.CodeDir = create.CodeDir
go startRuntime(runtime)
@ -88,29 +88,39 @@ func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp file
}
func startRuntime(runtime *model.Runtime) {
cmd := exec.Command("docker-compose", "-f", runtime.GetComposePath(), "up", "-d")
logPath := runtime.GetLogPath()
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
global.LOG.Errorf("Failed to open log file: %v", err)
if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return
}
multiWriterStdout := io.MultiWriter(os.Stdout, logFile)
cmd.Stdout = multiWriterStdout
var stderrBuf bytes.Buffer
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr)
cmd.Stderr = multiWriterStderr
err = cmd.Run()
if err != nil {
if err := SyncRuntimeContainerStatus(runtime); err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()
} else {
runtime.Status = constant.RuntimeRunning
runtime.Message = ""
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return
}
}
_ = runtimeRepo.Save(runtime)
func reCreateRuntime(runtime *model.Runtime) {
var err error
defer func() {
if err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
}
}()
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
}
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
}
if err := SyncRuntimeContainerStatus(runtime); err != nil {
return
}
}
func runComposeCmdWithLog(operate string, composePath string, logPath string) error {
@ -136,23 +146,57 @@ func runComposeCmdWithLog(operate string, composePath string, logPath string) er
return nil
}
func reCreateRuntime(runtime *model.Runtime) {
var err error
defer func() {
func SyncRuntimeContainerStatus(runtime *model.Runtime) error {
env, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return err
}
var containerNames []string
if containerName, ok := env["CONTAINER_NAME"]; !ok {
return buserr.New("ErrContainerNameNotFound")
} else {
containerNames = append(containerNames, containerName)
}
cli, err := docker.NewClient()
if err != nil {
return err
}
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
if len(containers) == 0 {
return buserr.WithNameAndErr("ErrContainerNotFound", containerNames[0], nil)
}
container := containers[0]
interval := 10 * time.Second
retries := 60
for i := 0; i < retries; i++ {
resp, err := cli.InspectContainer(container.ID)
if err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
time.Sleep(interval)
continue
}
}()
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
if resp.State.Health != nil {
status := strings.ToLower(resp.State.Health.Status)
switch status {
case "starting":
runtime.Status = constant.RuntimeStarting
_ = runtimeRepo.Save(runtime)
case "healthy":
runtime.Status = constant.RuntimeRunning
_ = runtimeRepo.Save(runtime)
return nil
case "unhealthy":
runtime.Status = constant.RuntimeUnhealthy
_ = runtimeRepo.Save(runtime)
return nil
}
}
time.Sleep(interval)
}
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
}
runtime.Status = constant.RuntimeRunning
_ = runtimeRepo.Save(runtime)
return nil
}
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {

View File

@ -10,10 +10,17 @@ const (
RuntimeStarting = "starting"
RuntimeRunning = "running"
RuntimeReCreating = "recreating"
RuntimeStopped = "stopped"
RuntimeUnhealthy = "unhealthy"
RuntimeCreating = "creating"
RuntimePHP = "php"
RuntimeNode = "node"
RuntimeProxyUnix = "unix"
RuntimeProxyTcp = "tcp"
RuntimeUp = "up"
RuntimeDown = "down"
RuntimeRestart = "restart"
)

View File

@ -102,6 +102,7 @@ ErrDelWithWebsite: "The operating environment has been associated with a website
ErrRuntimeStart: "Failed to start"
ErrPackageJsonNotFound: "package.json file does not exist"
ErrScriptsNotFound: "No scripts configuration item was found in package.json"
ErrContainerNameNotFound: "Unable to get container name, please check .env file"
#setting
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."

View File

@ -102,6 +102,7 @@ ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除"
ErrRuntimeStart: "啟動失敗"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件"
#setting
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"

View File

@ -102,6 +102,7 @@ ErrDelWithWebsite: "运行环境已经关联网站,无法删除"
ErrRuntimeStart: "启动失败"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件"
#setting
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"

View File

@ -21,5 +21,7 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
groupRouter.POST("/update", baseApi.UpdateRuntime)
groupRouter.GET("/:id", baseApi.GetRuntime)
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
groupRouter.POST("/operate", baseApi.OperateRuntime)
}
}

View File

@ -80,6 +80,10 @@ func (c Client) DeleteImage(imageID string) error {
return nil
}
func (c Client) InspectContainer(containerID string) (types.ContainerJSON, error) {
return c.cli.ContainerInspect(context.Background(), containerID)
}
func (c Client) PullImage(imageName string, force bool) error {
if !force {
exist, err := c.CheckImageExist(imageName)

View File

@ -7863,6 +7863,48 @@ const docTemplate = `{
}
}
},
"/runtimes/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作运行环境",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Operate runtime",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RuntimeOperate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"id"
],
"formatEN": "Operate runtime [name]",
"formatZH": "操作运行环境 [name]",
"paramKeys": []
}
}
},
"/runtimes/search": {
"post": {
"security": [
@ -15732,6 +15774,9 @@ const docTemplate = `{
"recommend": {
"type": "boolean"
},
"resource": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
@ -16491,6 +16536,17 @@ const docTemplate = `{
}
}
},
"request.RuntimeOperate": {
"type": "object",
"properties": {
"ID": {
"type": "integer"
},
"operate": {
"type": "string"
}
}
},
"request.RuntimeSearch": {
"type": "object",
"required": [

View File

@ -7856,6 +7856,48 @@
}
}
},
"/runtimes/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作运行环境",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Operate runtime",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RuntimeOperate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"id"
],
"formatEN": "Operate runtime [name]",
"formatZH": "操作运行环境 [name]",
"paramKeys": []
}
}
},
"/runtimes/search": {
"post": {
"security": [
@ -15725,6 +15767,9 @@
"recommend": {
"type": "boolean"
},
"resource": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
@ -16484,6 +16529,17 @@
}
}
},
"request.RuntimeOperate": {
"type": "object",
"properties": {
"ID": {
"type": "integer"
},
"operate": {
"type": "string"
}
}
},
"request.RuntimeSearch": {
"type": "object",
"required": [

View File

@ -2639,6 +2639,8 @@ definitions:
type: integer
recommend:
type: boolean
resource:
type: string
tags:
items:
type: string
@ -3151,6 +3153,13 @@ definitions:
id:
type: integer
type: object
request.RuntimeOperate:
properties:
ID:
type: integer
operate:
type: string
type: object
request.RuntimeSearch:
properties:
name:
@ -9086,6 +9095,33 @@ paths:
summary: Get Node package scripts
tags:
- Runtime
/runtimes/operate:
post:
consumes:
- application/json
description: 操作运行环境
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.RuntimeOperate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Operate runtime
tags:
- Runtime
x-panel-log:
BeforeFuntions: []
bodyKeys:
- id
formatEN: Operate runtime [name]
formatZH: 操作运行环境 [name]
paramKeys: []
/runtimes/search:
post:
consumes:

View File

@ -68,4 +68,9 @@ export namespace Runtime {
id: number;
forceDelete: boolean;
}
export interface RuntimeOperate {
ID: number;
operate: string;
}
}

View File

@ -25,3 +25,7 @@ export const UpdateRuntime = (req: Runtime.RuntimeUpdate) => {
export const GetNodeScripts = (req: Runtime.NodeReq) => {
return http.post<Runtime.NodeScripts[]>(`/runtimes/node/package`, req);
};
export const OperateRuntime = (req: Runtime.RuntimeOperate) => {
return http.post<any>(`/runtimes/operate`, req);
};

View File

@ -34,7 +34,16 @@ const getType = (status: string) => {
}
};
const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding', 'recreating', 'creating'];
const loadingStatus = [
'installing',
'building',
'restarting',
'upgrading',
'rebuilding',
'recreating',
'creating',
'starting',
];
const loadingIcon = (status: string): boolean => {
return loadingStatus.indexOf(status) > -1;

View File

@ -230,6 +230,7 @@ const message = {
unUsed: 'Unused',
starting: 'Starting',
recreating: 'Recreating',
creating: 'Creating',
},
units: {
second: 'Second',
@ -242,6 +243,11 @@ const message = {
time: 'Time',
core: 'Core',
},
operate: {
down: 'Stop',
up: 'Start',
restart: 'Restart',
},
},
menu: {
home: 'Overview',
@ -1703,6 +1709,10 @@ const message = {
runScriptHelper: 'The startup command list is parsed from the package.json file in the source directory',
open: 'Open',
close: 'Close',
operatorHelper:
'The {0} operation will be performed on the selected operating environment. Do you want to continue? ',
statusHelper:
'Status description: Starting - the container has been started, but the application is starting; abnormal - the container has been started, but the application status is abnormal',
},
process: {
pid: 'Process ID',

View File

@ -228,6 +228,7 @@ const message = {
unUsed: '未使用',
starting: '啟動中',
recreating: '重建中',
creating: '創建中',
},
units: {
second: '秒',
@ -240,6 +241,11 @@ const message = {
time: '次',
core: '核',
},
operate: {
down: '停止',
up: '启动',
restart: '重启',
},
},
menu: {
home: '概覽',
@ -1612,6 +1618,8 @@ const message = {
runScriptHelper: '啟動命令是指容器啟動後運行的命令',
open: '開啟',
close: '關閉',
operatorHelper: '將對選取的執行環境進行 {0} 操作是否繼續 ',
statusHelper: '狀態說明啟動中-容器已啟動但應用正在啟動異常-容器已啟動但應用狀態異常',
},
process: {
pid: '進程ID',

View File

@ -228,6 +228,7 @@ const message = {
unUsed: '未使用',
starting: '启动中',
recreating: '重建中',
creating: '创建中',
},
units: {
second: '秒',
@ -240,6 +241,11 @@ const message = {
time: '次',
core: '核',
},
operate: {
down: '停止',
up: '启动',
restart: '重启',
},
},
menu: {
home: '概览',
@ -1612,6 +1618,8 @@ const message = {
runScriptHelper: '启动命令列表是从源码目录下的 package.json 文件中解析而来',
open: '放开',
close: '关闭',
operatorHelper: '将对选中的运行环境进行 {0} 操作是否继续',
statusHelper: '状态说明启动中-容器已启动但应用正在启动异常-容器已启动但应用状态异常',
},
process: {
pid: '进程ID',

View File

@ -130,7 +130,7 @@
</div>
</template>
</LayoutContent>
<Detail :id="appId" ref="detailRef"></Detail>
<Detail ref="detailRef"></Detail>
<Install ref="installRef" />
</template>
@ -174,7 +174,6 @@ const colorArr = ['#005eeb', '#008B45', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B000
const loading = ref(false);
const activeTag = ref('all');
const showDetail = ref(false);
const appId = ref(0);
const canUpdate = ref(false);
const syncing = ref(false);
const detailRef = ref();

View File

@ -131,14 +131,19 @@ const toLink = (link: string) => {
};
const openInstall = () => {
if (app.value.type === 'php') {
router.push({ path: '/websites/runtime/php' });
} else {
const params = {
app: app.value,
};
installRef.value.acceptParams(params);
open.value = false;
switch (app.value.type) {
case 'php':
router.push({ path: '/websites/runtimes/php' });
break;
case 'node':
router.push({ path: '/websites/runtimes/node' });
break;
default:
const params = {
app: app.value,
};
installRef.value.acceptParams(params);
open.value = false;
}
};

View File

@ -1,337 +0,0 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader :header="$t('runtime.' + mode)" :resource="runtime.name" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form
ref="runtimeForm"
label-position="top"
:model="runtime"
label-width="125px"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input :disabled="mode === 'edit'" v-model="runtime.name"></el-input>
</el-form-item>
<el-form-item :label="$t('runtime.app')" prop="appId">
<el-row :gutter="20">
<el-col :span="12">
<el-select
v-model="runtime.appId"
:disabled="mode === 'edit'"
@change="changeApp(runtime.appId)"
>
<el-option
v-for="(app, index) in apps"
:key="index"
:label="app.name"
:value="app.id"
></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-select
v-model="runtime.version"
:disabled="mode === 'edit'"
@change="changeVersion()"
>
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="$t('runtime.codeDir')" prop="codeDir">
<el-input v-model.trim="runtime.codeDir">
<template #prepend>
<FileList :path="runtime.codeDir" @choose="getPath" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="params.EXEC_SCRIPT">
<el-select v-model="runtime.params['EXEC_SCRIPT']">
<el-option
v-for="(script, index) in scripts"
:key="index"
:label="script.name + ' 【 ' + script.script + ' 】'"
:value="script.name"
>
<el-row :gutter="10">
<el-col :span="4">{{ script.name }}</el-col>
<el-col :span="10">{{ ' 【 ' + script.script + ' 】' }}</el-col>
</el-row>
</el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="$t('runtime.appPort')" prop="params.NODE_APP_PORT">
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item :label="$t('runtime.externalPort')" prop="params.PANEL_APP_PORT_HTTP">
<el-input v-model.number="runtime.params['PANEL_APP_PORT_HTTP']" />
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
<el-select v-model="runtime.params['HOST_IP']">
<el-option label="放开" value="0.0.0.0"></el-option>
<el-option label="不放开" value="127.0.0.1"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('runtime.packageManager')" prop="params.PACKAGE_MANAGER">
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
<el-option label="npm" value="npm"></el-option>
<el-option label="yarn" value="yarn"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="params.CONTAINER_NAME">
<el-input v-model.trim="runtime.params['CONTAINER_NAME']"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(runtimeForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { Runtime } from '@/api/interface/runtime';
import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
import { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { reactive, ref, watch } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
interface OperateRrops {
id?: number;
mode: string;
type: string;
}
const open = ref(false);
const apps = ref<App.App[]>([]);
const runtimeForm = ref<FormInstance>();
const loading = ref(false);
const mode = ref('create');
const editParams = ref<App.InstallParams[]>();
const appVersions = ref<string[]>([]);
const appReq = reactive({
type: 'node',
page: 1,
pageSize: 20,
});
const initData = (type: string) => ({
name: '',
appDetailId: undefined,
image: '',
params: {
PACKAGE_MANAGER: 'npm',
HOST_IP: '0.0.0.0',
},
type: type,
resource: 'appstore',
rebuild: false,
codeDir: '/',
});
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
const rules = ref<any>({
name: [Rules.appName],
appId: [Rules.requiredSelect],
codeDir: [Rules.requiredInput],
params: {
NODE_APP_PORT: [Rules.requiredInput, Rules.port],
PANEL_APP_PORT_HTTP: [Rules.requiredInput, Rules.port],
PACKAGE_MANAGER: [Rules.requiredSelect],
HOST_IP: [Rules.requiredSelect],
EXEC_SCRIPT: [Rules.requiredSelect],
CONTAINER_NAME: [Rules.requiredInput],
},
});
const scripts = ref<Runtime.NodeScripts[]>([]);
const em = defineEmits(['close']);
watch(
() => runtime.params['NODE_APP_PORT'],
(newVal) => {
if (newVal) {
runtime.params['PANEL_APP_PORT_HTTP'] = newVal;
}
},
{ deep: true },
);
watch(
() => runtime.name,
(newVal) => {
if (newVal) {
runtime.params['CONTAINER_NAME'] = newVal;
}
},
{ deep: true },
);
const handleClose = () => {
open.value = false;
em('close', false);
};
const getPath = (codeDir: string) => {
runtime.codeDir = codeDir;
getScripts();
};
const getScripts = () => {
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
scripts.value = res.data;
if (scripts.value.length > 0) {
runtime.params['EXEC_SCRIPT'] = scripts.value[0].script;
}
});
};
const searchApp = (appId: number) => {
SearchApp(appReq).then((res) => {
apps.value = res.data.items || [];
if (res.data && res.data.items && res.data.items.length > 0) {
if (appId == null) {
runtime.appId = res.data.items[0].id;
getApp(res.data.items[0].key, mode.value);
} else {
res.data.items.forEach((item) => {
if (item.id === appId) {
getApp(item.key, mode.value);
}
});
}
}
});
};
const changeApp = (appId: number) => {
for (const app of apps.value) {
if (app.id === appId) {
getApp(app.key, mode.value);
break;
}
}
};
const changeVersion = () => {
loading.value = true;
GetAppDetail(runtime.appId, runtime.version, 'runtime')
.then((res) => {
runtime.appDetailId = res.data.id;
})
.finally(() => {
loading.value = false;
});
};
const getApp = (appkey: string, mode: string) => {
GetApp(appkey).then((res) => {
appVersions.value = res.data.versions || [];
if (res.data.versions.length > 0) {
runtime.version = res.data.versions[0];
if (mode === 'create') {
changeVersion();
}
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (mode.value == 'create') {
loading.value = true;
CreateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
} else {
loading.value = true;
UpdateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
}
});
};
const getRuntime = async (id: number) => {
try {
const res = await GetRuntime(id);
const data = res.data;
Object.assign(runtime, {
id: data.id,
name: data.name,
appDetailId: data.appDetailId,
image: data.image,
params: {},
type: data.type,
resource: data.resource,
appId: data.appId,
version: data.version,
rebuild: true,
source: data.source,
});
editParams.value = data.appParams;
if (mode.value == 'create') {
searchApp(data.appId);
}
} catch (error) {}
};
const acceptParams = async (props: OperateRrops) => {
mode.value = props.mode;
if (props.mode === 'create') {
Object.assign(runtime, initData(props.type));
searchApp(null);
} else {
searchApp(null);
getRuntime(props.id);
}
open.value = true;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -2,6 +2,13 @@
<div>
<RouterMenu />
<LayoutContent :title="'Node.js'" v-loading="loading">
<template #prompt>
<el-alert type="info" :closable="false">
<template #default>
<span><span v-html="$t('runtime.statusHelper')"></span></span>
</template>
</el-alert>
</template>
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
@ -56,7 +63,7 @@
/>
<fu-table-operations
:ellipsis="10"
width="120px"
width="250px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
@ -73,7 +80,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { SearchRuntimes } from '@/api/modules/runtime';
import { OperateRuntime, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat } from '@/utils/util';
import OperateNode from '@/views/website/runtime/node/operate/index.vue';
import Status from '@/components/status/index.vue';
@ -97,13 +104,40 @@ const req = reactive<Runtime.RuntimeReq>({
let timer: NodeJS.Timer | null = null;
const buttons = [
{
label: i18n.global.t('container.stop'),
click: function (row: Runtime.Runtime) {
operateRuntime('down', row.id);
},
disabled: function (row: Runtime.Runtime) {
return disabledRuntime(row) || row.status === 'stopped';
},
},
{
label: i18n.global.t('container.start'),
click: function (row: Runtime.Runtime) {
operateRuntime('up', row.id);
},
disabled: function (row: Runtime.Runtime) {
return disabledRuntime(row) || row.status === 'running';
},
},
{
label: i18n.global.t('container.restart'),
click: function (row: Runtime.Runtime) {
operateRuntime('restart', row.id);
},
disabled: function (row: Runtime.Runtime) {
return disabledRuntime(row);
},
},
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'starting' || row.status === 'recreating';
return disabledRuntime(row);
},
},
{
@ -118,6 +152,10 @@ const items = ref<Runtime.RuntimeDTO[]>([]);
const operateRef = ref();
const deleteRef = ref();
const disabledRuntime = (row: Runtime.Runtime) => {
return row.status === 'starting' || row.status === 'recreating';
};
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
@ -144,6 +182,28 @@ const openDelete = async (row: Runtime.Runtime) => {
deleteRef.value.acceptParams(row.id, row.name);
};
const operateRuntime = async (operate: string, ID: number) => {
try {
const action = await ElMessageBox.confirm(
i18n.global.t('runtime.operatorHelper', [i18n.global.t('commons.operate.' + operate)]),
i18n.global.t('commons.operate.' + operate),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
);
if (action === 'confirm') {
loading.value = true;
await OperateRuntime({ operate: operate, ID: ID });
search();
}
} catch (error) {
} finally {
loading.value = false;
}
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};

View File

@ -156,6 +156,7 @@ const appReq = reactive({
type: 'node',
page: 1,
pageSize: 20,
resource: 'remote',
});
const initData = (type: string) => ({
name: '',