mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
Added the ability to sort installed applications. (#11936)
This commit is contained in:
@@ -349,3 +349,15 @@ func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
func (b *BaseApi) UpdateAppInstallSort(c *gin.Context) {
|
||||
var req request.AppInstallSort
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := appInstallService.UpdateSort(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
@@ -138,3 +138,12 @@ type AppUpdateVersion struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
UpdateVersion string `json:"updateVersion"`
|
||||
}
|
||||
|
||||
type AppInstallSortItem struct {
|
||||
InstallID uint `json:"installID"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
type AppInstallSort struct {
|
||||
Items []AppInstallSortItem `json:"items" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ type AppInstallDTO struct {
|
||||
WebUI string `json:"webUI"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Favorite bool `json:"favorite"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
App AppDetail `json:"app"`
|
||||
Container string `json:"container"`
|
||||
IsEdit bool `json:"isEdit"`
|
||||
|
||||
@@ -26,6 +26,7 @@ type AppInstall struct {
|
||||
HttpsPort int `json:"httpsPort"`
|
||||
WebUI string `json:"webUI"`
|
||||
Favorite bool `json:"favorite"`
|
||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||
|
||||
App App `json:"app" gorm:"-:migration"`
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ type IAppInstallService interface {
|
||||
UpdateAppConfig(req request.AppConfigUpdate) error
|
||||
GetInstallList() ([]dto.AppInstallInfo, error)
|
||||
GetAppInstallInfo(appInstallID uint) (*response.AppInstallInfo, error)
|
||||
UpdateSort(req request.AppInstallSort) error
|
||||
}
|
||||
|
||||
func NewIAppInstalledService() IAppInstallService {
|
||||
@@ -83,6 +84,7 @@ func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []respo
|
||||
err error
|
||||
)
|
||||
opts = append(opts, repo.WithOrderRuleBy("favorite", "descending"))
|
||||
opts = append(opts, repo.WithOrderRuleBy("sort_order", "ascending"))
|
||||
|
||||
if req.Name != "" {
|
||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||
@@ -300,6 +302,9 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return opNginx(install.ContainerName, constant.NginxReload)
|
||||
case constant.Favorite:
|
||||
install.Favorite = req.Favorite
|
||||
var maxSort int
|
||||
global.DB.Model(&model.AppInstall{}).Where("favorite = ?", req.Favorite).Select("COALESCE(MAX(sort_order),0)").Scan(&maxSort)
|
||||
install.SortOrder = maxSort + 1
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
default:
|
||||
return errors.New("operate not support")
|
||||
@@ -318,6 +323,15 @@ func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||
return appInstallRepo.Save(context.Background(), &installed)
|
||||
}
|
||||
|
||||
func (a *AppInstallService) UpdateSort(req request.AppInstallSort) error {
|
||||
for _, item := range req.Items {
|
||||
if err := appInstallRepo.BatchUpdateBy(map[string]interface{}{"sort_order": item.SortOrder}, repo.WithByID(item.InstallID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallId))
|
||||
if err != nil {
|
||||
|
||||
@@ -1613,6 +1613,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
Document: installed.App.Document,
|
||||
},
|
||||
Favorite: installed.Favorite,
|
||||
SortOrder: installed.SortOrder,
|
||||
Container: installed.ContainerName,
|
||||
ServiceName: strings.ToLower(installed.ServiceName),
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ func InitAgentDB() {
|
||||
migrations.AddAgentTables,
|
||||
migrations.MigrateOpenclawAgents,
|
||||
migrations.AddAgentCustomModelFields,
|
||||
migrations.AddAppInstallSortOrder,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -957,3 +957,10 @@ var AddAgentCustomModelFields = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddAppInstallSortOrder = &gormigrate.Migration{
|
||||
ID: "20260222-add-app-install-sort-order",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.AppInstall{})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
appRouter.POST("/installed/params/update", baseApi.UpdateInstalled)
|
||||
appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions)
|
||||
appRouter.POST("/installed/config/update", baseApi.UpdateAppConfig)
|
||||
appRouter.POST("/installed/sort/update", baseApi.UpdateAppInstallSort)
|
||||
appRouter.GET("/installed/info/:appInstallId", baseApi.GetAppInstallInfo)
|
||||
|
||||
appRouter.POST("/installed/ignore", baseApi.IgnoreAppUpgrade)
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"punycode": "^2.3.1",
|
||||
"qs": "^6.12.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.7",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.27",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
|
||||
@@ -123,6 +123,10 @@ export const updateInstallConfig = (req: App.AppConfigUpdate) => {
|
||||
return http.post(`apps/installed/config/update`, req);
|
||||
};
|
||||
|
||||
export const updateAppInstallSort = (items: Array<{ installID: number; sortOrder: number }>) => {
|
||||
return http.post(`apps/installed/sort/update`, { items });
|
||||
};
|
||||
|
||||
export const syncCutomAppStore = (req: App.AppStoreSync) => {
|
||||
return http.post(`/custom/app/sync`, req);
|
||||
};
|
||||
|
||||
@@ -2395,6 +2395,7 @@ const message = {
|
||||
takeDown: 'TakeDown',
|
||||
allReadyInstalled: 'Installed',
|
||||
installHelper: 'If you have image pull issues, configure image acceleration.',
|
||||
sortMode: 'Sort',
|
||||
installWarn: `The external access isn't checked, and it will make the application unable to access through external network. Do you want to continue?`,
|
||||
showIgnore: 'View ignored applications',
|
||||
cancelIgnore: 'Cancel ignore',
|
||||
|
||||
@@ -2409,6 +2409,7 @@ const message = {
|
||||
takeDown: 'Desinstalar',
|
||||
allReadyInstalled: 'Instaladas',
|
||||
installHelper: 'Si tiene problemas con el pull de imagen, configure aceleración.',
|
||||
sortMode: 'Ordenar',
|
||||
installWarn: 'Si no habilita el acceso externo, la app no será accesible externamente. ¿Desea continuar?',
|
||||
showIgnore: 'Ver aplicaciones ignoradas',
|
||||
cancelIgnore: 'Cancelar ignoradas',
|
||||
|
||||
@@ -2324,6 +2324,7 @@ const message = {
|
||||
takeDown: '降ろす',
|
||||
allReadyInstalled: 'インストール',
|
||||
installHelper: '画像プルの問題がある場合は、画像アクセラレーションを構成します。',
|
||||
sortMode: '並べ替え',
|
||||
installWarn: `外部アクセスは有効になっていないため、アプリケーションが外部ネットワークを介してアクセスできるようになります。続けたいですか?`,
|
||||
showIgnore: '無視されたアプリケーションを表示します',
|
||||
cancelIgnore: 'キャンセルは無視します',
|
||||
|
||||
@@ -2288,6 +2288,7 @@ const message = {
|
||||
takeDown: '내리기',
|
||||
allReadyInstalled: '설치됨',
|
||||
installHelper: '이미지 풀 문제 시 이미지 가속을 구성하세요.',
|
||||
sortMode: '정렬',
|
||||
installWarn:
|
||||
'외부 접근이 활성화되지 않아 애플리케이션이 외부 네트워크에서 접근할 수 없습니다. 계속 하시겠습니까?',
|
||||
showIgnore: '무시된 애플리케이션 보기',
|
||||
|
||||
@@ -2384,6 +2384,7 @@ const message = {
|
||||
takeDown: 'Henti Operasi',
|
||||
allReadyInstalled: 'Telah Dipasang',
|
||||
installHelper: 'Jika terdapat isu tarikan imej, konfigurasikan pecutan imej.',
|
||||
sortMode: 'Susun',
|
||||
installWarn:
|
||||
'Akses luaran tidak diaktifkan, yang menghalang aplikasi daripada diakses melalui rangkaian luaran. Adakah anda mahu meneruskan?',
|
||||
showIgnore: 'Lihat aplikasi yang diabaikan',
|
||||
|
||||
@@ -2378,6 +2378,7 @@ const message = {
|
||||
takeDown: 'Retirar',
|
||||
allReadyInstalled: 'Instalado',
|
||||
installHelper: 'Se houver problemas ao puxar a imagem, configure a aceleração da imagem.',
|
||||
sortMode: 'Ordenar',
|
||||
upgradeHelper:
|
||||
'Coloque aplicativos anormais de volta ao estado normal antes de atualizar. Se a atualização falhar, vá para "Logs > Logs do Sistema" para verificar a razão da falha.',
|
||||
installWarn: `O acesso externo não foi habilitado, o que impede que o aplicativo seja acessado via redes externas. Deseja continuar?`,
|
||||
|
||||
@@ -2376,6 +2376,7 @@ const message = {
|
||||
takeDown: 'Отключить',
|
||||
allReadyInstalled: 'Установлено',
|
||||
installHelper: 'Если есть проблемы с загрузкой образа, настройте ускорение образов.',
|
||||
sortMode: 'Сортировка',
|
||||
installWarn:
|
||||
'Внешний доступ не включен, что делает приложение недоступным через внешние сети. Хотите продолжить?',
|
||||
showIgnore: 'Просмотреть игнорируемые приложения',
|
||||
|
||||
@@ -2421,6 +2421,7 @@ const message = {
|
||||
takeDown: 'Kaldır',
|
||||
allReadyInstalled: 'Kurulu',
|
||||
installHelper: 'Görüntü çekme sorunlarınız varsa, görüntü hızlandırmasını yapılandırın.',
|
||||
sortMode: 'Sırala',
|
||||
installWarn:
|
||||
'Dış erişim işaretlenmedi, bu uygulama dış ağ üzerinden erişilemez hale getirecek. Devam etmek istiyor musunuz?',
|
||||
showIgnore: 'Yoksayılan uygulamaları görüntüle',
|
||||
|
||||
@@ -2231,6 +2231,7 @@ const message = {
|
||||
takeDown: '已廢棄',
|
||||
allReadyInstalled: '已安裝',
|
||||
installHelper: '配置鏡像加速可以解決鏡像拉取失敗的問題',
|
||||
sortMode: '排序',
|
||||
installWarn: '目前未勾選埠外部訪問,將無法透過外網IP:埠訪問,是否繼續? ',
|
||||
showIgnore: '查看忽略應用',
|
||||
cancelIgnore: '取消忽略',
|
||||
|
||||
@@ -2228,6 +2228,7 @@ const message = {
|
||||
takeDown: '已废弃',
|
||||
allReadyInstalled: '已安装',
|
||||
installHelper: '配置镜像加速可以解决镜像拉取失败的问题',
|
||||
sortMode: '排序',
|
||||
installWarn: '当前未勾选端口外部访问,将无法通过外网IP:端口访问,是否继续?',
|
||||
showIgnore: '查看忽略应用',
|
||||
cancelIgnore: '取消忽略',
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.install-card {
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -4,9 +4,23 @@
|
||||
<Tags @change="changeTag" hideKey="Runtime" />
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button @click="sync" type="primary" plain v-if="mode === 'installed' && data != null">
|
||||
<el-button @click="sync" type="primary" plain v-if="mode === 'installed' && !sortMode && data != null">
|
||||
{{ $t('commons.button.refresh') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="enterSortMode"
|
||||
type="primary"
|
||||
plain
|
||||
v-if="mode === 'installed' && !sortMode && data != null"
|
||||
>
|
||||
{{ $t('app.sortMode') }}
|
||||
</el-button>
|
||||
<el-button @click="saveSortOrder" type="primary" v-if="sortMode">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
<el-button @click="exitSortMode" plain v-if="sortMode">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button @click="openIgnore" type="primary" plain v-if="mode === 'upgrade'">
|
||||
{{ $t('app.showIgnore') }}
|
||||
</el-button>
|
||||
@@ -38,10 +52,11 @@
|
||||
<img src="@/assets/images/no_update_app.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<el-row :gutter="5">
|
||||
<el-row :gutter="5" ref="sortContainer">
|
||||
<el-col
|
||||
v-for="(installed, index) in data"
|
||||
:key="index"
|
||||
:key="installed.id"
|
||||
:data-sort-index="index"
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
@@ -90,7 +105,7 @@
|
||||
</el-row>
|
||||
</MainDiv>
|
||||
</div>
|
||||
<div class="page-button" v-if="mode === 'installed'">
|
||||
<div class="page-button" v-if="mode === 'installed' && !sortMode">
|
||||
<fu-table-pagination
|
||||
v-model:current-page="paginationConfig.currentPage"
|
||||
v-model:page-size="paginationConfig.pageSize"
|
||||
@@ -132,8 +147,9 @@ import ComposeLogs from '@/components/log/compose/index.vue';
|
||||
import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
|
||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||
|
||||
import { searchAppInstalled, installedOp, appInstalledDeleteCheck } from '@/api/modules/app';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, updateAppInstallSort } from '@/api/modules/app';
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick } from 'vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { App } from '@/api/interface/app';
|
||||
@@ -148,6 +164,9 @@ const { currentNode, isMaster, currentNodeAddr } = useGlobalStore();
|
||||
const data = ref<any>();
|
||||
const loading = ref(false);
|
||||
const syncLoading = ref(false);
|
||||
const sortMode = ref(false);
|
||||
const sortContainer = ref();
|
||||
let sortableInstance: Sortable | null = null;
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'app-installed-page-size',
|
||||
@@ -429,6 +448,61 @@ const openTerminal = (row: any) => {
|
||||
dialogTerminalRef.value!.acceptParams({ containerID: row.container, title: title });
|
||||
};
|
||||
|
||||
const enterSortMode = async () => {
|
||||
sortMode.value = true;
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
const res = await searchAppInstalled({ page: 1, pageSize: 10000, name: '', tags: [], update: false, sync: false });
|
||||
data.value = res.data.items;
|
||||
await nextTick();
|
||||
const el = sortContainer.value?.$el;
|
||||
if (el) {
|
||||
const favCount = data.value.filter((i: any) => i.favorite).length;
|
||||
sortableInstance = Sortable.create(el, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onMove: (evt: any) => {
|
||||
const from = evt.dragged.dataset.sortIndex;
|
||||
const to = evt.related.dataset.sortIndex;
|
||||
const fromFav = Number(from) < favCount;
|
||||
const toFav = Number(to) < favCount;
|
||||
return fromFav === toFav;
|
||||
},
|
||||
onEnd: (evt: any) => {
|
||||
const list = [...data.value];
|
||||
const [moved] = list.splice(evt.oldIndex, 1);
|
||||
list.splice(evt.newIndex, 0, moved);
|
||||
data.value = list;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveSortOrder = async () => {
|
||||
if (!data.value) return;
|
||||
let favIdx = 0;
|
||||
let normalIdx = 0;
|
||||
const items = data.value.map((item: any) => ({
|
||||
installID: item.id,
|
||||
sortOrder: item.favorite ? favIdx++ : normalIdx++,
|
||||
}));
|
||||
await updateAppInstallSort(items);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
exitSortMode();
|
||||
};
|
||||
|
||||
const exitSortMode = () => {
|
||||
if (sortableInstance) {
|
||||
sortableInstance.destroy();
|
||||
sortableInstance = null;
|
||||
}
|
||||
sortMode.value = false;
|
||||
search();
|
||||
timer = setInterval(() => {
|
||||
search();
|
||||
}, 1000 * 30);
|
||||
};
|
||||
|
||||
const getConfig = async () => {
|
||||
try {
|
||||
const res = await getAgentSettingByKey('SystemIP');
|
||||
|
||||
Reference in New Issue
Block a user