Skip to content

Commit 8b719df

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 aca1f24 commit 8b719df

1 file changed

Lines changed: 43 additions & 19 deletions

File tree

react/src/components/ImageList.tsx

Lines changed: 43 additions & 19 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,6 @@ 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();
6982
const currentProject = useCurrentProjectValue();
7083

7184
const {
@@ -77,12 +90,19 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
7790
pageSize: 20,
7891
});
7992

93+
const [queryParams, setQueryParams] = useQueryStates(
94+
{
95+
order: parseAsStringLiteral(availableImageSorterValues),
96+
},
97+
{ history: 'replace' },
98+
);
99+
80100
const queryVariables: ImageListQuery$variables = {
81101
scopeId: `project:${currentProject.id}`,
82102
offset: baiPaginationOption.offset,
83103
first: baiPaginationOption.first,
84104
filter: imageFilter || undefined,
85-
order: undefined,
105+
order: queryParams.order || undefined,
86106
};
87107
const deferredQueryVariables = useDeferredValue(queryVariables);
88108
const deferredFetchKey = useDeferredValue(fetchKey);
@@ -157,10 +177,6 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
157177
title: t('environment.Status'),
158178
dataIndex: 'installed',
159179
key: 'installed',
160-
defaultSortOrder: 'descend',
161-
sorter: (a, b) => {
162-
return _.toNumber(a?.installed || 0) - _.toNumber(b?.installed || 0);
163-
},
164180
render: (_text, row) =>
165181
row?.id && installingImages.includes(row.id) ? (
166182
<Tag color="gold">{t('environment.Installing')}</Tag>
@@ -180,39 +196,38 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
180196
{getImageFullName(row) || ''}
181197
</Typography.Text>
182198
),
183-
sorter: (a, b) => localeCompare(getImageFullName(a), getImageFullName(b)),
199+
// Computed (`getImageFullName`) — not orderable on the server.
184200
width: token.screenXS,
185201
},
186202
{
187203
title: t('environment.Registry'),
188204
dataIndex: 'registry',
189205
key: 'registry',
190-
sorter: (a, b) => localeCompare(a?.registry, b?.registry),
206+
sorter: isEnableSorter('registry'),
191207
},
192208
{
193209
title: t('environment.Architecture'),
194210
dataIndex: 'architecture',
195211
key: 'architecture',
196-
sorter: (a, b) => localeCompare(a?.architecture, b?.architecture),
212+
sorter: isEnableSorter('architecture'),
197213
},
198214
{
199215
title: t('environment.Namespace'),
200216
key: 'namespace',
201217
dataIndex: 'namespace',
202-
sorter: (a, b) => localeCompare(a?.namespace, b?.namespace),
218+
sorter: isEnableSorter('namespace'),
203219
},
204220
{
205221
title: t('environment.BaseImageName'),
206222
key: 'base_image_name',
207223
dataIndex: 'base_image_name',
208-
sorter: (a, b) => localeCompare(a?.base_image_name, b?.base_image_name),
224+
sorter: isEnableSorter('base_image_name'),
209225
render: (text) => tagAlias(text),
210226
},
211227
{
212228
title: t('environment.Version'),
213229
key: 'version',
214230
dataIndex: 'version',
215-
sorter: (a, b) => localeCompare(a?.version, b?.version),
216231
},
217232
{
218233
title: t('environment.Tags'),
@@ -226,7 +241,6 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
226241
title: t('environment.Digest'),
227242
dataIndex: 'digest',
228243
key: 'digest',
229-
sorter: (a, b) => localeCompare(a?.digest || '', b?.digest || ''),
230244
render: (_text, row) => (
231245
<Typography.Text ellipsis={{ tooltip: true }} style={{ maxWidth: 200 }}>
232246
{row.digest}
@@ -390,10 +404,8 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
390404
])}
391405
value={imageFilter || undefined}
392406
onChange={(value) => {
393-
startFilterTransition(() => {
394-
setImageFilter(value || '');
395-
setTablePaginationOption({ current: 1 });
396-
});
407+
setImageFilter(value || '');
408+
setTablePaginationOption({ current: 1 });
397409
}}
398410
/>
399411
<BAIFlex gap={'xs'}>
@@ -458,7 +470,19 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
458470
columns,
459471
(column) => !_.includes(hiddenColumnKeys, _.toString(column?.key)),
460472
)}
461-
loading={isPendingFilterTransition}
473+
loading={
474+
deferredFetchKey !== fetchKey ||
475+
deferredQueryVariables !== queryVariables
476+
}
477+
order={queryParams.order}
478+
onChangeOrder={(order) => {
479+
setQueryParams({
480+
order: order as
481+
| (typeof availableImageSorterValues)[number]
482+
| null,
483+
});
484+
setTablePaginationOption({ current: 1 });
485+
}}
462486
rowSelection={{
463487
type: 'checkbox',
464488
onChange: (_, selectedRows) => {

0 commit comments

Comments
 (0)