feat: PHP 运行环境增加扩展管理 (#6352)

This commit is contained in:
zhengkunwang 2024-09-03 21:45:41 +08:00 committed by GitHub
parent 2538944701
commit ba9feb0941
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 552 additions and 433 deletions

View File

@ -233,3 +233,46 @@ func (b *BaseApi) SyncStatus(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Runtime
// @Summary Get php runtime extension
// @Description 获取 PHP 运行环境扩展
// @Accept json
// @Param id path string true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/php/:id/extensions [get]
func (b *BaseApi) GetRuntimeExtension(c *gin.Context) {
id, err := helper.GetIntParamByKey(c, "id")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
return
}
res, err := runtimeService.GetPHPExtensions(id)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags Runtime
// @Summary Install php extension
// @Description 安装 PHP 扩展
// @Accept json
// @Param request body request.PHPExtensionsCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/php/extensions/install [post]
func (b *BaseApi) InstallPHPExtension(c *gin.Context) {
var req request.PHPExtensionInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := runtimeService.InstallPHPExtension(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -70,3 +70,9 @@ type NodeModuleOperateReq struct {
type NodeModuleReq struct {
ID uint `json:"ID" validate:"required"`
}
type PHPExtensionInstallReq struct {
ID uint `json:"ID" validate:"required"`
Name string `json:"name" validate:"required"`
TaskID string `json:"taskID" validate:"required"`
}

View File

@ -57,3 +57,16 @@ type NodeModule struct {
License string `json:"license"`
Description string `json:"description"`
}
type SupportExtension struct {
Name string `json:"name"`
Description string `json:"description"`
Installed bool `json:"installed"`
Check string `json:"check"`
Versions []string `json:"versions"`
}
type PHPExtensionRes struct {
Extensions []string `json:"extensions"`
SupportExtensions []SupportExtension `json:"supportExtensions"`
}

View File

@ -22,6 +22,7 @@ type Runtime struct {
Port int `json:"port"`
Message string `json:"message"`
CodeDir string `json:"codeDir"`
ContainerName string `json:"containerName"`
}
func (r *Runtime) GetComposePath() string {

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"os"
"path"
"path/filepath"
@ -45,6 +47,9 @@ type IRuntimeService interface {
SyncForRestart() error
SyncRuntimeStatus() error
DeleteCheck(installID uint) ([]dto.AppResource, error)
GetPHPExtensions(runtimeID uint) (response.PHPExtensionRes, error)
InstallPHPExtension(req request.PHPExtensionInstallReq) error
}
func NewRuntimeService() IRuntimeService {
@ -101,10 +106,12 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
}
}
}
if containerName, ok := create.Params["CONTAINER_NAME"]; ok {
if err := checkContainerName(containerName.(string)); err != nil {
return nil, err
}
containerName, ok := create.Params["CONTAINER_NAME"]
if !ok {
return nil, buserr.New("ErrContainerNameIsNull")
}
if err := checkContainerName(containerName.(string)); err != nil {
return nil, err
}
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
@ -124,12 +131,13 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
}
runtime := &model.Runtime{
Name: create.Name,
AppDetailID: create.AppDetailID,
Type: create.Type,
Image: create.Image,
Resource: create.Resource,
Version: create.Version,
Name: create.Name,
AppDetailID: create.AppDetailID,
Type: create.Type,
Image: create.Image,
Resource: create.Resource,
Version: create.Version,
ContainerName: containerName.(string),
}
switch create.Type {
@ -204,35 +212,34 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
if website.ID > 0 {
return buserr.New(constant.ErrDelWithWebsite)
}
if runtime.Resource == constant.ResourceAppstore {
projectDir := runtime.GetPath()
switch runtime.Type {
case constant.RuntimePHP:
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" {
return errors.New(out)
}
return err
}
if runtime.Resource != constant.ResourceAppstore {
return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
}
projectDir := runtime.GetPath()
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" {
return errors.New(out)
}
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
return err
}
if runtime.Type == constant.RuntimePHP {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
}
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
return err
}
return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
}
@ -635,3 +642,100 @@ func (r *RuntimeService) SyncRuntimeStatus() error {
}
return nil
}
func (r *RuntimeService) GetPHPExtensions(runtimeID uint) (response.PHPExtensionRes, error) {
var res response.PHPExtensionRes
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeID))
if err != nil {
return res, err
}
phpCmd := fmt.Sprintf("docker exec -i %s %s", runtime.ContainerName, "php -m")
out, err := cmd2.ExecWithTimeOut(phpCmd, 20*time.Second)
if err != nil {
if out != "" {
return res, errors.New(out)
}
return res, err
}
extensions := strings.Split(out, "\n")
var cleanExtensions []string
for _, ext := range extensions {
extStr := strings.TrimSpace(ext)
if extStr != "" && extStr != "[Zend Modules]" && extStr != "[PHP Modules]" {
cleanExtensions = append(cleanExtensions, extStr)
}
}
res.Extensions = cleanExtensions
var phpExtensions []response.SupportExtension
if err = json.Unmarshal(nginx_conf.PHPExtensionsJson, &phpExtensions); err != nil {
return res, err
}
for _, ext := range phpExtensions {
for _, cExt := range cleanExtensions {
if ext.Check == cExt {
ext.Installed = true
break
}
}
res.SupportExtensions = append(res.SupportExtensions, ext)
}
return res, nil
}
func (r *RuntimeService) InstallPHPExtension(req request.PHPExtensionInstallReq) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
installTask, err := task.NewTaskWithOps(req.Name, task.TaskInstall, task.TaskScopeRuntime, req.TaskID, runtime.ID)
if err != nil {
return err
}
installTask.AddSubTask("", func(t *task.Task) error {
installCmd := fmt.Sprintf("docker exec -i %s %s %s", runtime.ContainerName, "install-ext", req.Name)
err = cmd2.ExecWithLogFile(installCmd, 15*time.Minute, t.Task.LogFile)
if err != nil {
return err
}
return nil
}, nil)
go func() {
err = installTask.Execute()
if err == nil {
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
global.LOG.Errorf("get runtime env error %v", err)
return
}
extensions, ok := envs["PHP_EXTENSIONS"]
exist := false
var extensionArray []string
if ok {
extensionArray = strings.Split(extensions, ",")
for _, ext := range extensionArray {
if ext == req.Name {
exist = true
break
}
}
}
if !exist {
extensionArray = append(extensionArray, req.Name)
envs["PHP_EXTENSIONS"] = strings.Join(extensionArray, ",")
if err = gotenv.Write(envs, runtime.GetEnvPath()); err != nil {
global.LOG.Errorf("write runtime env error %v", err)
return
}
envStr, err := gotenv.Marshal(envs)
if err != nil {
global.LOG.Errorf("marshal runtime env error %v", err)
return
}
runtime.Env = envStr
_ = runtimeRepo.Save(runtime)
}
}
}()
return nil
}

View File

@ -43,3 +43,6 @@ var PathAuth []byte
//go:embed upstream.conf
var Upstream []byte
//go:embed php_extensions.json
var PHPExtensionsJson []byte

View File

@ -0,0 +1,135 @@
[
{
"name": "ZendGuardLoader",
"description": "用于解密ZendGuard加密脚本!",
"check": "ZendGuardLoader",
"versions": ["53", "54", "55", "56"],
"installed": false
},
{
"name": "ionCube",
"description": "用于解密ionCube Encoder加密脚本!",
"check": "ionCube",
"versions": ["56", "70", "71", "72", "73", "74", "81", "82"],
"installed": false
},
{
"name": "fileinfo",
"description": "",
"check": "fileinfo",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "opcache",
"description": "用于加速PHP脚本!",
"check": "opcache",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "xcache",
"description": "支持脚本缓存和变量缓存!",
"check": "xcache",
"versions": ["56"],
"installed": false
},
{
"name": "memcache",
"description": "强大的内容缓存器",
"check": "memcache",
"versions": ["56", "70", "71", "72", "73", "74", "80"],
"installed": false
},
{
"name": "memcached",
"description": "比memcache支持更多高级功能",
"check": "memcached",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "redis",
"description": "基于内存亦可持久化的Key-Value数据库",
"check": "redis",
"versions": [ "56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "mcrypt",
"description": "mcrypt加密/解密",
"check": "mcrypt",
"versions": ["70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "apcu",
"description": "脚本缓存器",
"check": "apcu",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "imagemagick",
"description": "Imagick高性能图形库",
"check": "imagick",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "xdebug",
"description": "开源的PHP程序调试器",
"check": "xdebug",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "imap",
"description": "邮件服务器必备",
"check": "imap",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "exif",
"description": "用于读取图片EXIF信息",
"check": "exif",
"versions": [ "56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "intl",
"description": "提供国际化支持",
"check": "intl",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
"installed": false
},
{
"name": "xsl",
"description": "xsl解析扩展",
"check": "xsl",
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82"],
"installed": false
},
{
"name": "mbstring",
"description": "mbstring扩展",
"check": "mbstring",
"versions": ["83"],
"installed": false
},
{
"name": "Swoole",
"description": "异步、协程高性能网络通信引擎",
"check": "swoole",
"versions": ["70", "71", "72"],
"installed": false
},
{
"name": "zstd",
"description": "使用 Zstandard 库进行压缩和解压缩的 PHP 扩展",
"check": "zstd",
"versions": ["70", "71", "72"],
"installed": false
}
]

View File

@ -153,6 +153,7 @@ 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"
ErrNodeModulesNotFound: "The node_modules folder does not exist! Please edit the running environment or wait for the running environment to start successfully"
ErrContainerNameIsNull: "Container name cannot be empty"
#setting
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."

View File

@ -154,6 +154,7 @@ ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件"
ErrNodeModulesNotFound: "node_modules 文件夾不存在!請編輯運行環境或者等待運行環境啟動成功"
ErrContainerNameIsNull: "容器名稱不能為空"
#setting
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"

View File

@ -156,6 +156,7 @@ ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件"
ErrNodeModulesNotFound: "node_modules 文件夹不存在!请编辑运行环境或者等待运行环境启动成功"
ErrContainerNameIsNull: "容器名称不存在"
#setting
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"

View File

@ -16,7 +16,7 @@ import (
)
var AddTable = &gormigrate.Migration{
ID: "20240902-add-table",
ID: "20240903-add-table",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.AppDetail{},

View File

@ -30,6 +30,9 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions)
groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions)
groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions)
groupRouter.GET("/php/:id/extensions", baseApi.GetRuntimeExtension)
groupRouter.POST("/php/extensions/install", baseApi.InstallPHPExtension)
}
}

