feat(runtime): Add .NET runtime

This commit is contained in:
zhengkunwang223 2024-11-22 17:51:57 +08:00
parent c3565f72c4
commit 7018b1dc30
17 changed files with 746 additions and 19 deletions

View File

@ -1107,11 +1107,7 @@ func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error {
return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err)
} }
additionalProperties, ok := dataMap["additionalProperties"].(map[string]interface{}) additionalProperties, _ := dataMap["additionalProperties"].(map[string]interface{})
if !ok {
return buserr.WithName(constant.ErrAppParamKey, "additionalProperties")
}
formFieldsInterface, ok := additionalProperties["formFields"] formFieldsInterface, ok := additionalProperties["formFields"]
if ok { if ok {
formFields, ok := formFieldsInterface.([]interface{}) formFields, ok := formFieldsInterface.([]interface{})

View File

@ -83,7 +83,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
if exist != nil { if exist != nil {
return nil, buserr.New(constant.ErrImageExist) return nil, buserr.New(constant.ErrImageExist)
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
if !fileOp.Stat(create.CodeDir) { if !fileOp.Stat(create.CodeDir) {
return nil, buserr.New(constant.ErrPathNotFound) return nil, buserr.New(constant.ErrPathNotFound)
} }
@ -113,7 +113,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
} }
appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version) appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
if !fileOp.Stat(appVersionDir) || appDetail.Update { if !fileOp.Stat(appVersionDir) {
if err = downloadApp(app, appDetail, nil); err != nil { if err = downloadApp(app, appDetail, nil); err != nil {
return nil, err return nil, err
} }
@ -133,7 +133,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil { if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err return nil, err
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
runtime.Port = create.Port runtime.Port = create.Port
if err = handleNodeAndJava(create, runtime, fileOp, appVersionDir); err != nil { if err = handleNodeAndJava(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err return nil, err
@ -217,7 +217,7 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err) global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
} }
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete { if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" { if out != "" {
return errors.New(out) return errors.New(out)
@ -300,7 +300,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
} }
} }
res.AppParams = appParams res.AppParams = appParams
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
res.Params = make(map[string]interface{}) res.Params = make(map[string]interface{})
envs, err := gotenv.Unmarshal(runtime.Env) envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil { if err != nil {
@ -361,7 +361,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if exist != nil { if exist != nil {
return buserr.New(constant.ErrImageExist) return buserr.New(constant.ErrImageExist)
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
if runtime.Port != req.Port { if runtime.Port != req.Port {
if err = checkPortExist(req.Port); err != nil { if err = checkPortExist(req.Port); err != nil {
return err return err
@ -441,7 +441,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
return err return err
} }
go buildRuntime(runtime, imageID, req.Rebuild) go buildRuntime(runtime, imageID, req.Rebuild)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
runtime.Version = req.Version runtime.Version = req.Version
runtime.CodeDir = req.CodeDir runtime.CodeDir = req.CodeDir
runtime.Port = req.Port runtime.Port = req.Port
@ -613,7 +613,7 @@ func (r *RuntimeService) SyncRuntimeStatus() error {
return err return err
} }
for _, runtime := range runtimes { for _, runtime := range runtimes {
if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { if runtime.Type != constant.RuntimePHP {
_ = SyncRuntimeContainerStatus(&runtime) _ = SyncRuntimeContainerStatus(&runtime)
} }
} }

View File

@ -354,6 +354,14 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
if err != nil { if err != nil {
return return
} }
case constant.RuntimeDoNet:
create.Params["CODE_DIR"] = create.CodeDir
create.Params["DONET_VERSION"] = create.Version
create.Params["PANEL_APP_PORT_HTTP"] = create.Port
composeContent, err = handleCompose(env, composeContent, create, projectDir)
if err != nil {
return
}
} }
newMap := make(map[string]string) newMap := make(map[string]string)
@ -400,7 +408,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}") ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}")
case constant.RuntimeGo: case constant.RuntimeGo:
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}") ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}")
case constant.RuntimePython: case constant.RuntimePython, constant.RuntimeDoNet:
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${APP_PORT}") ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${APP_PORT}")
} }

View File

@ -352,7 +352,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
} }
website.Proxy = proxy website.Proxy = proxy
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port) website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port)
} }
} }

View File

@ -277,7 +277,7 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
server.UpdateRoot(rootIndex) server.UpdateRoot(rootIndex)
server.UpdatePHPProxy([]string{website.Proxy}, "") server.UpdatePHPProxy([]string{website.Proxy}, "")
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDoNet:
proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port) proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port)
server.UpdateRootProxy([]string{proxy}) server.UpdateRootProxy([]string{proxy})
} }

