Commit 50992ed4 authored by Andy-bubu's avatar Andy-bubu

feat: restore grouped application switcher

parent 48f2819d
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
## 组件边界 ## 组件边界
- `Layout.jsx`:HeroUI Pro Sidebar/Navbar、主题切换、应用切换、退出。 - `Layout.jsx`:HeroUI Pro Sidebar/Navbar、主题切换、应用切换、退出。
- `ApplicationSwitcherDialog.jsx`:HeroUI Modal/Card/Surface/Avatar。 - `ApplicationSwitcherDialog.jsx`:HeroUI Modal/Card/Surface/Avatar;从 `SiteContext` 的动态 `site.group` 构建应用分组,不在 UI 层写死分组数据。
- `XinyuanModal.jsx`:HeroUI `Modal.Root > Backdrop > Container > Dialog` 的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方 `lg` Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的 `xl/4xl` size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。 - `XinyuanModal.jsx`:HeroUI `Modal.Root > Backdrop > Container > Dialog` 的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方 `lg` Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的 `xl/4xl` size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。
- `ModelMonitorShared.jsx`:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。 - `ModelMonitorShared.jsx`:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。
- `XinyuanAutocomplete.jsx`:HeroUI Autocomplete/SearchField/ListBox 组合,暴露当前 HeroUI 值契约,不兼容旧 Select props。 - `XinyuanAutocomplete.jsx`:HeroUI Autocomplete/SearchField/ListBox 组合,暴露当前 HeroUI 值契约,不兼容旧 Select props。
......
onlyBuiltDependencies: allowBuilds:
- '@heroui-pro/react' "@heroui-pro/react": true
- '@zowe/secrets-for-zowe-sdk' "@zowe/secrets-for-zowe-sdk": true
...@@ -34,6 +34,7 @@ export default function XinyuanModal({ ...@@ -34,6 +34,7 @@ export default function XinyuanModal({
> >
<Modal.Container placement="center" scroll="inside" size={modalSize}> <Modal.Container placement="center" scroll="inside" size={modalSize}>
<Modal.Dialog <Modal.Dialog
aria-label={title}
className={clsx( className={clsx(
'xy-modal-dialog p-7', 'xy-modal-dialog p-7',
DIALOG_WIDTH_CLASS[size], DIALOG_WIDTH_CLASS[size],
......
...@@ -44,6 +44,47 @@ function mergeApplications(fetched, fallbacks) { ...@@ -44,6 +44,47 @@ function mergeApplications(fetched, fallbacks) {
return [...merged.values()].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title, 'zh-CN')); return [...merged.values()].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title, 'zh-CN'));
} }
function buildApplicationSections(applications, applicationGroups) {
if (!Array.isArray(applicationGroups) || applicationGroups.length === 0) {
return [{ key: 'all', label: '全部应用', applications }];
}
const applicationMap = new Map(applications.map((application) => [application.name, application]));
const groupedApplicationNames = new Set();
const sections = applicationGroups
.map((group, index) => {
const groupApplications = Array.isArray(group?.applications)
? group.applications
.map((name) => {
const application = applicationMap.get(name);
if (application) groupedApplicationNames.add(name);
return application;
})
.filter(Boolean)
: [];
return {
key: group?.label || `group-${index}`,
label: group?.label || '应用分组',
applications: groupApplications,
};
})
.filter((section) => section.applications.length > 0);
const ungroupedApplications = applications.filter(
(application) => !groupedApplicationNames.has(application.name),
);
if (ungroupedApplications.length > 0) {
sections.push({
key: 'ungrouped',
label: '其他应用',
applications: ungroupedApplications,
});
}
return sections;
}
function isCurrentApplication(application) { function isCurrentApplication(application) {
if (application.name === APP_ROUTE_NAME) return true; if (application.name === APP_ROUTE_NAME) return true;
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
...@@ -87,12 +128,16 @@ function LoadingSkeleton() { ...@@ -87,12 +128,16 @@ function LoadingSkeleton() {
); );
} }
export default function ApplicationSwitcherDialog({ isOpen, onOpenChange, userInfo }) { export default function ApplicationSwitcherDialog({ isOpen, onOpenChange, siteInfo, userInfo }) {
const fallbacks = useMemo(() => (Array.isArray(userInfo?.applications) ? userInfo.applications : []), [userInfo]); const fallbacks = useMemo(() => (Array.isArray(userInfo?.applications) ? userInfo.applications : []), [userInfo]);
const [applications, setApplications] = useState(() => mergeApplications([], fallbacks)); const [applications, setApplications] = useState(() => mergeApplications([], fallbacks));
const [status, setStatus] = useState('idle'); const [status, setStatus] = useState('idle');
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const currentApplicationName = useMemo(() => applications.find(isCurrentApplication)?.name || APP_ROUTE_NAME, [applications]); const currentApplicationName = useMemo(() => applications.find(isCurrentApplication)?.name || APP_ROUTE_NAME, [applications]);
const applicationSections = useMemo(
() => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group],
);
useEffect(() => { useEffect(() => {
if (!isOpen) return undefined; if (!isOpen) return undefined;
...@@ -122,7 +167,27 @@ export default function ApplicationSwitcherDialog({ isOpen, onOpenChange, userIn ...@@ -122,7 +167,27 @@ export default function ApplicationSwitcherDialog({ isOpen, onOpenChange, userIn
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
> >
{status === 'error' && errorMessage ? <Alert status="warning"><Alert.Indicator><WarningOutlined className="size-5" /></Alert.Indicator><Alert.Content><Alert.Description>{errorMessage}</Alert.Description></Alert.Content></Alert> : null} {status === 'error' && errorMessage ? <Alert status="warning"><Alert.Indicator><WarningOutlined className="size-5" /></Alert.Indicator><Alert.Content><Alert.Description>{errorMessage}</Alert.Description></Alert.Content></Alert> : null}
{status === 'loading' && applications.length === 0 ? <LoadingSkeleton /> : applications.length ? <div className="grid gap-3 sm:grid-cols-2">{applications.map((application) => <ApplicationCard application={application} current={application.name === currentApplicationName} key={application.name} />)}</div> : <EmptyState size="md"><EmptyState.Header><EmptyState.Title>暂无可切换应用</EmptyState.Title><EmptyState.Description>当前账号还没有分配其它应用权限。</EmptyState.Description></EmptyState.Header></EmptyState>} {status === 'loading' && applications.length === 0 ? <LoadingSkeleton /> : applications.length ? (
<div className="space-y-6">
{applicationSections.map((section) => (
<section key={section.key}>
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-sm font-medium leading-5">{section.label}</h3>
<Description>{section.applications.length} 个应用</Description>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{section.applications.map((application) => (
<ApplicationCard
application={application}
current={application.name === currentApplicationName}
key={application.name}
/>
))}
</div>
</section>
))}
</div>
) : <EmptyState size="md"><EmptyState.Header><EmptyState.Title>暂无可切换应用</EmptyState.Title><EmptyState.Description>当前账号还没有分配其它应用权限。</EmptyState.Description></EmptyState.Header></EmptyState>}
</XinyuanModal> </XinyuanModal>
); );
} }
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
import ApplicationSwitcherDialog from '@/pages/project_manager_xy/ApplicationSwitcherDialog'; import ApplicationSwitcherDialog from '@/pages/project_manager_xy/ApplicationSwitcherDialog';
import SidebarPanelIcon from '@/components/SidebarPanelIcon'; import SidebarPanelIcon from '@/components/SidebarPanelIcon';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
import { SiteContext } from '@/wrapper/Site';
import { import {
AppstoreOutlined, AppstoreOutlined,
DatabaseOutlined, DatabaseOutlined,
...@@ -136,6 +137,7 @@ function SidebarContents({ pathname, userInfo, onOpenApplicationSwitcher }) { ...@@ -136,6 +137,7 @@ function SidebarContents({ pathname, userInfo, onOpenApplicationSwitcher }) {
export default function ProjectManagerLayout() { export default function ProjectManagerLayout() {
const location = useLocation(); const location = useLocation();
const userInfo = useContext(UserContext); const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
const [theme, setTheme] = useState(getInitialTheme); const [theme, setTheme] = useState(getInitialTheme);
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false); const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const pageTitle = useMemo(() => getRouteTitle(location.pathname), [location.pathname]); const pageTitle = useMemo(() => getRouteTitle(location.pathname), [location.pathname]);
...@@ -203,6 +205,7 @@ export default function ProjectManagerLayout() { ...@@ -203,6 +205,7 @@ export default function ProjectManagerLayout() {
<ApplicationSwitcherDialog <ApplicationSwitcherDialog
isOpen={isApplicationDialogOpen} isOpen={isApplicationDialogOpen}
siteInfo={siteInfo}
userInfo={userInfo} userInfo={userInfo}
onOpenChange={setIsApplicationDialogOpen} onOpenChange={setIsApplicationDialogOpen}
/> />
......
...@@ -20,6 +20,13 @@ ...@@ -20,6 +20,13 @@
旧 `/analysis/overview`、`/analysis/views`、`/analysis/forwards`、`/analysis/boards` 仅用于重定向,不再加载旧分析页面。 旧 `/analysis/overview`、`/analysis/views`、`/analysis/forwards`、`/analysis/boards` 仅用于重定向,不再加载旧分析页面。
### 2.1 应用切换
- 侧栏应用切换入口打开 HeroUI 应用切换弹窗,不改变当前模型监控路由和登录状态。
- 可访问应用继续由 `GET /application/info` 与当前登录用户的 `applications` 合并得到,保留原排序、回退和跳转规则。
- 应用分组从 `GET /info` 返回的 `site.group` 动态读取;每个分组通过 `applications` 中的应用 `name` 关联,前端不写死分组名称或成员。
- 没有动态分组配置时展示“全部应用”;存在动态分组时,未被任何分组引用的可访问应用展示在“其他应用”中,空分组不渲染。
## 3. 外部数据源契约 ## 3. 外部数据源契约
当前外部来源仍使用 Mock 适配器,但返回结构已映射到 `ANALYZE_MXVIEW_INFO` 和 `ANALYZE_MODEL_RUN_BATCH_INFO`。UI 仅依赖以下方法,真实 Oracle 适配器接入时不得修改页面调用: 当前外部来源仍使用 Mock 适配器,但返回结构已映射到 `ANALYZE_MXVIEW_INFO` 和 `ANALYZE_MODEL_RUN_BATCH_INFO`。UI 仅依赖以下方法,真实 Oracle 适配器接入时不得修改页面调用:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment