feat: 应用商店设置增加默认访问地址 (#6250)
Some checks failed
sync2gitee / repo-sync (push) Failing after -8m19s

This commit is contained in:
zhengkunwang 2024-08-26 18:34:34 +08:00 committed by GitHub
parent 53cfb2e755
commit 7b9456453c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 325 additions and 36 deletions

View file

@ -209,3 +209,39 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
} }
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }
// @Tags App
// @Summary Update appstore config
// @Description 更新应用商店配置
// @Accept json
// @Param request body request.AppstoreUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /apps/store/update [post]
func (b *BaseApi) UpdateAppstoreConfig(c *gin.Context) {
var req request.AppstoreUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := appService.UpdateAppstoreConfig(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags App
// @Summary Get appstore config
// @Description 获取应用商店配置
// @Success 200 {object} response.AppstoreConfig
// @Security ApiKeyAuth
// @Router /apps/store/config [get]
func (b *BaseApi) GetAppstoreConfig(c *gin.Context) {
res, err := appService.GetAppstoreConfig()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View file

@ -125,3 +125,7 @@ type AppUpdateVersion struct {
AppInstallID uint `json:"appInstallID" validate:"required"` AppInstallID uint `json:"appInstallID" validate:"required"`
UpdateVersion string `json:"updateVersion"` UpdateVersion string `json:"updateVersion"`
} }
type AppstoreUpdate struct {
DefaultDomain string `json:"defaultDomain"`
}

View file

@ -159,3 +159,7 @@ type AppConfig struct {
Params []AppParam `json:"params"` Params []AppParam `json:"params"`
request.AppContainerConfig request.AppContainerConfig
} }
type AppstoreConfig struct {
DefaultDomain string `json:"defaultDomain"`
}

View file

@ -45,6 +45,9 @@ type IAppService interface {
GetAppDetailByID(id uint) (*response.AppDetailDTO, error) GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
SyncAppListFromLocal(taskID string) SyncAppListFromLocal(taskID string)
GetIgnoredApp() ([]response.IgnoredApp, error) GetIgnoredApp() ([]response.IgnoredApp, error)
GetAppstoreConfig() (*response.AppstoreConfig, error)
UpdateAppstoreConfig(req request.AppstoreUpdate) error
} }
func NewIAppService() IAppService { func NewIAppService() IAppService {
@ -1067,3 +1070,15 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
return nil return nil
} }
func (a AppService) UpdateAppstoreConfig(req request.AppstoreUpdate) error {
settingService := NewISettingService()
return settingService.Update("AppDefaultDomain", req.DefaultDomain)
}
func (a AppService) GetAppstoreConfig() (*response.AppstoreConfig, error) {
defaultDomain, _ := settingRepo.Get(settingRepo.WithByKey("AppDefaultDomain"))
res := &response.AppstoreConfig{}
res.DefaultDomain = defaultDomain.Value
return res, nil
}

View file

@ -46,14 +46,16 @@ func (u *SettingService) Update(key, value string) error {
case "AppStoreLastModified": case "AppStoreLastModified":
exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified")) exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified"))
if exist.ID == 0 { if exist.ID == 0 {
_ = settingRepo.Create("AppStoreLastModified", value) return settingRepo.Create("AppStoreLastModified", value)
return nil }
case "AppDefaultDomain":
exist, _ := settingRepo.Get(settingRepo.WithByKey("AppDefaultDomain"))
if exist.ID == 0 {
return settingRepo.Create("AppDefaultDomain", value)
} }
} }
if err := settingRepo.Update(key, value); err != nil { if err := settingRepo.Update(key, value); err != nil {
return err return err
} }
return nil return nil
} }

View file

@ -39,5 +39,7 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp) appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp)
appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions) appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions)
appRouter.POST("/installed/config/update", baseApi.UpdateAppConfig) appRouter.POST("/installed/config/update", baseApi.UpdateAppConfig)
appRouter.POST("/store/update", baseApi.UpdateAppstoreConfig)
appRouter.GET("/store/config", baseApi.GetAppstoreConfig)
} }
} }

View file

@ -263,4 +263,8 @@ export namespace App {
installID: number; installID: number;
webUI: string; webUI: string;
} }
export interface AppStoreConfig {
defaultDomain: string;
}
} }

View file

