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)
}
// @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"`
UpdateVersion string `json:"updateVersion"`
}
type AppstoreUpdate struct {
DefaultDomain string `json:"defaultDomain"`
}

View file

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

View file

@ -45,6 +45,9 @@ type IAppService interface {
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
SyncAppListFromLocal(taskID string)
GetIgnoredApp() ([]response.IgnoredApp, error)
GetAppstoreConfig() (*response.AppstoreConfig, error)
UpdateAppstoreConfig(req request.AppstoreUpdate) error
}
func NewIAppService() IAppService {
@ -1067,3 +1070,15 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
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":
exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified"))
if exist.ID == 0 {
_ = settingRepo.Create("AppStoreLastModified", value)
return nil
return settingRepo.Create("AppStoreLastModified", value)
}
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 {
return err
}
return nil
}

View file

@ -39,5 +39,7 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp)
appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions)
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;
webUI: string;
}
export interface AppStoreConfig {
defaultDomain: string;
}
}

View file

@ -110,3 +110,11 @@ export const GetIgnoredApp = () => {
export const UpdateInstallConfig = (req: App.AppConfigUpdate) => {
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) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
@ -577,6 +594,7 @@ interface CommonRule {
filePermission: FormItemRule;
phpExtensions: FormItemRule;
supervisorName: FormItemRule;
domainOrIP: FormItemRule;
paramCommon: FormItemRule;
paramComplexity: FormItemRule;
@ -806,4 +824,8 @@ export const Rules: CommonRule = {
validator: checkIpv4,
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',
webUI: 'Web Access Address',
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',

View file

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

View file

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

View file

@ -50,6 +50,17 @@ const appStoreRouter = {
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',
count: 0,
},
{
label: i18n.global.t('commons.button.set'),
path: '/apps/setting',
},
];
const search = () => {

View file

@ -7,27 +7,29 @@
</template>
<div v-if="!edit">
<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">
<span>{{ param.showValue && param.showValue != '' ? param.showValue : param.value }}</span>
</el-descriptions-item>
</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 v-else v-loading="loading">
<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-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">
<el-form-item :prop="p.key" :label="getLabel(p)">
<el-input
@ -150,6 +152,7 @@ const appConfigUpdate = ref<App.AppConfigUpdate>({
installID: 0,
webUI: '',
});
const openConfig = ref(false);
const acceptParams = async (props: ParamProps) => {
submitModel.value.installId = props.id;
@ -159,6 +162,7 @@ const acceptParams = async (props: ParamProps) => {
edit.value = false;
await get();
open.value = true;
openConfig.value = false;
};
const handleClose = () => {

View file

@ -175,23 +175,6 @@
</el-button>
</el-tooltip>
</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
class="h-button"
plain
@ -254,7 +237,6 @@
v-if="installed.httpPort > 0"
@click="goDashboard(installed.httpPort, 'http')"
class="tagMargin"
icon="Position"
plain
size="small"
>
@ -265,13 +247,60 @@
v-if="installed.httpsPort > 0"
@click="goDashboard(installed.httpsPort, 'https')"
class="tagMargin"
icon="Position"
plain
size="small"
>
{{ $t('app.busPort') }}{{ installed.httpsPort }}
</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">
<span>
{{ $t('app.alreadyRun') }}
@ -339,6 +368,7 @@ import {
SyncInstalledApp,
AppInstalledDeleteCheck,
GetAppTags,
GetAppStoreConfig,
} from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang';
@ -401,6 +431,8 @@ const activeName = ref(i18n.global.t('app.installed'));
const mode = ref('installed');
const moreTag = ref('');
const language = getLanguage();
const defaultLink = ref('');
const options = {
modifiers: [
{
@ -670,7 +702,17 @@ const toLink = (link: string) => {
window.open(link, '_blank');
};
const getAppstoreConfig = async () => {
try {
const res = await GetAppStoreConfig();
if (res.data.defaultDomain != '') {
defaultLink.value = res.data.defaultDomain;
}
} catch (error) {}
};
onMounted(() => {
getAppstoreConfig();
const path = router.currentRoute.value.path;
if (path == '/apps/upgrade') {
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>