View File

@ -99,6 +99,7 @@ var WebUrlMap = map[string]struct{}{
"/websites/runtimes/net": {}, "/websites/runtimes/net": {},
"/websites/runtimes/go": {}, "/websites/runtimes/go": {},
"/websites/runtimes/python": {}, "/websites/runtimes/python": {},
"/websites/runtimes/donet": {},
"/login": {}, "/login": {},

View File

@ -19,6 +19,7 @@ const (
RuntimeJava = "java" RuntimeJava = "java"
RuntimeGo = "go" RuntimeGo = "go"
RuntimePython = "python" RuntimePython = "python"
RuntimeDoNet = "donet"
RuntimeProxyUnix = "unix" RuntimeProxyUnix = "unix"
RuntimeProxyTcp = "tcp" RuntimeProxyTcp = "tcp"

View File

@ -2428,6 +2428,7 @@ const message = {
goDirHelper: 'The directory must contain go files or binary files, subdirectories are also acceptable', goDirHelper: 'The directory must contain go files or binary files, subdirectories are also acceptable',
pythonHelper: pythonHelper:
'Please fill in the complete startup command, for example: pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000', 'Please fill in the complete startup command, for example: pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
donetHelper: 'Please fill in the complete startup comman, for example: dotnet MyWebApp.dll',
}, },
process: { process: {
pid: 'Process ID', pid: 'Process ID',

View File

@ -2247,6 +2247,7 @@ const message = {
goDirHelper: '目錄中要包含 go 文件或者二進制文件子目錄中包含也可', goDirHelper: '目錄中要包含 go 文件或者二進制文件子目錄中包含也可',
pythonHelper: pythonHelper:
'請填入完整啟動指令例如pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000', '請填入完整啟動指令例如pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
donetHelper: '請填入完整啟動指令例如 dotnet MyWebApp.dll',
}, },
process: { process: {
pid: '進程ID', pid: '進程ID',

View File

@ -2249,6 +2249,7 @@ const message = {
goDirHelper: '目录中要包含 go 文件或者二进制文件子目录中包含也可', goDirHelper: '目录中要包含 go 文件或者二进制文件子目录中包含也可',
pythonHelper: pythonHelper:
'请填写完整启动命令例如pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000', '请填写完整启动命令例如pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
donetHelper: '请填写完整启动命令例如 dotnet MyWebApp.dll',
}, },
process: { process: {
pid: '进程ID', pid: '进程ID',

View File

@ -88,6 +88,16 @@ const webSiteRouter = {
requiresAuth: false, requiresAuth: false,
}, },
}, },
{
path: '/websites/runtimes/donet',
name: 'doNet',
hidden: true,
component: () => import('@/views/website/runtime/donet/index.vue'),
meta: {
activeMenu: '/websites/runtimes/php',
requiresAuth: false,
},
},
], ],
}; };

View File