@ -110,3 +110,11 @@ export const GetIgnoredApp = () => {
export const UpdateInstallConfig = (req: App.AppConfigUpdate) => { export const UpdateInstallConfig = (req: App.AppConfigUpdate) => {
return http.post(`apps/installed/config/update`, req); return http.post(`apps/installed/config/update`, req);
}; };
export const GetAppStoreConfig = () => {
return http.get<App.AppStoreConfig>(`apps/store/config`);
};
export const UpdateAppStoreConfig = (req: App.AppStoreConfig) => {
return http.post(`apps/store/update`, req);
};

View file

@ -92,6 +92,23 @@ const checkIpV4V6OrDomain = (rule: any, value: any, callback: any) => {
} }
}; };
const checkDomainOrIP = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback();
} else {
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex =
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
if (ipv4Regex.test(value) || ipv6Regex.test(value) || domainRegex.test(value)) {
callback();
} else {
callback(new Error(i18n.global.t('commons.rule.domain')));
}
}
};
const checkHost = (rule: any, value: any, callback: any) => { const checkHost = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) { if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.requiredInput'))); callback(new Error(i18n.global.t('commons.rule.requiredInput')));
@ -577,6 +594,7 @@ interface CommonRule {
filePermission: FormItemRule; filePermission: FormItemRule;
phpExtensions: FormItemRule; phpExtensions: FormItemRule;
supervisorName: FormItemRule; supervisorName: FormItemRule;
domainOrIP: FormItemRule;
paramCommon: FormItemRule; paramCommon: FormItemRule;
paramComplexity: FormItemRule; paramComplexity: FormItemRule;
@ -806,4 +824,8 @@ export const Rules: CommonRule = {
validator: checkIpv4, validator: checkIpv4,
trigger: 'blur', trigger: 'blur',
}, },
domainOrIP: {
validator: checkDomainOrIP,
trigger: 'blur',
},
}; };

View file

@ -1885,6 +1885,11 @@ const message = {
'Please ensure the machine has an NVIDIA GPU and that NVIDIA drivers and the NVIDIA Docker Container Toolkit are installed', 'Please ensure the machine has an NVIDIA GPU and that NVIDIA drivers and the NVIDIA Docker Container Toolkit are installed',
webUI: 'Web Access Address', webUI: 'Web Access Address',
webUIPlaceholder: 'For example: http://example.com:8080/login', webUIPlaceholder: 'For example: http://example.com:8080/login',
defaultWebDomain: 'Default Access Address',
defaultWebDomainHepler:
'The default access is used for application port forwarding. For example, if the application port is 8080, the forwarding address would be http(s)://default-access-address:8080',
webUIConfig: 'Please add the access address in the application parameters or the app store settings',
toLink: 'Open',
}, },
website: { website: {
website: 'Website', website: 'Website',

View file

@ -1750,6 +1750,11 @@ const message = {
gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 NVIDIA docker Container Toolkit', gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 NVIDIA docker Container Toolkit',
webUI: 'Web 訪問地址', webUI: 'Web 訪問地址',
webUIPlaceholder: '例如http://example.com:8080/login', webUIPlaceholder: '例如http://example.com:8080/login',
defaultWebDomain: '默認訪問地址',
defaultWebDomainHepler:
'默認訪問用於應用端口跳轉例如應用端口為 8080 則跳轉地址為 http(s)://默認訪問地址:8080',
webUIConfig: '請在應用參數或者應用商店設置處添加訪問地址',
toLink: '連結',
}, },
website: { website: {
website: '網站', website: '網站',

View file

@ -1751,6 +1751,10 @@ const message = {
gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 NVIDIA docker Container Toolkit', gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 NVIDIA docker Container Toolkit',
webUI: 'Web 访问地址', webUI: 'Web 访问地址',
webUIPlaceholder: '例如http://example.com:8080/login', webUIPlaceholder: '例如http://example.com:8080/login',
defaultWebDomain: '默认访问地址',
defaultWebDomainHepler: '默认访问用于应用端口跳转例如应用端口为 8080 则跳转地址为http(s)://默认访问地址:8080',
webUIConfig: '请在应用参数或者应用商店设置处添加访问地址',
toLink: '跳转',
}, },
website: { website: {
website: '网站', website: '网站',

View file

@ -50,6 +50,17 @@ const appStoreRouter = {
requiresAuth: false, requiresAuth: false,
}, },
}, },
{
path: 'setting',
name: 'AppStoreSetting',
component: () => import('@/views/app-store/setting/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/apps',
requiresAuth: false,
},
},
], ],
}, },
], ],

View file

