Skip to content

feat: mobile-responsive UI for layouts, filters, tables, and playground#1272

Open
axsapronov wants to merge 13 commits into
gpustack:mainfrom
axsapronov:mobile-fixes-clean
Open

feat: mobile-responsive UI for layouts, filters, tables, and playground#1272
axsapronov wants to merge 13 commits into
gpustack:mainfrom
axsapronov:mobile-fixes-clean

Conversation

@axsapronov

@axsapronov axsapronov commented Jul 10, 2026

Copy link
Copy Markdown

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-ui newer 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: -1 pagination sentinel.

What's included

Infrastructure

  • lint, typecheck, validate scripts + CI lint step
  • Mobile viewport meta tag

App shell

  • Mobile nav drawer (sider hidden below 768px)
  • Compact header and shared PageBox title bar

List pages

  • Filter migration: legacy left filter panels → FilterBar + side FilterForm drawer
  • Responsive column metadata (hideBelow, mobileTitle, flex sizing)
  • AntdResponsiveTable on CRUD list pages

Domain-specific

  • Playground: params drawer, mobile styles, PageHeaderTitle / ResponsiveSegmented
  • LLModels: responsive deploy modal and online-model wizard
  • Dashboard: responsive charts and overview layout
  • Benchmark: responsive detail views, summary grids, compact actions

Bugfix

  • Usage tabs use USAGE_FULL_DATA_PAGINATION instead of page: -1

Commit breakdown

Commit Purpose
chore(ci): add lint, typecheck, and validate pipeline Local/CI validation
chore(config): enable mobile viewport meta Mobile viewport
refactor(responsive): re-export core-ui layout helpers Thin wrappers over core-ui
feat(layout): add mobile navigation drawer and compact header App shell
feat(filters): migrate list pages to FilterBar drawer pattern Filter drawer migration
feat(columns): add responsive column metadata for mobile cards Column metadata
feat(tables): adopt AntdResponsiveTable across list pages Responsive tables
feat(ui): responsive domain-specific layouts Playground, LLModels, dashboard, benchmark
fix(usage): replace legacy page:-1 with explicit large-page fetch Usage pagination fix
chore(deps): require @gpustack/core-ui newer than 1.0.35 Dependency gate (marker commit)

Notes for maintainers

  • @gpustack/core-ui is still ^1.0.35 in package.json; bump to the published core-ui version after that PR ships.
  • CI/build will not pass against published 1.0.35 — this is expected until core-ui is released.

Test plan

  • Blocked until core-ui PR is merged and published; then bump dependency and run pnpm run validate
  • Mobile (<768px): hamburger menu opens nav drawer; sider hidden; list pages show filter button + drawer
  • Mobile: CRUD tables render as cards with key fields visible
  • Tablet: tables scroll horizontally; toolbars switch to icon-only where expected
  • Playground: params panel opens in drawer on mobile; chat/images/speech layouts usable
  • LLModels: deploy modal and online-model flow usable on narrow viewport
  • Dashboard / Benchmark: charts and detail pages readable on mobile
  • Usage pages: data loads without page: -1; export still works
  • Desktop (≥1200px): no regressions vs current layout

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

Copy link
Copy Markdown
Author

Examples of mobile UI:

image image image

@axsapronov

Copy link
Copy Markdown
Author

Exampes of desktop UI:
image
image

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +4 to 14
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]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Функция handler передается в массив зависимостей useEffect. Поскольку в вызывающих компонентах (например, в embedding/index.tsx, rerank/index.tsx, video/index.tsx) обработчик передается как анонимная стрелочная функция, он пересоздается при каждом рендере. Это приводит к тому, что эффект внутри useCollapseLayout выполняется при каждом рендере компонента.\n\nРекомендуется сохранить handler в useRef, чтобы избежать лишних запусков эффекта.

Suggested change
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]);
}

Comment on lines +120 to +131
usePageContentStyle(
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Объект стилей, передаваемый в 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);

Comment on lines +170 to +181
usePageContentStyle(
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Объект стилей, передаваемый в 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);

Comment on lines +127 to +138
usePageContentStyle(
isMobile
? {
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0
}
: { padding: 0 }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Объект стилей, передаваемый в 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);

Comment on lines +31 to +36
<GSDrawer
title={intl.formatMessage({ id: 'playground.parameters' })}
placement="right"
open={!collapsed}
onClose={onDismiss}
destroyOnHidden={false}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

В компоненте Drawer библиотеки Ant Design (и, соответственно, в обертке GSDrawer) используется свойство destroyOnClose для уничтожения дочерних элементов при закрытии. Свойство destroyOnHidden отсутствует в API и будет проигнорировано. Рекомендуется заменить его на destroyOnClose={false}.

Suggested change
<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}

Comment on lines +205 to 210
const handleUserChange = (val: number) => {
handleQueryChange({
page: 1,
user_id: val || '*'
});
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Использование оператора || для числового значения val может привести к нежелательному поведению, если val равен 0 (так как 0 является ложным значением в JavaScript и выражение вычислится в '*'). Безопаснее использовать оператор нулевого слияния ??.

Suggested change
const handleUserChange = (val: number) => {
handleQueryChange({
page: 1,
user_id: val || '*'
});
};
const handleUserChange = (val: number) => {
handleQueryChange({
page: 1,
user_id: val ?? '*'
});
};

Comment on lines +85 to +90
onFilterChange: (filters) => {
handleQueryChange({
page: 1,
user_id: filters.user_id || '*'
});
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Использование оператора || для числового значения filters.user_id может привести к нежелательному поведению, если идентификатор пользователя равен 0. Рекомендуется использовать оператор нулевого слияния ??.

Suggested change
onFilterChange: (filters) => {
handleQueryChange({
page: 1,
user_id: filters.user_id || '*'
});
},
onFilterChange: (filters) => {
handleQueryChange({
page: 1,
user_id: filters.user_id ?? '*'
});
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant