Skip to content

Commit 3ff5409

Browse files
committed
fix(FR-2823): migrate Environment > Images table to server-side ordering
Resolves #7257(FR-2823) The Images tab on `/environment` paginates server-side but sorted client-side via per-column `sorter: (a, b) => ...` callbacks, which can only reorder rows on the current page — column header clicks gave a misleading partial sort. The backing `image_nodes` GraphQL query already exposes `$order: String`, but the page hard-coded `order: undefined` and never plumbed the table's sort state through. - Add URL-persisted `order` state via `nuqs` `useQueryStates` (default `-installed`, matching the prior `defaultSortOrder: descend` on the Status column). - Pass `order: queryParams.order` through to `image_nodes(...)`. - Replace each per-column in-memory comparator with `sorter: true` so BAITable emits sort changes via `onChangeOrder` instead of sorting locally. - Drop the sortable affordance on `FullImagePath` (computed via `getImageFullName`, not server-orderable). - Wire `order` and `onChangeOrder` to BAITable; resetting to page 1 on order change matches the listing-page convention. - Remove the now-unused `localeCompare` import. This follows the URL-state-driven server-order pattern already established in `react/src/pages/ProjectPage.tsx`. Verification: - Relay: PASS (no schema change) - Lint: PASS - Format: PASS - TypeScript: pre-existing failures only (unrelated)
1 parent 024fcd9 commit 3ff5409

1 file changed

Lines changed: 42 additions & 15 deletions

File tree

react/src/components/ImageList.tsx

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ImageListQuery$data,
88
ImageListQuery$variables,
99
} from '../__generated__/ImageListQuery.graphql';
10-
import { getImageFullName, localeCompare } from '../helper';
10+
import { getImageFullName } from '../helper';
1111
import { useBackendAIImageMetaData } from '../hooks';
1212
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
1313
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
@@ -38,6 +38,7 @@ import {
3838
INITIAL_FETCH_KEY,
3939
} from 'backend.ai-ui';
4040
import * as _ from 'lodash-es';
41+
import { parseAsStringLiteral, useQueryStates } from 'nuqs';
4142
import { Key, useDeferredValue, useState, useTransition } from 'react';
4243
import { useTranslation } from 'react-i18next';
4344
import { graphql, useLazyLoadQuery } from 'react-relay';
@@ -46,6 +47,19 @@ export type EnvironmentImage = NonNullableNodeOnEdges<
4647
ImageListQuery$data['image_nodes']
4748
>;
4849

50+
const availableImageSorterKeys = [
51+
'registry',
52+
'architecture',
53+
'namespace',
54+
'base_image_name',
55+
] as const;
56+
const availableImageSorterValues = [
57+
...availableImageSorterKeys,
58+
...availableImageSorterKeys.map((key) => `-${key}` as const),
59+
] as const;
60+
const isEnableSorter = (key: string) =>
61+
_.includes(availableImageSorterKeys, key);
62+
4963
const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
5064
'use memo';
5165

@@ -65,7 +79,7 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
6579
const [visibleColumnSettingModal, { toggle: toggleColumnSettingModal }] =
6680
useToggle();
6781
const [isPendingRefreshTransition, startRefreshTransition] = useTransition();
68-
const [isPendingFilterTransition, startFilterTransition] = useTransition();
82+
const [, startFilterTransition] = useTransition();
6983
const currentProject = useCurrentProjectValue();
7084

7185
const {
@@ -77,12 +91,19 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
7791
pageSize: 20,
7892
});
7993

94+
const [queryParams, setQueryParams] = useQueryStates(
95+
{
96+
order: parseAsStringLiteral(availableImageSorterValues),
97+
},
98+
{ history: 'replace' },
99+
);
100+
80101
const queryVariables: ImageListQuery$variables = {
81102
scopeId: `project:${currentProject.id}`,
82103
offset: baiPaginationOption.offset,
83104
first: baiPaginationOption.first,
84105
filter: imageFilter || undefined,
85-
order: undefined,
106+
order: queryParams.order || undefined,
86107
};
87108
const deferredQueryVariables = useDeferredValue(queryVariables);
88109
const deferredFetchKey = useDeferredValue(fetchKey);
@@ -157,10 +178,6 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
157178
title: t('environment.Status'),
158179
dataIndex: 'installed',
159180
key: 'installed',
160-
defaultSortOrder: 'descend',
161-
sorter: (a, b) => {
162-
return _.toNumber(a?.installed || 0) - _.toNumber(b?.installed || 0);
163-
},
164181
render: (_text, row) =>
165182
row?.id && installingImages.includes(row.id) ? (
166183
<Tag color="gold">{t('environment.Installing')}</Tag>
@@ -180,39 +197,38 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
180197
{getImageFullName(row) || ''}
181198
</Typography.Text>
182199
),
183-
sorter: (a, b) => localeCompare(getImageFullName(a), getImageFullName(b)),
200+
// Computed (`getImageFullName`) — not orderable on the server.
184201
width: token.screenXS,
185202
},
186203
{
187204
title: t('environment.Registry'),
188205
dataIndex: 'registry',
189206
key: 'registry',
190-
sorter: (a, b) => localeCompare(a?.registry, b?.registry),
207+
sorter: isEnableSorter('registry'),
191208
},
192209
{
193210
title: t('environment.Architecture'),
194211
dataIndex: 'architecture',
195212
key: 'architecture',
196-
sorter: (a, b) => localeCompare(a?.architecture, b?.architecture),
213+
sorter: isEnableSorter('architecture'),
197214
},
198215
{
199216
title: t('environment.Namespace'),
200217
key: 'namespace',
201218
dataIndex: 'namespace',
202-
sorter: (a, b) => localeCompare(a?.namespace, b?.namespace),
219+
sorter: isEnableSorter('namespace'),
203220
},
204221
{
205222
title: t('environment.BaseImageName'),
206223
key: 'base_image_name',
207224
dataIndex: 'base_image_name',
208-
sorter: (a, b) => localeCompare(a?.base_image_name, b?.base_image_name),
225+
sorter: isEnableSorter('base_image_name'),
209226
render: (text) => tagAlias(text),
210227
},
211228
{
212229
title: t('environment.Version'),
213230
key: 'version',
214231
dataIndex: 'version',
215-
sorter: (a, b) => localeCompare(a?.version, b?.version),
216232
},
217233
{
218234
title: t('environment.Tags'),
@@ -226,7 +242,6 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
226242
title: t('environment.Digest'),
227243
dataIndex: 'digest',
228244
key: 'digest',
229-
sorter: (a, b) => localeCompare(a?.digest || '', b?.digest || ''),
230245
render: (_text, row) => (
231246
<Typography.Text ellipsis={{ tooltip: true }} style={{ maxWidth: 200 }}>
232247
{row.digest}
@@ -458,7 +473,19 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
458473
columns,
459474
(column) => !_.includes(hiddenColumnKeys, _.toString(column?.key)),
460475
)}
461-
loading={isPendingFilterTransition}
476+
loading={
477+
deferredFetchKey !== fetchKey ||
478+
deferredQueryVariables !== queryVariables
479+
}
480+
order={queryParams.order}
481+
onChangeOrder={(order) => {
482+
setQueryParams({
483+
order: order as
484+
| (typeof availableImageSorterValues)[number]
485+
| null,
486+
});
487+
setTablePaginationOption({ current: 1 });
488+
}}
462489
rowSelection={{
463490
type: 'checkbox',
464491
onChange: (_, selectedRows) => {

0 commit comments

Comments
 (0)