View File

@ -60,6 +60,41 @@ func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) {
return stdout.String(), nil
}
func ExecWithLogFile(cmdStr string, timeout time.Duration, outputFile string) error {
cmd := exec.Command("bash", "-c", cmdStr)
outFile, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer outFile.Close()
cmd.Stdout = outFile
cmd.Stderr = outFile
if err := cmd.Start(); err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
after := time.After(timeout)
select {
case <-after:
_ = cmd.Process.Kill()
return buserr.New(constant.ErrCmdTimeout)
case err := <-done:
if err != nil {
return err
}
}
return nil
}
func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) error {
cmdStr = fmt.Sprintf("docker exec -i %s bash -c '%s'", containerName, cmdStr)
out, err := ExecWithTimeOut(cmdStr, timeout)

View File

@ -122,4 +122,23 @@ export namespace Runtime {
export interface PHPExtensionsDelete {
id: number;
}
export interface PHPExtensionsRes {
extensions: string[];
supportExtensions: SupportExtension[];
}
export interface SupportExtension {
name: string;
description: string;
installed: boolean;
check: string;
versions: string[];
}
export interface PHPExtensionInstall {
name: string;
id: number;
taskID: string;
}
}

View File

@ -67,3 +67,11 @@ export const DeletePHPExtensions = (req: Runtime.PHPExtensionsDelete) => {
export const SyncRuntime = () => {
return http.post(`/runtimes/sync`, {});
};
export const GetPHPExtensions = (id: number) => {
return http.get<Runtime.PHPExtensionsRes>(`/runtimes/php/${id}/extensions`);
};
export const InstallPHPExtension = (req: Runtime.PHPExtensionInstall) => {
return http.post(`/runtimes/php/extensions/install`, req);
};

View File

@ -195,6 +195,7 @@ const initCodemirror = () => {
});
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
hljsDom.style['min-height'] = '100px';
hljsDom.style['max-height'] = '400px';
}
});
};

View File

@ -152,6 +152,7 @@ const message = {
rootInfoErr: "It's already the root directory",
resetSuccess: 'Reset successful',
creatingInfo: 'Creating, no need for this operation',
installSuccess: 'Install successful',
},
login: {
username: 'Username',
@ -2434,6 +2435,10 @@ const message = {
javaDirHelper: 'The directory must contain jar files, subdirectories are also acceptable',
goHelper: 'Please provide a complete start command, for example: go run main.go or ./main',
goDirHelper: 'The directory must contain go files or binary files, subdirectories are also acceptable',
extension: 'Extension',
installExtension: 'Do you confirm to install the extension {0}',
loadedExtension: 'Loaded Extension',
popularExtension: 'Popular Extension',
},
process: {
pid: 'Process ID',

View File

@ -152,6 +152,7 @@ const message = {
rootInfoErr: '已經是根目錄了',
resetSuccess: '重置成功',
creatingInfo: '正在創建無需此操作',
installSuccess: '安裝成功',
},
login: {
username: '用戶名',
@ -2258,6 +2259,10 @@ const message = {
javaDirHelper: '目錄中要包含 jar 子目錄中包含也可',
goHelper: '請填寫完整啟動命令例如go run main.go ./main',
goDirHelper: '目錄中要包含 go 文件或者二進制文件子目錄中包含也可',
extension: '擴充',
installExtension: '是否確認安裝擴充功能 {0}',
loadedExtension: '已載入擴充功能',
popularExtension: '常用擴充',
},
process: {
pid: '進程ID',

View File

@ -152,6 +152,7 @@ const message = {
rootInfoErr: '已经是根目录了',
resetSuccess: '重置成功',
creatingInfo: '正在创建无需此操作',
installSuccess: '安装成功',
},
login: {
username: '用户名',
@ -2260,6 +2261,10 @@ const message = {
javaDirHelper: '目录中要包含 jar 子目录中包含也可',
goHelper: '请填写完整启动命令例如go run main.go ./main',
goDirHelper: '目录中要包含 go 文件或者二进制文件子目录中包含也可',
extension: '扩展',
installExtension: '是否确认安装扩展 {0}',
loadedExtension: '已加载扩展',
popularExtension: '常用扩展',
},
process: {
pid: '进程ID',

View File

@ -1,112 +0,0 @@
<template>
<div v-for="(p, index) in paramObjs" :key="index">
<el-form-item :label="getLabel(p)" :prop="p.prop">
<el-select
v-model="form[p.key]"
v-if="p.type == 'select'"
:multiple="p.multiple"
filterable
allow-create
default-first-option
@change="updateParam"
clearable
>
<el-option
v-for="service in p.values"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
</el-form-item>
</div>
</template>
<script setup lang="ts">
import { App } from '@/api/interface/app';
import { Rules } from '@/global/form-rules';
import { getLanguage } from '@/utils/util';
import { computed, onMounted, reactive, ref } from 'vue';
interface ParamObj extends App.InstallParams {
prop: string;
}
const props = defineProps({
form: {
type: Object,
default: function () {
return {};
},
},
params: {
type: Array<App.InstallParams>,
default: function () {
return [];
},
},
rules: {
type: Object,
default: function () {
return {};
},
},
});
let form = reactive({});
let rules = reactive({});
const params = computed({
get() {
return props.params;
},
set() {},
});
const emit = defineEmits(['update:form', 'update:rules']);
const updateParam = () => {
emit('update:form', form);
};
const paramObjs = ref<ParamObj[]>([]);
const handleParams = () => {
rules = props.rules;
if (params.value != undefined) {
for (const p of params.value) {
form[p.key] = p.value;
paramObjs.value.push({
prop: p.key,
labelEn: p.labelEn,
labelZh: p.labelZh,
values: p.values,
value: p.value,
required: p.required,
edit: p.edit,
key: p.key,
rule: p.rule,
type: p.type,
multiple: p.multiple,
});
if (p.required) {
if (p.type === 'select') {
rules[p.key] = [Rules.requiredSelect];
} else {
rules[p.key] = [Rules.requiredInput];
}
}
}
emit('update:rules', rules);
updateParam();
}
};
const getLabel = (row: ParamObj): string => {
const language = getLanguage();
if (language == 'zh' || language == 'tw') {
return row.labelZh;
} else {
return row.labelEn;
}
};
onMounted(() => {
handleParams();
});
</script>

View File

@ -0,0 +1,98 @@
<template>
<DrawerPro v-model="open" :header="$t('runtime.extension')" size="large" :back="handleClose">
<el-descriptions title="" border>
<el-descriptions-item :label="$t('runtime.loadedExtension')" width="100px">
<el-tag v-for="(ext, index) in extensions" :key="index" type="info" class="mr-1 mt-1">{{ ext }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="mt-5">
<el-text>{{ $t('runtime.popularExtension') }}</el-text>
</div>
<ComplexTable :data="supportExtensions" @search="search()" :heightDiff="350" :loading="loading">
<el-table-column prop="name" :label="$t('commons.table.name')" width="150" />
<el-table-column prop="description" :label="$t('commons.table.description')" />
<el-table-column prop="installed" :label="$t('commons.table.status')" width="100">
<template #default="{ row }">
<el-icon v-if="row.installed" color="green"><Select /></el-icon>
<el-icon v-else><CloseBold /></el-icon>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="10"
width="100px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</DrawerPro>
<TaskLog ref="taskLogRef" />
</template>
<script setup lang="ts">
import { Runtime } from '@/api/interface/runtime';
import { GetPHPExtensions, InstallPHPExtension } from '@/api/modules/runtime';
import i18n from '@/lang';
import { ref } from 'vue';
import { newUUID } from '@/utils/util';
const open = ref(false);
const runtime = ref();
const extensions = ref([]);
const supportExtensions = ref([]);
const loading = ref(false);
const taskLogRef = ref();
const handleClose = () => {
open.value = false;
};
const buttons = [
{
label: i18n.global.t('commons.operate.install'),
click: function (row: Runtime.SupportExtension) {
installExtension(row);
},
show: function (row: Runtime.SupportExtension) {
return !row.installed;
},
},
];
const installExtension = async (row: Runtime.SupportExtension) => {
ElMessageBox.confirm(i18n.global.t('runtime.installExtension', [row.name]), i18n.global.t('runtime.extension'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
const req = {
id: runtime.value.id,
name: row.check,
taskID: newUUID(),
};
loading.value = true;
try {
await InstallPHPExtension(req);
taskLogRef.value.openWithTaskID(req.taskID);
loading.value = false;
} catch (error) {}
});
};
const search = async () => {
try {
const res = await GetPHPExtensions(runtime.value.id);
extensions.value = res.data.extensions;
supportExtensions.value = res.data.supportExtensions;
} catch (error) {}
};
const acceptParams = (req: Runtime.Runtime): void => {
open.value = true;
runtime.value = req;
search();
};
defineExpose({ acceptParams });
</script>

View File

@ -14,7 +14,7 @@
{{ $t('runtime.create') }}
</el-button>
<el-button @click="openExtensions">
<el-button type="primary" plain @click="openExtensions">
{{ $t('php.extensions') }}
</el-button>
@ -94,7 +94,7 @@
/>
<fu-table-operations
:ellipsis="10"
width="120px"
width="200px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
@ -109,6 +109,7 @@
<Log ref="logRef" @close="search" />
<Extensions ref="extensionsRef" @close="search" />
<AppResources ref="checkRef" @close="search" />
<ExtManagement ref="extManagementRef" @close="search" />
</div>
</template>
@ -117,16 +118,17 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, RuntimeDeleteCheck, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat, toLowerCase } from '@/utils/util';
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
import Status from '@/components/status/index.vue';
import i18n from '@/lang';
import RouterMenu from '../index.vue';
import Log from '@/components/log-dialog/index.vue';
import Extensions from './extensions/index.vue';
import AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import ExtManagement from './extension-management/index.vue';
import Extensions from './extension-template/index.vue';
import AppResources from '@/views/website/runtime/php/check/index.vue';
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
import Status from '@/components/status/index.vue';
import RouterMenu from '../index.vue';
import Log from '@/components/log-dialog/index.vue';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
@ -144,10 +146,22 @@ let timer: NodeJS.Timer | null = null;
const opRef = ref();
const logRef = ref();
const extensionsRef = ref();
const extManagementRef = ref();
const checkRef = ref();
const createRef = ref();
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const buttons = [
{
label: i18n.global.t('runtime.extension'),
click: function (row: Runtime.Runtime) {
openExtensionsManagement(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status != 'running';
},
},
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
@ -167,9 +181,6 @@ const buttons = [
},
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const createRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
@ -205,6 +216,10 @@ const openExtensions = () => {
extensionsRef.value.acceptParams();
};
const openExtensionsManagement = (row: Runtime.Runtime) => {
extManagementRef.value.acceptParams(row);
};
const openDelete = async (row: Runtime.Runtime) => {
RuntimeDeleteCheck(row.id).then(async (res) => {
const items = res.data;

View File

@ -1,271 +0,0 @@
<template>
<div v-for="(p, index) in paramObjs" :key="index">
<el-form-item :label="getLabel(p)" :prop="p.prop">
<el-input
v-model.trim="form[p.envKey]"
v-if="p.type == 'text'"
:type="p.type"
@change="updateParam"
:disabled="p.disabled"
></el-input>
<el-input
v-model.number="form[p.envKey]"
@blur="form[p.envKey] = Number(form[p.envKey])"
v-if="p.type == 'number'"
maxlength="15"
@change="updateParam"
:disabled="p.disabled"
></el-input>
<el-input
v-model.trim="form[p.envKey]"
v-if="p.type == 'password'"
:type="p.type"
show-password
clearable
@change="updateParam"
></el-input>
<el-select
class="p-w-200"
v-model="form[p.envKey]"
v-if="p.type == 'service'"
@change="changeService(form[p.envKey], p.services)"
>
<el-option
v-for="service in p.services"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
<span v-if="p.type === 'service' && p.services.length === 0" class="ml-1.5">
<el-link type="primary" :underline="false" @click="toPage(p.key)">
{{ $t('app.toInstall') }}
</el-link>
</span>
<el-select
v-model="form[p.envKey]"
v-if="p.type == 'select'"
:multiple="p.multiple"
:allowCreate="p.allowCreate"
filterable
>
<el-option
v-for="service in p.values"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
<el-row :gutter="10" v-if="p.type == 'apps'">
<el-col :span="12">
<el-form-item :prop="p.prop">
<el-select
v-model="form[p.envKey]"
@change="getServices(p.child.envKey, form[p.envKey], p)"
class="p-w-200"
>
<el-option
v-for="service in p.values"
:label="service.label"
:key="service.value"
:value="service.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :prop="p.childProp">
<el-select
v-model="form[p.child.envKey]"
v-if="p.child.type == 'service'"
@change="changeService(form[p.child.envKey], p.services)"
class="p-w-200"
>
<el-option
v-for="service in p.services"
:key="service.label"
:value="service.value"
:label="service.label"
>
<span>{{ service.label }}</span>
<span class="float-right" v-if="service.from != ''">
<el-tag v-if="service.from === 'local'">{{ $t('database.local') }}</el-tag>
<el-tag v-else type="success">{{ $t('database.remote') }}</el-tag>
</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col>
<span v-if="p.child.type === 'service' && p.services.length === 0">
<el-link type="primary" :underline="false" @click="toPage(form[p.envKey])">
{{ $t('app.toInstall') }}
</el-link>
</span>
</el-col>
</el-row>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { getRandomStr } from '@/utils/util';
import { GetAppService } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import { App } from '@/api/interface/app';
import { getDBName } from '@/utils/util';
interface ParamObj extends App.FromField {
services: App.AppService[];
prop: string;
disabled: false;
childProp: string;
}
const emit = defineEmits(['update:form', 'update:rules']);
const props = defineProps({
form: {
type: Object,
default: function () {
return {};
},
},
params: {
type: Object,
default: function () {
return {};
},
},
rules: {
type: Object,
default: function () {
return {};
},
},
propStart: {
type: String,
default: '',
},
});
const form = reactive({});
let rules = reactive({});
const params = computed({
get() {
return props.params;
},
set() {},
});
const propStart = computed({
get() {
return props.propStart;
},
set() {},
});
const paramObjs = ref<ParamObj[]>([]);
const updateParam = () => {
emit('update:form', form);
};
const handleParams = () => {
rules = props.rules;
if (params.value != undefined && params.value.formFields != undefined) {
for (const p of params.value.formFields) {
const pObj = p;
pObj.prop = propStart.value + p.envKey;
pObj.disabled = p.disabled;
paramObjs.value.push(pObj);
if (p.random) {
if (p.envKey === 'PANEL_DB_NAME') {
form[p.envKey] = p.default + '_' + getDBName(6);
} else {
form[p.envKey] = p.default + '_' + getRandomStr(6);
}
} else {
form[p.envKey] = p.default;
}
if (p.required) {
if (p.type === 'service' || p.type === 'apps') {
rules[p.envKey] = [Rules.requiredSelect];
if (p.child) {
p.childProp = propStart.value + p.child.envKey;
if (p.child.type === 'service') {
rules[p.child.envKey] = [Rules.requiredSelect];
}
}
} else {
rules[p.envKey] = [Rules.requiredInput];
}
if (p.rule && p.rule != '') {
rules[p.envKey].push(Rules[p.rule]);
}
} else {
delete rules[p.envKey];
}
if (p.type === 'apps') {
getServices(p.child.envKey, p.default, p);
p.child.services = [];
form[p.child.envKey] = '';
}
if (p.type === 'service') {
getServices(p.envKey, p.key, p);
p.services = [];
form[p.envKey] = '';
}
emit('update:rules', rules);
updateParam();
}
}
};
const getServices = async (childKey: string, key: string | undefined, pObj: ParamObj | undefined) => {
pObj.services = [];
await GetAppService(key).then((res) => {
pObj.services = res.data || [];
form[childKey] = '';
if (res.data && res.data.length > 0) {
form[childKey] = res.data[0].value;
if (pObj.params) {
pObj.params.forEach((param: App.FromParam) => {
if (param.key === key) {
form[param.envKey] = param.value;
}
});
}
changeService(form[childKey], pObj.services);
}
});
};
const changeService = (value: string, services: App.AppService[]) => {
services.forEach((item) => {
if (item.value === value && item.config) {
Object.entries(item.config).forEach(([k, v]) => {
if (form.hasOwnProperty(k)) {
form[k] = v;
}
});
}
});
updateParam();
};
const getLabel = (row: ParamObj): string => {
const language = localStorage.getItem('lang') || 'zh';
if (language == 'zh' || language == 'tw') {
return row.labelZh;
} else {
return row.labelEn;
}
};
const toPage = (key: string) => {
window.location.href = '/apps/all?install=' + key;
};
onMounted(() => {
handleParams();
});
</script>