1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 00:09:16 +08:00

feat: 增加应用参数修改功能

This commit is contained in:
zhengkunwang223 2023-03-08 11:04:22 +08:00 committed by zhengkunwang223
parent b54cbe1d11
commit 1e9833d7b7
19 changed files with 285 additions and 55 deletions

View File

@ -283,3 +283,25 @@ func (b *BaseApi) GetParams(c *gin.Context) {
}
helper.SuccessWithData(c, content)
}
// @Tags App
// @Summary Change app params
// @Description 修改应用参数
// @Accept json
// @Param request body request.AppInstalledUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /apps/installed/params/update [post]
// @x-panel-log {"bodyKeys":["installId"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"应用参数修改 [installId]","formatEN":"Application param update [installId]"}
func (b *BaseApi) UpdateInstalled(c *gin.Context) {
var req request.AppInstalledUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := appInstallService.Update(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -76,6 +76,8 @@ type AppFormFields struct {
Default interface{} `json:"default"`
EnvKey string `json:"envKey"`
Disabled bool `json:"disabled"`
Edit bool `json:"edit"`
Rule string `json:"rule"`
}
type AppResource struct {

View File

@ -48,6 +48,11 @@ type AppInstalledOperate struct {
DeleteDB bool `json:"deleteDB"`
}
type AppInstalledUpdate struct {
InstallId uint `json:"installId" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"`
}
type PortUpdate struct {
Key string `json:"key"`
Name string `json:"name"`

View File

@ -61,6 +61,11 @@ type AppService struct {
}
type AppParam struct {
Label string `json:"label"`
Value interface{} `json:"value"`
Value interface{} `json:"value"`
Edit bool `json:"edit"`
Key string `json:"key"`
Rule string `json:"rule"`
LabelZh string `json:"labelZh"`
LabelEn string `json:"labelEn"`
Type string `json:"type"`
}

View File

@ -3,7 +3,9 @@ package service
import (
"encoding/json"
"fmt"
"github.com/joho/godotenv"
"io/ioutil"
"math"
"os"
"path"
"reflect"
@ -156,15 +158,7 @@ func (a AppInstallService) Operate(req request.AppInstalledOperate) error {
dockerComposePath := install.GetComposePath()
switch req.Operate {
case constant.Rebuild:
out, err := compose.Down(dockerComposePath)
if err != nil {
return handleErr(install, err, out)
}
out, err = compose.Up(dockerComposePath)
if err != nil {
return handleErr(install, err, out)
}
return syncById(install.ID)
return rebuildApp(install)
case constant.Start:
out, err := compose.Start(dockerComposePath)
if err != nil {
@ -193,13 +187,59 @@ func (a AppInstallService) Operate(req request.AppInstalledOperate) error {
return nil
case constant.Sync:
return syncById(install.ID)
case constant.Update:
case constant.Upgrade:
return updateInstall(install.ID, req.DetailId)
default:
return errors.New("operate not support")
}
}
func (a AppInstallService) Update(req request.AppInstalledUpdate) error {
installed, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId))
if err != nil {
return err
}
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
if ok {
portN := int(math.Ceil(port.(float64)))
if portN != installed.HttpPort {
httpPort, err := checkPort("PANEL_APP_PORT_HTTP", req.Params)
if err != nil {
return err
}
installed.HttpPort = httpPort
}
}
ports, ok := req.Params["PANEL_APP_PORT_HTTPS"]
if ok {
portN := int(math.Ceil(ports.(float64)))
if portN != installed.HttpsPort {
httpsPort, err := checkPort("PANEL_APP_PORT_HTTPS", req.Params)
if err != nil {
return err
}
installed.HttpsPort = httpsPort
}
}
envPath := path.Join(installed.GetPath(), ".env")
oldEnvMaps, err := godotenv.Read(envPath)
if err != nil {
return err
}
handleMap(req.Params, oldEnvMaps)
paramByte, err := json.Marshal(oldEnvMaps)
if err != nil {
return err
}
installed.Env = string(paramByte)
if err := godotenv.Write(oldEnvMaps, envPath); err != nil {
return err
}
_ = appInstallRepo.Save(&installed)
return rebuildApp(installed)
}
func (a AppInstallService) SyncAll() error {
allList, err := appInstallRepo.ListBy()
if err != nil {
@ -391,17 +431,24 @@ func (a AppInstallService) GetParams(id uint) ([]response.AppParam, error) {
}
for _, form := range appForm.FormFields {
if v, ok := envs[form.EnvKey]; ok {
appParam := response.AppParam{
Edit: false,
Key: form.EnvKey,
Rule: form.Rule,
Type: form.Type,
}
if form.Edit {
appParam.Edit = true
}
appParam.LabelZh = form.LabelZh
appParam.LabelEn = form.LabelEn
if form.Type == "service" {
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithServiceName(v.(string)))
res = append(res, response.AppParam{
Label: form.LabelZh,
Value: appInstall.Name,
})
appParam.Value = appInstall.Name
res = append(res, appParam)
} else {
res = append(res, response.AppParam{
Label: form.LabelZh,
Value: v,
})
appParam.Value = v
res = append(res, appParam)
}
}
}

View File

@ -345,6 +345,19 @@ func upApp(composeFilePath string, appInstall model.AppInstall) {
}
}
func rebuildApp(appInstall model.AppInstall) error {
dockerComposePath := appInstall.GetComposePath()
out, err := compose.Down(dockerComposePath)
if err != nil {
return handleErr(appInstall, err, out)
}
out, err = compose.Up(dockerComposePath)
if err != nil {
return handleErr(appInstall, err, out)
}
return syncById(appInstall.ID)
}
func getAppDetails(details []model.AppDetail, versions []string) map[string]model.AppDetail {
appDetails := make(map[string]model.AppDetail, len(details))
for _, old := range details {

View File

@ -32,4 +32,5 @@ var (
Restore AppOperate = "restore"
Update AppOperate = "update"
Rebuild AppOperate = "rebuild"
Upgrade AppOperate = "upgrade"
)

View File

@ -33,5 +33,6 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
appRouter.GET("/services/:key", baseApi.GetServices)
appRouter.GET("/installed/conf/:key", baseApi.GetDefaultConfig)
appRouter.GET("/installed/params/:appInstallId", baseApi.GetParams)
appRouter.POST("/installed/params/update", baseApi.UpdateInstalled)
}
}

View File

@ -1,6 +1,6 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}
export {}

View File

@ -151,7 +151,12 @@ export namespace App {
}
export interface InstallParams {
label: string;
value: string;
labelZh: string;
labelEn: string;
value: any;
edit: boolean;
key: string;
rule: string;
type: string;
}
}

View File

@ -77,3 +77,7 @@ export const GetAppDefaultConfig = (key: string) => {
export const GetAppInstallParams = (id: number) => {
return http.get<App.InstallParams[]>(`apps/installed/params/${id}`);
};
export const UpdateAppInstallParams = (req: any) => {
return http.post<any>(`apps/installed/params/update`, req);
};

View File

@ -150,7 +150,7 @@ const checkParamCommon = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.paramName')));
} else {
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9._-]{2,30}$/;
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9._-]{1,29}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.paramName')));
} else {
@ -163,7 +163,7 @@ const checkParamComplexity = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.paramComplexity', ['.%@$!&~_'])));
} else {
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9.%@$!&~_]{6,30}$/;
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9.%@$!&~_]{5,29}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.paramComplexity', ['.%@$!&~_'])));
} else {

View File

@ -881,7 +881,7 @@ export default {
deleteWarn:
'The delete operation will delete all data and backups together. This operation cannot be rolled back. Do you want to continue? ',
syncSuccess: 'Sync successfully',
canUpdate: 'Upgrade',
canUpgrade: 'Upgrade',
backup: 'Backup',
backupName: 'File Name',
backupPath: 'File Path',
@ -889,7 +889,8 @@ export default {
restore: 'Restore',
restoreWarn:
'The restore operation will restart the application and replace the data. This operation cannot be rolled back. Do you want to continue?',
update: 'upgrade',
update: 'update',
upgrade: 'upgrade',
versioneSelect: 'Please select a version',
operatorHelper: 'Operation {0} will be performed on the selected application, continue? ',
checkInstalledWarn: '{0} is not detected, please enter the app store and click to install!',
@ -919,6 +920,7 @@ export default {
syncAppList: 'Sync',
updatePrompt: 'The current application is the latest version',
installPrompt: 'No apps installed yet',
updateHelper: 'Updating parameters may cause the application to fail to start, please operate with caution',
},
website: {
website: 'Website',

View File

@ -885,14 +885,15 @@ export default {
delete: '删除',
deleteWarn: '删除操作会把所有数据和备份一并删除此操作不可回滚是否继续',
syncSuccess: '同步成功',
canUpdate: '可升级',
canUpgrade: '可升级',
backup: '备份',
backupName: '文件名称',
backupPath: '文件路径',
backupdate: '备份时间',
restore: '恢复',
restoreWarn: '恢复操作会重启应用,并替换数据,此操作不可回滚,是否继续?',
update: '升级',
update: '更新',
upgrade: '升级',
versioneSelect: '请选择版本',
operatorHelper: '将对选中应用进行 {0} 操作是否继续',
checkInstalledWarn: '未检测到 {0} ,请进入应用商店点击安装!',
@ -929,6 +930,8 @@ export default {
document: '文档说明',
updatePrompt: '当前应用均为最新版本',
installPrompt: '尚未安装任何应用',
updateHelper: '更新参数可能导致应用无法启动请提前备份并谨慎操作',
updateWarn: '更新参数需要重建应用是否继续',
},
website: {
website: '网站',

View File

@ -51,8 +51,8 @@ const appStoreRouter = {
},
},
{
path: 'update',
name: 'AppUpdate',
path: 'upgrade',
name: 'AppUpgrade',
component: () => import('@/views/app-store/installed/index.vue'),
props: true,
hidden: true,

View File

@ -27,8 +27,8 @@ const buttons = [
path: '/apps/installed',
},
{
label: i18n.global.t('app.canUpdate'),
path: '/apps/update',
label: i18n.global.t('app.canUpgrade'),
path: '/apps/upgrade',
count: 0,
},
];
@ -49,7 +49,7 @@ const search = () => {
onMounted(() => {
search();
bus.on('update', () => {
bus.on('upgrade', () => {
showButton.value = false;
search();
});

View File

@ -1,20 +1,57 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="40%">
<template #header>
<Header :header="$t('app.param')" :back="handleClose"></Header>
<Header :header="$t('app.param')" :back="handleClose">
<template #buttons v-if="canEdit">
<el-button type="primary" plain @click="editParam" :disabled="loading">
{{ edit ? $t('app.detail') : $t('commons.button.edit') }}
</el-button>
</template>
</Header>
</template>
<el-descriptions border :column="1">
<el-descriptions-item v-for="(param, key) in params" :label="param.label" :key="key">
<el-descriptions border :column="1" v-if="!edit">
<el-descriptions-item v-for="(param, key) in params" :label="getLabel(param)" :key="key">
{{ param.value }}
</el-descriptions-item>
</el-descriptions>
<el-row v-else v-loading="loading">
<el-col :span="22" :offset="1">
<el-alert :title="$t('app.updateHelper')" type="warning" :closable="false" />
<el-form ref="paramForm" :model="paramModel" label-position="top" :rules="rules">
<div v-for="(p, index) in params" :key="index">
<el-form-item :prop="p.key" :label="getLabel(p)">
<el-input
v-if="p.type == 'number'"
type="number"
v-model.number="paramModel[p.key]"
:disabled="!p.edit"
></el-input>
<el-input v-else v-model.trim="paramModel[p.key]" :disabled="!p.edit"></el-input>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
<template #footer v-if="edit">
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" :disabled="loading" @click="submit(paramForm)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { GetAppInstallParams } from '@/api/modules/app';
import { ref } from 'vue';
import { GetAppInstallParams, UpdateAppInstallParams } from '@/api/modules/app';
import { reactive, ref } from 'vue';
import Header from '@/components/drawer-header/index.vue';
import { useI18n } from 'vue-i18n';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
interface ParamProps {
id: Number;
@ -23,31 +60,114 @@ const paramData = ref<ParamProps>({
id: 0,
});
interface EditForm extends App.InstallParams {
default: any;
}
let open = ref(false);
let loading = ref(false);
const params = ref<App.InstallParams[]>();
const params = ref<EditForm[]>();
let edit = ref(false);
const paramForm = ref<FormInstance>();
let paramModel = ref<any>({});
let rules = reactive({});
let submitModel = ref<any>({});
let canEdit = ref(false);
const acceptParams = (props: ParamProps) => {
const acceptParams = async (props: ParamProps) => {
submitModel.value.installId = props.id;
params.value = [];
paramData.value.id = props.id;
get();
edit.value = false;
await get();
open.value = true;
};
const handleClose = () => {
open.value = false;
};
const editParam = () => {
params.value.forEach((param: EditForm) => {
paramModel.value[param.key] = param.value;
});
edit.value = !edit.value;
};
const get = async () => {
try {
loading.value = true;
const res = await GetAppInstallParams(Number(paramData.value.id));
params.value = res.data;
if (res.data && res.data.length > 0) {
res.data.forEach((d) => {
if (d.edit) {
console.log(d.edit);
canEdit.value = true;
}
let value = d.value;
if (d.type === 'number') {
value = Number(value);
}
params.value.push({
default: value,
labelEn: d.labelEn,
labelZh: d.labelZh,
rule: d.rule,
value: value,
edit: d.edit,
key: d.key,
type: d.type,
});
rules[d.key] = [Rules.requiredInput];
if (d.rule) {
rules[d.key].push(Rules[d.rule]);
}
});
}
} catch (error) {
} finally {
loading.value = false;
}
};
const getLabel = (row: EditForm): string => {
const language = useI18n().locale.value;
if (language == 'zh') {
return row.labelZh;
} else {
return row.labelEn;
}
};
const submit = async (formEl: FormInstance) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
ElMessageBox.confirm(i18n.global.t('app.updateWarn'), i18n.global.t('app.update'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
submitModel.value.params = paramModel.value;
try {
loading.value = true;
await UpdateAppInstallParams(submitModel.value);
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
} catch (error) {
loading.value = false;
}
});
});
};
defineExpose({ acceptParams });
</script>
<style lang="scss">
.change-button {
margin-top: 5px;
}
</style>

View File

@ -50,7 +50,7 @@
<template #main>
<div class="update-prompt" v-if="data == null">
<span>{{ mode === 'update' ? $t('app.updatePrompt') : $t('app.installPrompt') }}</span>
<span>{{ mode === 'upgrade' ? $t('app.updatePrompt') : $t('app.installPrompt') }}</span>
<div>
<img src="@/assets/images/no_update_app.svg" />
</div>
@ -129,7 +129,7 @@
round
size="small"
@click="openOperate(installed, 'update')"
v-if="mode === 'update'"
v-if="mode === 'upgrade'"
>
{{ $t('app.update') }}
</el-button>
@ -171,7 +171,7 @@
<AppResources ref="checkRef" />
<AppDelete ref="deleteRef" @close="search" />
<AppParams ref="appParamRef" />
<AppUpdate ref="updateRef" @close="search" />
<AppUpgrade ref="upgradeRef" @close="search" />
</template>
<script lang="ts" setup>
@ -191,7 +191,7 @@ import Uploads from '@/components/upload/index.vue';
import AppResources from './check/index.vue';
import AppDelete from './delete/index.vue';
import AppParams from './detail/index.vue';
import AppUpdate from './update/index.vue';
import AppUpgrade from './upgrade/index.vue';
import { App } from '@/api/interface/app';
import Status from '@/components/status/index.vue';
import { getAge } from '@/utils/util';
@ -217,7 +217,7 @@ const uploadRef = ref();
const checkRef = ref();
const deleteRef = ref();
const appParamRef = ref();
const updateRef = ref();
const upgradeRef = ref();
let tags = ref<App.Tag[]>([]);
let activeTag = ref('all');
let searchReq = reactive({
@ -273,7 +273,7 @@ const openOperate = (row: any, op: string) => {
operateReq.installId = row.id;
operateReq.operate = op;
if (op == 'update') {
updateRef.value.acceptParams(row.id, row.name);
upgradeRef.value.acceptParams(row.id, row.name);
} else if (op == 'delete') {
AppInstalledDeleteCheck(row.id).then(async (res) => {
const items = res.data;
@ -393,9 +393,9 @@ const openParam = (installId: number) => {
onMounted(() => {
const path = router.currentRoute.value.path;
if (path == '/apps/update') {
activeName.value = i18n.global.t('app.canUpdate');
mode.value = 'update';
if (path == '/apps/upgrade') {
activeName.value = i18n.global.t('app.canUpgrade');
mode.value = 'upgrade';
searchReq.update = true;
}
search();

View File

@ -46,7 +46,7 @@ let loading = ref(false);
let versions = ref<App.VersionDetail[]>();
let operateReq = reactive({
detailId: 0,
operate: 'update',
operate: 'upgrade',
installId: 0,
});
const resourceName = ref('');
@ -77,7 +77,7 @@ const operate = async () => {
await InstalledOp(operateReq)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
bus.emit('update', true);
bus.emit('upgrade', true);
handleClose();
})
.finally(() => {
@ -100,7 +100,7 @@ const onOperate = async () => {
};
onBeforeUnmount(() => {
bus.off('update');
bus.off('upgrade');
});
defineExpose({