feat: reconcile hide menu integrity (#13681)

This commit is contained in:
2026-09-01 17:52:12 +08:00
committed by GitHub
parent b361f464c5
commit d7edbd1e95
8 changed files with 130 additions and 124 deletions

View File

@@ -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

View File

@@ -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},

View File

@@ -65,5 +65,6 @@ func coreMigrations() []*gormigrate.Migration {
migrations.AddWebsiteTemplateMenu,
migrations.RepairXpackAppMenus,
migrations.UpdateFirewallMenuPath,
migrations.RemoveUpageHideMenu,
}
}

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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;
}