Skip to content

Commit

Permalink
feat(FR-527): introduce BAIFetchKeyButton (#3169)
Browse files Browse the repository at this point in the history
resolves #3174 (FR-567)

Adds auto-refresh functionality to the compute session list page with a dedicated refresh button that shows loading state and supports automatic updates every 15 seconds. The table now displays a loading state with reduced opacity during updates and shows the total number of items.

Key changes:
- New `BAIFetchKeyButton` component for manual/automatic data refresh
- Enhanced table loading states with smooth opacity transitions
- Added "Last Updated" translations across all languages
- Improved badge visibility for running sessions
- Fixed state updates in `useDeferredQueryParams` hook
  • Loading branch information
yomybaby committed Feb 17, 2025
1 parent ba1b285 commit af19cee
Show file tree
Hide file tree
Showing 25 changed files with 203 additions and 54 deletions.
100 changes: 100 additions & 0 deletions react/src/components/BAIFetchKeyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import useControllableState from '../hooks/useControllableState';
import { useInterval, useIntervalValue } from '../hooks/useIntervalValue';
import { ReloadOutlined } from '@ant-design/icons';
import { Button, ButtonProps, Tooltip } from 'antd';
import dayjs from 'dayjs';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

interface BAIAutoRefetchButtonProps {
value: string;
loading?: boolean;
lastLoadTime?: Date;
showLastLoadTime?: boolean;
autoUpdateDelay?: number | null;
size?: ButtonProps['size'];
onChange: (fetchKey: string) => void;
hidden?: boolean;
}
const BAIFetchKeyButton: React.FC<BAIAutoRefetchButtonProps> = ({
value,
loading,
onChange,
showLastLoadTime,
autoUpdateDelay = null,
size,
hidden,
...props
}) => {
const { t } = useTranslation();
const [lastLoadTime, setLastLoadTime] = useControllableState(
{
value: props.lastLoadTime,
},
{
defaultValue: new Date(),
},
);

// display loading icon for at least "some ms" to avoid flickering
const [displayLoading, setDisplayLoading] = useState(false);
useEffect(() => {
if (loading) {
const startTime = Date.now();
setDisplayLoading(true);

return () => {
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(700 - elapsedTime, 0);

setTimeout(() => {
setDisplayLoading(false);
}, remainingTime);
};
}
}, [loading]);

const loadTimeMessage = useIntervalValue(
() => {
if (lastLoadTime) {
return `${t('general.LastUpdated')}: ${dayjs(lastLoadTime).fromNow()}`;
}
return '';
},
showLastLoadTime ? 5_000 : null,
lastLoadTime.toISOString(),
);

// remember when loading is done to display when the last fetch was done
useLayoutEffect(() => {
if (!loading) {
setLastLoadTime(new Date());
}
}, [loading, setLastLoadTime]);

useInterval(
() => {
onChange(new Date().toISOString());
},
// only start auto-updating after the previous loading is false(done).
loading ? null : autoUpdateDelay,
);

return hidden ? null : (
<Tooltip
title={showLastLoadTime ? loadTimeMessage : undefined}
placement="topLeft"
>
<Button
loading={displayLoading}
size={size}
icon={<ReloadOutlined />}
onClick={() => {
onChange(new Date().toISOString());
}}
/>
</Tooltip>
);
};

export default BAIFetchKeyButton;
5 changes: 5 additions & 0 deletions react/src/components/BAITable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const BAITable = <RecordType extends object = any>({
columns,
components,
neoStyle,
loading,
...tableProps
}: BAITableProps<RecordType>) => {
const { styles } = useStyles();
Expand Down Expand Up @@ -174,6 +175,10 @@ const BAITable = <RecordType extends object = any>({
resizable && styles.resizableTable,
neoStyle && styles.neoHeader,
)}
style={{
opacity: loading ? 0.7 : 1,
transition: 'opacity 0.3s ease',
}}
components={
resizable
? _.merge(components || {}, {
Expand Down
2 changes: 1 addition & 1 deletion react/src/hooks/useDeferredQueryParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function useDeferredQueryParams<QPCMap extends QueryParamConfigMap>(

// Update Jotai state
if (updateType === 'replaceIn' || updateType === 'pushIn') {
setLocalQuery((prev) => ({ ...prev, ...newQuery }));
setLocalQuery({ ...localQuery, ...newQuery });
} else {
setLocalQuery(newQuery as DecodedValueMap<QPCMap>);
}
Expand Down
86 changes: 54 additions & 32 deletions react/src/pages/ComputeSessionListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BAIFetchKeyButton from '../components/BAIFetchKeyButton';
import BAILink from '../components/BAILink';
import BAIPropertyFilter, {
mergeFilterValues,
Expand All @@ -14,18 +15,16 @@ import { useUpdatableState } from '../hooks';
import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions';
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
import { useDeferredQueryParams } from '../hooks/useDeferredQueryParams';
import { useInterval } from '../hooks/useIntervalValue';
import {
ComputeSessionListPageQuery,
ComputeSessionListPageQuery$data,
ComputeSessionListPageQuery$variables,
} from './__generated__/ComputeSessionListPageQuery.graphql';
import { LoadingOutlined } from '@ant-design/icons';
import { Badge, Button, Card, Radio, Tabs, theme, Tooltip } from 'antd';
import { Badge, Button, Card, Radio, Tabs, theme, Tooltip, Typography } from 'antd';
import graphql from 'babel-plugin-relay/macro';
import _ from 'lodash';
import { PowerOffIcon } from 'lucide-react';
import { startTransition, useDeferredValue, useRef, useState } from 'react';
import { useDeferredValue, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyLoadQuery } from 'react-relay';
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
Expand Down Expand Up @@ -84,16 +83,28 @@ const ComputeSessionListPage = () => {

const [fetchKey, updateFetchKey] = useUpdatableState('first');

const queryVariables: ComputeSessionListPageQuery$variables = {
projectId: currentProject.id,
offset: baiPaginationOption.offset,
first: baiPaginationOption.first,
filter: mergeFilterValues([statusFilter, queryParams.filter, typeFilter]),
order: queryParams.order,
runningTypeFilter: 'status != "TERMINATED" & status != "CANCELLED"',
};
const queryVariables: ComputeSessionListPageQuery$variables = useMemo(
() => ({
projectId: currentProject.id,
offset: baiPaginationOption.offset,
first: baiPaginationOption.first,
filter: mergeFilterValues([statusFilter, queryParams.filter, typeFilter]),
order: queryParams.order,
runningTypeFilter: 'status != "TERMINATED" & status != "CANCELLED"',
}),
[
currentProject.id,
baiPaginationOption.offset,
baiPaginationOption.first,
statusFilter,
queryParams.filter,
typeFilter,
queryParams.order,
],
);

const deferredQueryVariables = useDeferredValue(queryVariables);
const deferredFetchKey = useDeferredValue(fetchKey);

const { compute_session_nodes, allRunningSessionForCount } =
useLazyLoadQuery<ComputeSessionListPageQuery>(
Expand Down Expand Up @@ -135,28 +146,36 @@ const ComputeSessionListPage = () => {
deferredQueryVariables,
{
fetchPolicy: 'network-only',
fetchKey,
fetchKey: deferredFetchKey,
},
);

useInterval(() => {
startTransition(() => {
updateFetchKey();
});
}, 15_000);

return (
<>
{/* TODO: add legacy opener */}
{/* <SessionDetailAndContainerLogOpenerForLegacy /> */}
<Card
bordered={false}
title={t('webui.menu.Sessions')}
extra={[
<BAILink to={'/session/start'} key={'start-session'}>
<Button type="primary">{t('session.launcher.Start')}</Button>
</BAILink>,
]}
extra={
<Flex gap={'xs'}>
<BAIFetchKeyButton
loading={
deferredQueryVariables !== queryVariables ||
deferredFetchKey !== fetchKey
}
autoUpdateDelay={15_000}
// showLastLoadTime
value={fetchKey}
onChange={(newFetchKey) => {
updateFetchKey(newFetchKey);
}}
/>
<BAILink to={'/session/start'}>
<Button type="primary">{t('session.launcher.Start')}</Button>
</BAILink>
</Flex>
}
styles={{
header: {
borderBottom: 'none',
Expand Down Expand Up @@ -197,7 +216,11 @@ const ComputeSessionListPage = () => {
{key === 'all' && (
<Badge
count={allRunningSessionForCount?.count}
color={token.colorPrimary}
color={
queryParams.type === key
? token.colorPrimary
: token.colorTextDisabled
}
size="small"
showZero
style={{
Expand Down Expand Up @@ -283,6 +306,7 @@ const ComputeSessionListPage = () => {
onClickSessionName={(session) => {
setSessionDetailId(session.row_id);
}}
loading={deferredQueryVariables !== queryVariables}
rowSelection={{
type: 'checkbox',
// Preserve selected rows between pages, but clear when filter changes
Expand Down Expand Up @@ -311,13 +335,11 @@ const ComputeSessionListPage = () => {
pageSize: tablePaginationOption.pageSize,
current: tablePaginationOption.current,
total: compute_session_nodes?.count ?? 0,
// showTotal: (total) => {
// return total;
// },
}}
loading={{
spinning: queryVariables !== deferredQueryVariables,
indicator: <LoadingOutlined />,
showTotal: (total) => (
<Typography.Text type="secondary">
{t('general.TotalItems', { total: total })}
</Typography.Text>
),
}}
onChange={({ current, pageSize }, filters, sorter) => {
if (_.isNumber(current) && _.isNumber(pageSize)) {
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@
"TotalItems": "Insgesamt {{Gesamt}} Artikel",
"ExtendLoginSession": "Eine Anmeldesitzung verlängern",
"Cores": "Kerne",
"InvalidJSONFormat": "Ungültiges JSON-Format."
"InvalidJSONFormat": "Ungültiges JSON-Format.",
"LastUpdated": "Zuletzt aktualisiert"
},
"credential": {
"Permission": "Genehmigung",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@
"TotalItems": "Σύνολο {{total}} στοιχείων",
"ExtendLoginSession": "Επέκταση μιας περιόδου σύνδεσης",
"Cores": "πυρήνες",
"InvalidJSONFormat": "Μη έγκυρη μορφή JSON."
"InvalidJSONFormat": "Μη έγκυρη μορφή JSON.",
"LastUpdated": "Τελευταία ενημέρωση"
},
"credential": {
"Permission": "Αδεια",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,8 @@
"TotalItems": "Total {{total}} items",
"ExtendLoginSession": "Extend login session",
"Cores": "cores",
"InvalidJSONFormat": "Invalid JSON format."
"InvalidJSONFormat": "Invalid JSON format.",
"LastUpdated": "Last Updated"
},
"credential": {
"Permission": "Permission",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,8 @@
"TotalItems": "Total {{total}} artículos",
"ExtendLoginSession": "Prolongar una sesión de inicio de sesión",
"Cores": "núcleos",
"InvalidJSONFormat": "Formato JSON no válido."
"InvalidJSONFormat": "Formato JSON no válido.",
"LastUpdated": "Última actualización"
},
"import": {
"CleanUpImportTask": "Tarea de importación de limpieza...",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,8 @@
"TotalItems": "Yhteensä {{total}} kohteita",
"ExtendLoginSession": "Sisäänkirjautumisistunnon laajentaminen",
"Cores": "ytimet",
"InvalidJSONFormat": "Väärä JSON-muoto."
"InvalidJSONFormat": "Väärä JSON-muoto.",
"LastUpdated": "Viimeksi päivitetty"
},
"import": {
"CleanUpImportTask": "Tuontitehtävän siivous...",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@
"TotalItems": "Total des éléments {{total}}",
"ExtendLoginSession": "Prolonger une session de connexion",
"Cores": "cœurs",
"InvalidJSONFormat": "Format JSON non valide."
"InvalidJSONFormat": "Format JSON non valide.",
"LastUpdated": "Dernière mise à jour"
},
"credential": {
"Permission": "Autorisation",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,8 @@
"TotalItems": "Total item {{total}}",
"ExtendLoginSession": "Memperpanjang sesi masuk",
"Cores": "core",
"InvalidJSONFormat": "Format JSON tidak valid."
"InvalidJSONFormat": "Format JSON tidak valid.",
"LastUpdated": "Terakhir diperbarui"
},
"credential": {
"Permission": "Izin",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@
"TotalItems": "Totale articoli {{totale}}",
"ExtendLoginSession": "Estendere una sessione di login",
"Cores": "core",
"InvalidJSONFormat": "Formato JSON non valido."
"InvalidJSONFormat": "Formato JSON non valido.",
"LastUpdated": "Ultimo aggiornamento"
},
"credential": {
"Permission": "Autorizzazione",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@
"TotalItems": "合計{{total}}項目",
"ExtendLoginSession": "ログインセッションの延長",
"Cores": "コア",
"InvalidJSONFormat": "JSON形式が間違っています。"
"InvalidJSONFormat": "JSON形式が間違っています。",
"LastUpdated": "最終更新日"
},
"credential": {
"Permission": "許可",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,8 @@
"ExtendLoginSession": "로그인 세션 연장",
"Cores": "코어",
"Enable": "활성화",
"InvalidJSONFormat": "잘못된 JSON 형식입니다."
"InvalidJSONFormat": "잘못된 JSON 형식입니다.",
"LastUpdated": "최근 업데이트"
},
"credential": {
"Permission": "권한",
Expand Down
3 changes: 2 additions & 1 deletion resources/i18n/mn.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,8 @@
"TotalItems": "Нийт {{total}} зүйл",
"ExtendLoginSession": "Нэвтрэх сессийг сунгах",
"Cores": "цөм",
"InvalidJSONFormat": "Буруу JSON формат."
"InvalidJSONFormat": "Буруу JSON формат.",
"LastUpdated": "Өнөөхээр нь онуулсан өдөр"
},
"credential": {
"Permission": "Зөвшөөрөл",
Expand Down
Loading

0 comments on commit af19cee

Please sign in to comment.