mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
feat: reconcile hide menu integrity (#13681)
This commit is contained in:
@@ -147,7 +147,7 @@ func repairAndSortHideMenu(settingMap map[string]string) {
|
||||
return
|
||||
}
|
||||
|
||||
menus, changed := menutree.EnsureXpackAppMenus(menus, nil)
|
||||
menus, changed := menutree.ReconcileHideMenuIntegrity(menus, nil)
|
||||
if changed {
|
||||
repairedBytes, err := json.Marshal(menus)
|
||||
if err != nil {
|
||||
@@ -213,7 +213,7 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error {
|
||||
if len(menus) == 0 {
|
||||
return fmt.Errorf("hide menu cannot be empty")
|
||||
}
|
||||
menus, _ = menutree.EnsureXpackAppMenus(menus, previousMenus)
|
||||
menus, _ = menutree.ReconcileHideMenuIntegrity(menus, previousMenus)
|
||||
for i := 0; i < len(menus); i++ {
|
||||
if menus[i].Label == "Home-Menu" || menus[i].Label == "App-Menu" || menus[i].Label == "Setting-Menu" {
|
||||
menus[i].IsShow = true
|
||||
|
||||
@@ -131,7 +131,6 @@ func LoadMenus() string {
|
||||
{ID: "111", Disabled: false, Title: "xpack.node.nodeManagement", IsShow: true, Label: "NodeDashboard", Path: "/xpack/node/dashboard", Sort: 300},
|
||||
{ID: "113", Disabled: false, Title: "xpack.monitor.name", IsShow: true, Label: "MonitorDashboard", Path: "/xpack/monitor/dashboard", Sort: 600},
|
||||
{ID: "115", Disabled: false, Title: "xpack.sync.menu", IsShow: true, Label: "Sync", Path: "/xpack/sync", Sort: 700},
|
||||
{ID: "119", Disabled: false, Title: "xpack.upage", IsShow: true, Label: "Upage", Path: "/xpack/upage", Sort: 800},
|
||||
{ID: "114", Disabled: false, Title: "xpack.tamper.tamper", IsShow: true, Label: "Tamper", Path: "/xpack/tamper", Sort: 1000},
|
||||
{ID: "120", Disabled: false, Title: "xpack.cluster.cluster", IsShow: true, Label: "Cluster", Path: "/xpack/cluster", Sort: 1100},
|
||||
{ID: "117", Disabled: false, Title: "xpack.setting.setting", IsShow: true, Label: "XSetting", Path: "/xpack/setting", Sort: 1200},
|
||||
@@ -200,7 +199,7 @@ func LoadMenus() string {
|
||||
Label: "VirtualMachine",
|
||||
Path: "/enterprise/vm",
|
||||
Sort: 900,
|
||||
}, "Upage")
|
||||
}, "Sync")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -255,7 +254,6 @@ func XpackMenuSort() []dto.MenuLabelSort {
|
||||
{Label: "OpsReport", Sort: 500},
|
||||
{Label: "MonitorDashboard", Sort: 600},
|
||||
{Label: "Sync", Sort: 700},
|
||||
{Label: "Upage", Sort: 800},
|
||||
{Label: "VirtualMachine", Sort: 900},
|
||||
{Label: "Tamper", Sort: 1000},
|
||||
{Label: "Cluster", Sort: 1100},
|
||||
|
||||
@@ -65,5 +65,6 @@ func coreMigrations() []*gormigrate.Migration {
|
||||
migrations.AddWebsiteTemplateMenu,
|
||||
migrations.RepairXpackAppMenus,
|
||||
migrations.UpdateFirewallMenuPath,
|
||||
migrations.RemoveUpageHideMenu,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,40 +1333,47 @@ var AddWebsiteTemplateMenu = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
func reconcileHideMenuSetting(tx *gorm.DB) error {
|
||||
var setting model.Setting
|
||||
if err := tx.Where("key = ?", "HideMenu").First(&setting).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return tx.Create(&model.Setting{Key: "HideMenu", Value: helper.LoadMenus()}).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
updateValue := func(value string) error {
|
||||
return tx.Model(&setting).Update("value", value).Error
|
||||
}
|
||||
|
||||
if strings.TrimSpace(setting.Value) == "" {
|
||||
return updateValue(helper.LoadMenus())
|
||||
}
|
||||
|
||||
var menus []dto.ShowMenu
|
||||
if err := json.Unmarshal([]byte(setting.Value), &menus); err != nil || len(menus) == 0 {
|
||||
return updateValue(helper.LoadMenus())
|
||||
}
|
||||
|
||||
updatedMenus, changed := menutree.ReconcileHideMenuIntegrity(menus, nil)
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
updatedJSON, err := json.Marshal(updatedMenus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateValue(string(updatedJSON))
|
||||
}
|
||||
|
||||
var RepairXpackAppMenus = &gormigrate.Migration{
|
||||
ID: "20260818-repair-xapp-upage-hide-menu",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var setting model.Setting
|
||||
if err := tx.Where("key = ?", "HideMenu").First(&setting).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return tx.Create(&model.Setting{Key: "HideMenu", Value: helper.LoadMenus()}).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
ID: "20260818-repair-xapp-upage-hide-menu",
|
||||
Migrate: reconcileHideMenuSetting,
|
||||
}
|
||||
|
||||
updateValue := func(value string) error {
|
||||
return tx.Model(&setting).Update("value", value).Error
|
||||
}
|
||||
|
||||
if strings.TrimSpace(setting.Value) == "" {
|
||||
return updateValue(helper.LoadMenus())
|
||||
}
|
||||
|
||||
var menus []dto.ShowMenu
|
||||
if err := json.Unmarshal([]byte(setting.Value), &menus); err != nil || len(menus) == 0 {
|
||||
return updateValue(helper.LoadMenus())
|
||||
}
|
||||
|
||||
updatedMenus, changed := menutree.EnsureXpackAppMenus(menus, nil)
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
updatedJSON, err := json.Marshal(updatedMenus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateValue(string(updatedJSON))
|
||||
},
|
||||
var RemoveUpageHideMenu = &gormigrate.Migration{
|
||||
ID: "20260901-remove-upage-hide-menu",
|
||||
Migrate: reconcileHideMenuSetting,
|
||||
}
|
||||
|
||||
var UpdateFirewallMenuPath = &gormigrate.Migration{
|
||||
|
||||
@@ -32,34 +32,46 @@ func preserveMissingMenus(root, current *[]dto.ShowMenu, fallback []dto.ShowMenu
|
||||
return changed
|
||||
}
|
||||
|
||||
func EnsureXpackAppMenus(menus, fallback []dto.ShowMenu) ([]dto.ShowMenu, bool) {
|
||||
func ReconcileHideMenuIntegrity(menus, fallback []dto.ShowMenu) ([]dto.ShowMenu, bool) {
|
||||
updated := cloneMenus(menus)
|
||||
parentIndex := findXpackMenu(updated)
|
||||
fallbackParentIndex := findXpackMenu(fallback)
|
||||
|
||||
changed := false
|
||||
if preferredXApp := findSystemMenu(updated, defaultXAppMenu()); preferredXApp != nil {
|
||||
changed = normalizeSystemFields(preferredXApp, defaultXAppMenu())
|
||||
}
|
||||
|
||||
var removed bool
|
||||
updated, removed = removeSystemMenus(updated, retiredUpageMenuIdentity())
|
||||
sanitizedFallback := cloneMenus(fallback)
|
||||
if preferredFallbackXApp := findSystemMenu(sanitizedFallback, defaultXAppMenu()); preferredFallbackXApp != nil {
|
||||
normalizeSystemFields(preferredFallbackXApp, defaultXAppMenu())
|
||||
}
|
||||
sanitizedFallback, _ = removeSystemMenus(sanitizedFallback, retiredUpageMenuIdentity())
|
||||
changed = changed || removed
|
||||
if existingXApp := findSystemMenu(updated, defaultXAppMenu()); existingXApp != nil {
|
||||
normalized := normalizeSystemFields(existingXApp, defaultXAppMenu())
|
||||
deduplicated, deduplicatedChanged := deduplicateSystemMenu(updated, existingXApp, defaultXAppMenu())
|
||||
return deduplicated, changed || normalized || deduplicatedChanged
|
||||
}
|
||||
|
||||
parentIndex := findXpackMenu(updated)
|
||||
if parentIndex < 0 {
|
||||
parent := defaultXpackMenu()
|
||||
fallbackParentIndex := findXpackMenu(sanitizedFallback)
|
||||
if fallbackParentIndex >= 0 {
|
||||
parent = cloneMenu(fallback[fallbackParentIndex])
|
||||
parent = cloneMenu(sanitizedFallback[fallbackParentIndex])
|
||||
}
|
||||
updated = append(updated, parent)
|
||||
parentIndex = len(updated) - 1
|
||||
changed = true
|
||||
}
|
||||
|
||||
required := []dto.ShowMenu{defaultXAppMenu(), defaultUpageMenu()}
|
||||
for _, canonical := range required {
|
||||
parentIndex = findXpackMenu(updated)
|
||||
var childChanged bool
|
||||
updated, childChanged = ensureSystemMenu(
|
||||
updated,
|
||||
parentIndex,
|
||||
fallback,
|
||||
canonical,
|
||||
)
|
||||
changed = changed || childChanged
|
||||
}
|
||||
updated, xAppChanged := ensureSystemMenu(
|
||||
updated,
|
||||
parentIndex,
|
||||
sanitizedFallback,
|
||||
defaultXAppMenu(),
|
||||
)
|
||||
changed = changed || xAppChanged
|
||||
|
||||
return updated, changed
|
||||
}
|
||||
@@ -87,15 +99,11 @@ func defaultXAppMenu() dto.ShowMenu {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultUpageMenu() dto.ShowMenu {
|
||||
func retiredUpageMenuIdentity() dto.ShowMenu {
|
||||
return dto.ShowMenu{
|
||||
ID: "119",
|
||||
Disabled: false,
|
||||
Title: "xpack.upage",
|
||||
IsShow: true,
|
||||
Label: "Upage",
|
||||
Path: "/xpack/upage",
|
||||
Sort: 800,
|
||||
ID: "119",
|
||||
Label: "Upage",
|
||||
Path: "/xpack/upage",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +239,29 @@ func deduplicateSystemMenu(menus []dto.ShowMenu, selected *dto.ShowMenu, canonic
|
||||
return deduplicated, changed
|
||||
}
|
||||
|
||||
func removeSystemMenus(menus []dto.ShowMenu, identity dto.ShowMenu) ([]dto.ShowMenu, bool) {
|
||||
if menus == nil {
|
||||
return nil, false
|
||||
}
|
||||
filtered := make([]dto.ShowMenu, 0, len(menus))
|
||||
changed := false
|
||||
for i := range menus {
|
||||
menu := &menus[i]
|
||||
children, childChanged := removeSystemMenus(menu.Children, identity)
|
||||
if matchesSystemIdentity(*menu, identity) {
|
||||
filtered = append(filtered, children...)
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
if childChanged {
|
||||
menu.Children = children
|
||||
changed = true
|
||||
}
|
||||
filtered = append(filtered, *menu)
|
||||
}
|
||||
return filtered, changed
|
||||
}
|
||||
|
||||
func matchesSystemIdentity(menu, canonical dto.ShowMenu) bool {
|
||||
return menu.ID == canonical.ID || menu.Label == canonical.Label || menu.Path == canonical.Path
|
||||
}
|
||||
|
||||
@@ -19,16 +19,6 @@
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-else-if="subItem.path === '/xpack/upage'" :index="''" @click="goUpage">
|
||||
<el-icon v-if="subItem.meta?.icon && level === 0">
|
||||
<SvgIcon :iconName="subItem.meta?.icon as string" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span v-if="subItem.meta?.icon && level === 0">{{ $t(subItem.meta?.title as string, 2) }}</span>
|
||||
<span v-else style="margin-left: 10px">{{ $t(subItem.meta?.title as string, 2) }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-else :index="subItem.path">
|
||||
<el-icon v-if="subItem.meta?.icon && level === 0">
|
||||
<SvgIcon :iconName="subItem.meta?.icon as string" />
|
||||
@@ -46,10 +36,6 @@ import { RouteRecordRaw } from 'vue-router';
|
||||
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
|
||||
|
||||
defineProps<{ menuList: RouteRecordRaw[]; level?: number }>();
|
||||
|
||||
const goUpage = () => {
|
||||
window.open('https://www.lxware.cn/upage', '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { RouteRecordRaw, useRoute } from 'vue-router';
|
||||
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { loadingSvg } from '@/utils/svg';
|
||||
import Logo from './components/Logo.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
@@ -42,8 +42,9 @@ import { hasPermissionMetaAccess, hasRouteRoleAccess } from '@/utils/rbac';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const menuStore = MenuStore();
|
||||
const { currentNode, isAdmin, isEE, isIntl, menuAccordion, permissions } = useGlobalStore();
|
||||
const { currentNode, isAdmin, menuAccordion, permissions } = useGlobalStore();
|
||||
const version = ref();
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
@@ -53,9 +54,29 @@ const activeMenu = computed(() => {
|
||||
const isCollapse = computed((): boolean => menuStore.isCollapse);
|
||||
|
||||
let routerMenus = computed((): RouteRecordRaw[] => {
|
||||
return menuStore.menuList.filter((route) => route.meta && !route.meta.hideInSidebar) as RouteRecordRaw[];
|
||||
return buildRegisteredMenuList(menuStore.menuList as RouteRecordRaw[]).filter(
|
||||
(route) => route.meta && !route.meta.hideInSidebar,
|
||||
);
|
||||
});
|
||||
|
||||
function buildRegisteredMenuList(source: RouteRecordRaw[]): RouteRecordRaw[] {
|
||||
return source.reduce<RouteRecordRaw[]>((result, item) => {
|
||||
if (!item.name || !router.hasRoute(item.name)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const menuItem = { ...item };
|
||||
if (Array.isArray(item.children)) {
|
||||
menuItem.children = buildRegisteredMenuList(item.children);
|
||||
if (item.children.length > 0 && menuItem.children.length === 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result.push(menuItem);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const screenWidth = ref(0);
|
||||
const listeningWindow = () => {
|
||||
window.onresize = () => {
|
||||
@@ -192,17 +213,7 @@ function buildVisibleMenu(menu: RouteRecordRaw, showSet: Set<string>): RouteReco
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
const visibleChildren = children
|
||||
.map((item) => {
|
||||
if (item.name === 'Upage' && (isIntl.value || (isEE.value && !isAdmin.value))) {
|
||||
return null;
|
||||
}
|
||||
if (item.name === 'XApp' && isIntl.value) {
|
||||
return null;
|
||||
}
|
||||
return buildVisibleMenu(item, showSet);
|
||||
})
|
||||
.filter(Boolean) as RouteRecordRaw[];
|
||||
const visibleChildren = children.map((item) => buildVisibleMenu(item, showSet)).filter(Boolean) as RouteRecordRaw[];
|
||||
|
||||
menuItem.children = visibleChildren;
|
||||
if (menuItem.children.length === 0) {
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
<div class="menu-setting-card__label mb-3">{{ $t('setting.menuHide') }}</div>
|
||||
<el-alert :closable="false" :title="$t('setting.menuSettingHelper')" type="warning" />
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
:data="treeData.hideMenu"
|
||||
:allow-drag="allowDrag"
|
||||
:allow-drop="allowDrop"
|
||||
:filter-node-method="filterMenu"
|
||||
draggable
|
||||
node-key="id"
|
||||
class="mt-3 menu-hide-tree"
|
||||
@@ -77,19 +75,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
import { AllowDropType, ElMessageBox, ElTree, RenderContentContext } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { AllowDropType, ElMessageBox, RenderContentContext } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { defaultMenu, updateMenu, updateSetting } from '@/api/modules/setting';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { sortMenu } from '@/utils/misc';
|
||||
const { isEE, isIntl, isAdmin, menuAccordion } = useGlobalStore();
|
||||
const { menuAccordion } = useGlobalStore();
|
||||
|
||||
const drawerVisible = ref();
|
||||
const loading = ref();
|
||||
const menuTreeRef = ref<InstanceType<typeof ElTree>>();
|
||||
const em = defineEmits(['search']);
|
||||
interface DialogProps {
|
||||
hideMenu: string;
|
||||
@@ -106,7 +103,6 @@ const acceptParams = (params: DialogProps): void => {
|
||||
let hideMenu = JSON.parse(params.hideMenu);
|
||||
sortMenu(hideMenu);
|
||||
treeData.hideMenu = hideMenu;
|
||||
nextTick(() => menuTreeRef.value?.filter(true));
|
||||
};
|
||||
type Node = RenderContentContext['node'];
|
||||
|
||||
@@ -150,16 +146,8 @@ const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
|
||||
const handleDrop = (draggingNode: Node, dropNode: Node) => {
|
||||
const siblingNodes = dropNode.level == 2 ? dropNode.parent.parent.data : dropNode.parent.data;
|
||||
const updateSort = (nodes) => {
|
||||
const reservedSorts = new Set(nodes.filter((node) => !isMenuVisible(node)).map((node) => node.sort));
|
||||
let nextSort = 100;
|
||||
nodes.forEach((node) => {
|
||||
if (isMenuVisible(node)) {
|
||||
while (reservedSorts.has(nextSort)) {
|
||||
nextSort += 100;
|
||||
}
|
||||
node.sort = nextSort;
|
||||
nextSort += 100;
|
||||
}
|
||||
nodes.forEach((node, index) => {
|
||||
node.sort = (index + 1) * 100;
|
||||
if (node.children && node.children.length) {
|
||||
updateSort(node.children);
|
||||
}
|
||||
@@ -176,23 +164,10 @@ const treeData = reactive({
|
||||
checkedData: [],
|
||||
});
|
||||
|
||||
const isMenuVisible = (data: { label: string }) => {
|
||||
if (data.label === 'Upage') {
|
||||
return !(isIntl.value || (isEE.value && !isAdmin.value));
|
||||
}
|
||||
if (data.label === 'XApp') {
|
||||
return !isIntl.value;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const filterMenu = (_value: boolean, data: { label: string }) => isMenuVisible(data);
|
||||
|
||||
const onChangeShow = async (row: any) => {
|
||||
if (row.children) {
|
||||
for (const item of row.children) {
|
||||
if (isMenuVisible(item)) {
|
||||
item.isShow = row.isShow;
|
||||
}
|
||||
item.isShow = row.isShow;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -202,9 +177,6 @@ const onChangeShow = async (row: any) => {
|
||||
}
|
||||
let allHide = true;
|
||||
for (const item2 of item.children) {
|
||||
if (!isMenuVisible(item2)) {
|
||||
continue;
|
||||
}
|
||||
if (item2.isShow) {
|
||||
allHide = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user