feat: mobile-responsive UI for layouts, filters, tables, and playground#1272
feat: mobile-responsive UI for layouts, filters, tables, and playground#1272axsapronov wants to merge 13 commits into
Conversation
Adds local lint/typecheck scripts, validate wrapper, CI lint step, and contributor docs for the validation workflow.
Sets width=device-width and initial-scale=1 for correct mobile rendering.
Thin re-exports for getResponsiveLayout and getDrawerWidth; simplifies useWindowResize to delegate breakpoint logic to core-ui.
Hides the sider below 768px, adds a mobile nav drawer, and compacts the app header and shared PageBox title bar for small screens.
Replaces legacy left filter panels with useFilterDrawer, side FilterForm drawers, and inline desktop selects across list and resource pages.
Adds hideBelow, mobileTitle, and flex sizing hints so tables can render as mobile cards without losing important cell content.
Swaps ant Design Table for AntdResponsiveTable on CRUD list pages with horizontal scroll and smaller pagination on mobile.
Mobile layouts for playground, LLModels deploy flows, dashboard charts, benchmark detail views, and compact resource toolbars.
Uses USAGE_FULL_DATA_PAGINATION instead of the deprecated page:-1 sentinel when loading the full filtered usage dataset.
Introduce vitest for pure-function tests and fix menu filtering when items expose children via routes instead of children.
Prevent Infinity% ECharts radius values on initial mount when the chart container has not been measured yet.
Ensure ProviderLogo always receives a string when config.type is missing.
Export DeployDataFormHandle and DownloadDataFormHandle and use them in parent modals instead of useRef<any>.
There was a problem hiding this comment.
Code Review
This pull request introduces comprehensive mobile responsiveness and layout adaptations across multiple modules, including API Keys, Backends, Benchmark, GPU Service, LLModels, Playground, and Resources. It adds a mobile navigation drawer, responsive layout hooks, mobile-friendly filter forms, and sets up Vitest for unit testing. Additionally, it replaces the legacy page: -1 no-pagination sentinel with a large perPage limit in usage queries. The review feedback provides valuable, actionable suggestions to optimize performance and correctness: refactoring useCollapseLayout to store the handler in a ref, memoizing style objects passed to usePageContentStyle to prevent redundant renders, replacing the invalid destroyOnHidden prop with destroyOnClose in RightContainer, and using nullish coalescing (??) instead of logical OR (||) in api-keys/index.tsx to correctly handle a user ID of 0.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export default function useCollapseLayout(handler: () => void) { | ||
| const { isMobile } = useWindowResize(); | ||
| const wasMobileRef = useRef(isMobile); | ||
|
|
||
| useEffect(() => { | ||
| if (size.width < breakpoints.lg) { | ||
| if (!options.triggeredRef?.collapse) { | ||
| options.handler(); | ||
| } | ||
| if (isMobile && !wasMobileRef.current) { | ||
| handler(); | ||
| } | ||
| }, [size.width]); | ||
| wasMobileRef.current = isMobile; | ||
| }, [isMobile, handler]); | ||
| } |
There was a problem hiding this comment.
Функция handler передается в массив зависимостей useEffect. Поскольку в вызывающих компонентах (например, в embedding/index.tsx, rerank/index.tsx, video/index.tsx) обработчик передается как анонимная стрелочная функция, он пересоздается при каждом рендере. Это приводит к тому, что эффект внутри useCollapseLayout выполняется при каждом рендере компонента.\n\nРекомендуется сохранить handler в useRef, чтобы избежать лишних запусков эффекта.
| export default function useCollapseLayout(handler: () => void) { | |
| const { isMobile } = useWindowResize(); | |
| const wasMobileRef = useRef(isMobile); | |
| useEffect(() => { | |
| if (size.width < breakpoints.lg) { | |
| if (!options.triggeredRef?.collapse) { | |
| options.handler(); | |
| } | |
| if (isMobile && !wasMobileRef.current) { | |
| handler(); | |
| } | |
| }, [size.width]); | |
| wasMobileRef.current = isMobile; | |
| }, [isMobile, handler]); | |
| } | |
| export default function useCollapseLayout(handler: () => void) { | |
| const { isMobile } = useWindowResize(); | |
| const wasMobileRef = useRef(isMobile); | |
| const handlerRef = useRef(handler); | |
| handlerRef.current = handler; | |
| useEffect(() => { | |
| if (isMobile && !wasMobileRef.current) { | |
| handlerRef.current(); | |
| } | |
| wasMobileRef.current = isMobile; | |
| }, [isMobile]); | |
| } |
| usePageContentStyle( | ||
| isMobile | ||
| ? { | ||
| padding: 0, | ||
| overflow: 'hidden', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| flex: 1, | ||
| minHeight: 0 | ||
| } | ||
| : { padding: 0 } | ||
| ); |
There was a problem hiding this comment.
Объект стилей, передаваемый в usePageContentStyle, пересоздается при каждом рендере. Если внутри хука usePageContentStyle используется useEffect с этим объектом в качестве зависимости, это приведет к повторному выполнению эффекта при каждом рендере. Рекомендуется мемоизировать объект стилей с помощью useMemo.
const pageContentStyle = useMemo(
() =>
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 },
[isMobile]
);
usePageContentStyle(pageContentStyle);
| usePageContentStyle( | ||
| isMobile | ||
| ? { | ||
| padding: 0, | ||
| overflow: 'hidden', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| flex: 1, | ||
| minHeight: 0 | ||
| } | ||
| : { padding: 0 } | ||
| ); |
There was a problem hiding this comment.
Объект стилей, передаваемый в usePageContentStyle, пересоздается при каждом рендере. Если внутри хука usePageContentStyle используется useEffect с этим объектом в качестве зависимости, это приведет к повторному выполнению эффекта при каждом рендере. Рекомендуется мемоизировать объект стилей с помощью useMemo.
const pageContentStyle = useMemo(
() =>
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 },
[isMobile]
);
usePageContentStyle(pageContentStyle);
| usePageContentStyle( | ||
| isMobile | ||
| ? { | ||
| padding: 0, | ||
| overflow: 'hidden', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| flex: 1, | ||
| minHeight: 0 | ||
| } | ||
| : { padding: 0 } | ||
| ); |
There was a problem hiding this comment.
Объект стилей, передаваемый в usePageContentStyle, пересоздается при каждом рендере. Если внутри хука usePageContentStyle используется useEffect с этим объектом в качестве зависимости, это приведет к повторному выполнению эффекта при каждом рендере. Рекомендуется мемоизировать объект стилей с помощью useMemo.
const pageContentStyle = useMemo(
() =>
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 },
[isMobile]
);
usePageContentStyle(pageContentStyle);
| <GSDrawer | ||
| title={intl.formatMessage({ id: 'playground.parameters' })} | ||
| placement="right" | ||
| open={!collapsed} | ||
| onClose={onDismiss} | ||
| destroyOnHidden={false} |
There was a problem hiding this comment.
В компоненте Drawer библиотеки Ant Design (и, соответственно, в обертке GSDrawer) используется свойство destroyOnClose для уничтожения дочерних элементов при закрытии. Свойство destroyOnHidden отсутствует в API и будет проигнорировано. Рекомендуется заменить его на destroyOnClose={false}.
| <GSDrawer | |
| title={intl.formatMessage({ id: 'playground.parameters' })} | |
| placement="right" | |
| open={!collapsed} | |
| onClose={onDismiss} | |
| destroyOnHidden={false} | |
| <GSDrawer | |
| title={intl.formatMessage({ id: 'playground.parameters' })} | |
| placement="right" | |
| open={!collapsed} | |
| onClose={onDismiss} | |
| destroyOnClose={false} |
| const handleUserChange = (val: number) => { | ||
| handleQueryChange({ | ||
| page: 1, | ||
| user_id: val || '*' | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Использование оператора || для числового значения val может привести к нежелательному поведению, если val равен 0 (так как 0 является ложным значением в JavaScript и выражение вычислится в '*'). Безопаснее использовать оператор нулевого слияния ??.
| const handleUserChange = (val: number) => { | |
| handleQueryChange({ | |
| page: 1, | |
| user_id: val || '*' | |
| }); | |
| }; | |
| const handleUserChange = (val: number) => { | |
| handleQueryChange({ | |
| page: 1, | |
| user_id: val ?? '*' | |
| }); | |
| }; |
| onFilterChange: (filters) => { | ||
| handleQueryChange({ | ||
| page: 1, | ||
| user_id: filters.user_id || '*' | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Использование оператора || для числового значения filters.user_id может привести к нежелательному поведению, если идентификатор пользователя равен 0. Рекомендуется использовать оператор нулевого слияния ??.
| onFilterChange: (filters) => { | |
| handleQueryChange({ | |
| page: 1, | |
| user_id: filters.user_id || '*' | |
| }); | |
| }, | |
| onFilterChange: (filters) => { | |
| handleQueryChange({ | |
| page: 1, | |
| user_id: filters.user_id ?? '*' | |
| }); | |
| }, |





Summary
Adapts gpustack-ui for mobile and tablet viewports: mobile navigation, filter drawers instead of left panels, responsive tables/columns, and domain-specific layout fixes across playground, dashboard, LLModels, and benchmark pages.
Depends on: core-ui PR — requires
@gpustack/core-uinewer than 1.0.35. Do not merge until core-ui is merged and published.Also includes a small unrelated fix: usage pages no longer send the deprecated
page: -1pagination sentinel.What's included
Infrastructure
lint,typecheck,validatescripts + CI lint stepApp shell
PageBoxtitle barList pages
FilterBar+ sideFilterFormdrawerhideBelow,mobileTitle, flex sizing)AntdResponsiveTableon CRUD list pagesDomain-specific
PageHeaderTitle/ResponsiveSegmentedBugfix
USAGE_FULL_DATA_PAGINATIONinstead ofpage: -1Commit breakdown
chore(ci): add lint, typecheck, and validate pipelinechore(config): enable mobile viewport metarefactor(responsive): re-export core-ui layout helpersfeat(layout): add mobile navigation drawer and compact headerfeat(filters): migrate list pages to FilterBar drawer patternfeat(columns): add responsive column metadata for mobile cardsfeat(tables): adopt AntdResponsiveTable across list pagesfeat(ui): responsive domain-specific layoutsfix(usage): replace legacy page:-1 with explicit large-page fetchchore(deps): require @gpustack/core-ui newer than 1.0.35Notes for maintainers
@gpustack/core-uiis still^1.0.35inpackage.json; bump to the published core-ui version after that PR ships.1.0.35— this is expected until core-ui is released.Test plan
pnpm run validatepage: -1; export still works