@ -237,6 +237,7 @@ const openInstall = (app: App.App) => {
case 'java': case 'java':
case 'go': case 'go':
case 'python': case 'python':
case 'donet':
router.push({ path: '/websites/runtimes/' + app.type }); router.push({ path: '/websites/runtimes/' + app.type });
break; break;
default: default:

View File

@ -146,6 +146,7 @@ const openInstall = () => {
case 'java': case 'java':
case 'go': case 'go':
case 'python': case 'python':
case 'donet':
router.push({ path: '/websites/runtimes/' + app.value.type }); router.push({ path: '/websites/runtimes/' + app.value.type });
break; break;
default: default:

View File

@ -67,10 +67,12 @@ const acceptParams = (): void => {
const goInstall = (key: string, type: string) => { const goInstall = (key: string, type: string) => {
switch (type) { switch (type) {
case 'php': case 'php':
router.push({ path: '/websites/runtimes/php' });
break;
case 'node': case 'node':
router.push({ path: '/websites/runtimes/node' }); case 'java':
case 'go':
case 'python':
case 'donet':
router.push({ path: '/websites/runtimes/' + type });
break; break;
default: default:
router.push({ name: 'AppAll', query: { install: key } }); router.push({ name: 'AppAll', query: { install: key } });

View File

@ -0,0 +1,306 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'.NET'" v-loading="loading">
<template #prompt>
<el-alert type="info" :closable="false">
<template #title>
<span class="input-help whitespace-break-spaces">
{{ $t('runtime.statusHelper') }}
</span>
</template>
</el-alert>
</template>
<template #toolbar>
<div class="flex flex-wrap gap-3">
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</div>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column
:label="$t('commons.table.name')"
fix
prop="name"
min-width="120px"
show-overflow-tooltip
>
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="openDetail(row)">
{{ row.name }}
</el-text>
</template>
</el-table-column>
<el-table-column :label="$t('website.runDir')" prop="codeDir">
<template #default="{ row }">
<el-button type="primary" link @click="toFolder(row.codeDir)">
<el-icon>
<FolderOpened />
</el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column :label="$t('runtime.externalPort')" prop="port">
<template #default="{ row }">
{{ row.port }}
<el-button link :icon="Promotion" @click="goDashboard(row.port, 'http')"></el-button>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.button.log')" prop="path">
<template #default="{ row }">
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 3"
:min-width="mobile ? 'auto' : 300"
:buttons="buttons"
fixed="right"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OperateDonet ref="operateRef" @close="search" />
<Delete ref="deleteRef" @close="search" />
<ComposeLogs ref="composeLogRef" />
<PortJumpDialog ref="dialogPortJumpRef" />
<AppResources ref="checkRef" @close="search" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, computed } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { OperateRuntime, RuntimeDeleteCheck, SearchRuntimes, SyncRuntime } from '@/api/modules/runtime';
import { dateFormat } from '@/utils/util';
import OperateDonet from '@/views/website/runtime/donet/operate/index.vue';
import Status from '@/components/status/index.vue';
import Delete from '@/views/website/runtime/delete/index.vue';
import i18n from '@/lang';
import RouterMenu from '../index.vue';
import router from '@/routers/router';
import ComposeLogs from '@/components/compose-log/index.vue';
import { Promotion } from '@element-plus/icons-vue';
import PortJumpDialog from '@/components/port-jump/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 { GlobalStore } from '@/store';
let timer: NodeJS.Timer | null = null;
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const operateRef = ref();
const deleteRef = ref();
const dialogPortJumpRef = ref();
const composeLogRef = ref();
const checkRef = ref();
const globalStore = GlobalStore();
const mobile = computed(() => {
return globalStore.isMobile();
});
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
const req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
type: 'donet',
});
const buttons = [
{
label: i18n.global.t('container.stop'),
click: function (row: Runtime.Runtime) {
operateRuntime('down', row.id);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'recreating' || row.status === 'stopped';
},
},
{
label: i18n.global.t('container.start'),
click: function (row: Runtime.Runtime) {
operateRuntime('up', row.id);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'starting' || row.status === 'recreating' || row.status === 'running';
},
},
{
label: i18n.global.t('container.restart'),
click: function (row: Runtime.Runtime) {
operateRuntime('restart', row.id);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'recreating';
},
},
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'recreating';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
},
];
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const sync = () => {
SyncRuntime();
};
const openCreate = () => {
operateRef.value.acceptParams({ type: 'donet', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
operateRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = (row: Runtime.Runtime) => {
RuntimeDeleteCheck(row.id).then(async (res) => {
const items = res.data;
if (res.data && res.data.length > 0) {
checkRef.value.acceptParams({ items: items, key: 'website', installID: row.id });
} else {
deleteRef.value.acceptParams(row.id, row.name);
}
});
};
const onOpenBuildCache = () => {
ElMessageBox.confirm(i18n.global.t('container.delBuildCacheHelper'), i18n.global.t('container.cleanBuildCache'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
let params = {
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const openLog = (row: any) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
};
const goDashboard = async (port: any, protocol: string) => {
dialogPortJumpRef.value.acceptParams({ port: port, protocol: protocol });
};
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 } });
};
onMounted(() => {
sync();
search();
timer = setInterval(() => {
search();
sync();
}, 1000 * 10);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,394 @@
<template>
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
:hideResource="mode == 'create'"
: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)"
class="p-w-200"
>
<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()"
class="p-w-200"
>
<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('tool.supervisor.dir')" prop="codeDir">
<el-input v-model.trim="runtime.codeDir" :disabled="mode === 'edit'">
<template #prepend>
<FileList
:disabled="mode === 'edit'"
: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-input v-model="runtime.params['EXEC_SCRIPT']"></el-input>
<span class="input-help">
{{ $t('runtime.donetHelper') }}
</span>
</el-form-item>
<el-row :gutter="20">
<el-col :span="7">
<el-form-item :label="$t('runtime.appPort')" prop="params.APP_PORT">
<el-input v-model.number="runtime.params['APP_PORT']" />
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item :label="$t('runtime.externalPort')" prop="port">
<el-input v-model.number="runtime.port" />
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="$t('commons.button.add') + $t('commons.table.port')">
<el-button @click="addPort">
<el-icon><Plus /></el-icon>
</el-button>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
<el-switch
v-model="runtime.params['HOST_IP']"
:active-value="'0.0.0.0'"
:inactive-value="'127.0.0.1'"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-for="(port, index) of runtime.exposedPorts" :key="index">
<el-col :span="7">
<el-form-item
:prop="'exposedPorts.' + index + '.containerPort'"
:rules="rules.params.APP_PORT"
>
<el-input v-model.number="port.containerPort" :placeholder="$t('runtime.appPort')" />
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item :prop="'exposedPorts.' + index + '.hostPort'" :rules="rules.params.APP_PORT">
<el-input v-model.number="port.hostPort" :placeholder="$t('runtime.externalPort')" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item>
<el-button type="primary" @click="removePort(index)" link>
{{ $t('commons.button.delete') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
<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, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgError, 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: 'donet',
page: 1,
pageSize: 20,
resource: 'remote',
});
const initData = (type: string) => ({
name: '',
appDetailID: undefined,
image: '',
params: {
HOST_IP: '0.0.0.0',
},
type: type,
resource: 'appstore',
rebuild: false,
codeDir: '/',
port: 8080,
exposedPorts: [],
});
let runtime = reactive<Runtime.RuntimeCreate>(initData('donet'));
const rules = ref<any>({
name: [Rules.requiredInput, Rules.appName],
appID: [Rules.requiredSelect],
codeDir: [Rules.requiredInput],
port: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
source: [Rules.requiredSelect],
params: {
APP_PORT: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
HOST_IP: [Rules.requiredSelect],
CONTAINER_NAME: [Rules.requiredInput, Rules.containerName],
EXEC_SCRIPT: [Rules.requiredInput],
},
});
const scripts = ref<Runtime.NodeScripts[]>([]);
const em = defineEmits(['close']);
watch(
() => runtime.params['APP_PORT'],
(newVal) => {
if (newVal && mode.value == 'create') {
runtime.port = newVal;
}
},
{ deep: true },
);
watch(
() => runtime.name,
(newVal) => {
if (newVal && mode.value == 'create') {
runtime.params['CONTAINER_NAME'] = newVal;
}
},
{ deep: true },
);
const handleClose = () => {
open.value = false;
em('close', false);
runtimeForm.value?.resetFields();
};
const getPath = (codeDir: string) => {
runtime.codeDir = codeDir;
};
const addPort = () => {
runtime.exposedPorts.push({
hostPort: undefined,
containerPort: undefined,
});
};
const removePort = (index: number) => {
runtime.exposedPorts.splice(index, 1);
};
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) {
if (mode === 'create') {
runtime.version = res.data.versions[0];
changeVersion();
}
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (runtime.exposedPorts && runtime.exposedPorts.length > 0) {
const containerPortMap = new Map();
const hostPortMap = new Map();
containerPortMap[runtime.params['APP_PORT']] = true;
hostPortMap[runtime.port] = true;
for (const port of runtime.exposedPorts) {
if (containerPortMap[port.containerPort]) {
MsgError(i18n.global.t('runtime.portError'));
return;
}
if (hostPortMap[port.hostPort]) {
MsgError(i18n.global.t('runtime.portError'));
return;
}
hostPortMap[port.hostPort] = true;
containerPortMap[port.containerPort] = true;
}
}
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,
type: data.type,
resource: data.resource,
appID: data.appID,
version: data.version,
rebuild: true,
source: data.source,
params: data.params,
codeDir: data.codeDir,
port: data.port,
});
runtime.exposedPorts = data.exposedPorts || [];
editParams.value = data.appParams;
searchApp(data.appID);
open.value = true;
} catch (error) {}
};
const acceptParams = async (props: OperateRrops) => {
mode.value = props.mode;
scripts.value = [];
if (props.mode === 'create') {
Object.assign(runtime, initData(props.type));
searchApp(null);
open.value = true;
} else {
getRuntime(props.id);
}
};
defineExpose({
acceptParams,
});
</script>

View File

@ -29,5 +29,9 @@ const buttons = [
label: 'Python', label: 'Python',
path: '/websites/runtimes/python', path: '/websites/runtimes/python',
}, },
{
label: '.NET',
path: '/websites/runtimes/donet',
},
]; ];
</script> </script>