@ -29,6 +29,10 @@ const buttons = [
path: '/apps/upgrade', path: '/apps/upgrade',
count: 0, count: 0,
}, },
{
label: i18n.global.t('commons.button.set'),
path: '/apps/setting',
},
]; ];
const search = () => { const search = () => {

View file

@ -7,27 +7,29 @@
</template> </template>
<div v-if="!edit"> <div v-if="!edit">
<el-descriptions border :column="1"> <el-descriptions border :column="1">
<el-descriptions-item :label="$t('app.webUI')">
<span v-if="!openConfig">
{{ appConfigUpdate.webUI }}
<el-button type="primary" @click="openConfig = true">
{{ $t('commons.button.edit') }}
</el-button>
</span>
<el-input v-else v-model="appConfigUpdate.webUI" :placeholder="$t('app.webUIPlaceholder')">
<template #append>
<el-button type="primary" @click="updateAppConfig">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</el-input>
</el-descriptions-item>
<el-descriptions-item v-for="(param, key) in params" :label="getLabel(param)" :key="key"> <el-descriptions-item v-for="(param, key) in params" :label="getLabel(param)" :key="key">
<span>{{ param.showValue && param.showValue != '' ? param.showValue : param.value }}</span> <span>{{ param.showValue && param.showValue != '' ? param.showValue : param.value }}</span>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-form label-position="top" class="mt-2">
<el-form-item v-if="appType == 'website'" :label="$t('app.webUI')">
<el-input v-model="appConfigUpdate.webUI" :placeholder="$t('app.webUIPlaceholder')"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="loading" @click="updateAppConfig">
{{ $t('commons.button.confirm') }}
</el-button>
</el-form-item>
</el-form>
</div> </div>
<div v-else v-loading="loading"> <div v-else v-loading="loading">
<el-alert :title="$t('app.updateHelper')" type="warning" :closable="false" class="common-prompt" /> <el-alert :title="$t('app.updateHelper')" type="warning" :closable="false" class="common-prompt" />
<el-form @submit.prevent ref="paramForm" :model="paramModel" label-position="top" :rules="rules"> <el-form @submit.prevent ref="paramForm" :model="paramModel" label-position="top" :rules="rules">
<el-form-item v-if="appType == 'website'" :label="$t('app.webUI')">
<el-input v-model="appConfigUpdate.webUI" :placeholder="$t('app.webUIPlaceholder')"></el-input>
</el-form-item>
<div v-for="(p, index) in params" :key="index"> <div v-for="(p, index) in params" :key="index">
<el-form-item :prop="p.key" :label="getLabel(p)"> <el-form-item :prop="p.key" :label="getLabel(p)">
<el-input <el-input
@ -150,6 +152,7 @@ const appConfigUpdate = ref<App.AppConfigUpdate>({
installID: 0, installID: 0,
webUI: '', webUI: '',
}); });
const openConfig = ref(false);
const acceptParams = async (props: ParamProps) => { const acceptParams = async (props: ParamProps) => {
submitModel.value.installId = props.id; submitModel.value.installId = props.id;
@ -159,6 +162,7 @@ const acceptParams = async (props: ParamProps) => {
edit.value = false; edit.value = false;
await get(); await get();
open.value = true; open.value = true;
openConfig.value = false;
}; };
const handleClose = () => { const handleClose = () => {

View file

@ -175,23 +175,6 @@
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</span> </span>
<span class="ml-1">
<el-tooltip
v-if="installed.webUI !== ''"
effect="dark"
:content="installed.webUI"
placement="top"
>
<el-button
type="primary"
link
@click="toLink(installed.webUI)"
>
<el-icon><Promotion /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-button <el-button
class="h-button" class="h-button"
plain plain
@ -254,7 +237,6 @@
v-if="installed.httpPort > 0" v-if="installed.httpPort > 0"
@click="goDashboard(installed.httpPort, 'http')" @click="goDashboard(installed.httpPort, 'http')"
class="tagMargin" class="tagMargin"
icon="Position"
plain plain
size="small" size="small"
> >
@ -265,13 +247,60 @@
v-if="installed.httpsPort > 0" v-if="installed.httpsPort > 0"
@click="goDashboard(installed.httpsPort, 'https')" @click="goDashboard(installed.httpsPort, 'https')"
class="tagMargin" class="tagMargin"
icon="Position"
plain plain
size="small" size="small"
> >
{{ $t('app.busPort') }}{{ installed.httpsPort }} {{ $t('app.busPort') }}{{ installed.httpsPort }}
</el-button> </el-button>
<el-popover
placement="top-start"
trigger="hover"
v-if="installed.appType == 'website'"
:width="260"
>
<template #reference>
<el-button plain icon="Promotion" size="small">
{{ $t('app.toLink') }}
</el-button>
</template>
<table>
<tbody>
<tr v-if="defaultLink != ''">
<td>
<el-button
type="primary"
link
@click="
toLink(
defaultLink +
':' +
installed.httpPort,
)
"
>
{{ defaultLink + ':' + installed.httpPort }}
</el-button>
</td>
</tr>
<tr v-if="installed.webUI != ''">
<td>
<el-button
type="primary"
link
@click="toLink(installed.webUI)"
>
{{ installed.webUI }}
</el-button>
</td>
</tr>
</tbody>
</table>
<span v-if="defaultLink == '' && installed.webUI == ''">
{{ $t('app.webUIConfig') }}
</span>
</el-popover>
<div class="description"> <div class="description">
<span> <span>
{{ $t('app.alreadyRun') }} {{ $t('app.alreadyRun') }}
@ -339,6 +368,7 @@ import {
SyncInstalledApp, SyncInstalledApp,
AppInstalledDeleteCheck, AppInstalledDeleteCheck,
GetAppTags, GetAppTags,
GetAppStoreConfig,
} from '@/api/modules/app'; } from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
@ -401,6 +431,8 @@ const activeName = ref(i18n.global.t('app.installed'));
const mode = ref('installed'); const mode = ref('installed');
const moreTag = ref(''); const moreTag = ref('');
const language = getLanguage(); const language = getLanguage();
const defaultLink = ref('');
const options = { const options = {
modifiers: [ modifiers: [
{ {
@ -670,7 +702,17 @@ const toLink = (link: string) => {
window.open(link, '_blank'); window.open(link, '_blank');
}; };
const getAppstoreConfig = async () => {
try {
const res = await GetAppStoreConfig();
if (res.data.defaultDomain != '') {
defaultLink.value = res.data.defaultDomain;
}
} catch (error) {}
};
onMounted(() => { onMounted(() => {
getAppstoreConfig();
const path = router.currentRoute.value.path; const path = router.currentRoute.value.path;
if (path == '/apps/upgrade') { if (path == '/apps/upgrade') {
activeName.value = i18n.global.t('app.canUpgrade'); activeName.value = i18n.global.t('app.canUpgrade');

View file

@ -0,0 +1,117 @@
<template>
<LayoutContent :title="$t('commons.button.set')">
<template #main>
<el-form
:model="config"
label-position="left"
label-width="150px"
class="ml-2.5"
v-loading="loading"
:rules="rules"
ref="configForm"
>
<el-row>
<el-col :xs="24" :sm="20" :md="15" :lg="12" :xl="12">
<el-form-item :label="$t('app.defaultWebDomain')" prop="defaultDomain">
<el-input v-model="config.defaultDomain">
<template #prepend>
<el-select v-model="protocol" placeholder="Select" class="p-w-100">
<el-option label="HTTP" value="http://" />
<el-option label="HTTPS" value="https://" />
</el-select>
</template>
</el-input>
<span class="input-help">{{ $t('app.defaultWebDomainHepler') }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="loading" @click="submit()">
{{ $t('commons.button.confirm') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
</LayoutContent>
</template>
<script setup lang="ts">
import { GetAppStoreConfig, UpdateAppStoreConfig } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormRules } from 'element-plus';
const rules = ref<FormRules>({
defaultDomain: [Rules.domainOrIP],
});
const config = ref({
defaultDomain: '',
});
const loading = ref(false);
const configForm = ref();
const protocol = ref('http://');
function getUrl(url: string) {
const regex = /^(https?:\/\/)(.*)/;
const match = url.match(regex);
if (match) {
const protocol = match[1];
const remainder = match[2];
return {
protocol: protocol,
remainder: remainder,
};
} else {
return null;
}
}
const search = async () => {
loading.value = true;
try {
const res = await GetAppStoreConfig();
if (res.data.defaultDomain != '') {
const url = getUrl(res.data.defaultDomain);
if (url) {
config.value.defaultDomain = url.remainder;
protocol.value = url.protocol;
}
}
} catch (error) {
} finally {
loading.value = false;
}
};
const submit = async () => {
if (!configForm.value) return;
await configForm.value.validate(async (valid) => {
if (!valid) {
return;
}
loading.value = true;
try {
let defaultDomain = '';
if (config.value.defaultDomain) {
defaultDomain = protocol.value + config.value.defaultDomain;
}
const req = {
defaultDomain: defaultDomain,
};
await UpdateAppStoreConfig(req);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} catch (error) {
} finally {
loading.value = false;
search();
}
});
};
onMounted(() => {
search();
});
</script>