chore(trading): handle timeout and offline errors (#2918)

This commit is contained in:
Maciek 2023-02-16 17:30:32 +01:00 committed by GitHub
parent 9dfed0a211
commit b2a115f935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 136 additions and 50 deletions

View File

@ -6,7 +6,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
export const DepositsContainer = () => { export const DepositsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error } = useDataProvider({ const { data, loading, error, reload } = useDataProvider({
dataProvider: depositsProvider, dataProvider: depositsProvider,
variables: { partyId: pubKey || '' }, variables: { partyId: pubKey || '' },
skip: !pubKey, skip: !pubKey,
@ -27,6 +27,7 @@ export const DepositsContainer = () => {
error={error} error={error}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No deposits')} noDataMessage={t('No deposits')}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container';
export const WithdrawalsContainer = () => { export const WithdrawalsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error } = useDataProvider({ const { data, loading, error, reload } = useDataProvider({
dataProvider: withdrawalProvider, dataProvider: withdrawalProvider,
variables: { partyId: pubKey || '' }, variables: { partyId: pubKey || '' },
skip: !pubKey, skip: !pubKey,
@ -33,6 +33,7 @@ export const WithdrawalsContainer = () => {
error={error} error={error}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No withdrawals')} noDataMessage={t('No withdrawals')}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -24,7 +24,10 @@ export const AccountManager = ({
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
const { data, loading, error } = useDataProvider<AccountFields[], never>({ const { data, loading, error, reload } = useDataProvider<
AccountFields[],
never
>({
dataProvider: aggregatedAccountsDataProvider, dataProvider: aggregatedAccountsDataProvider,
variables, variables,
}); });
@ -32,7 +35,7 @@ export const AccountManager = ({
<div className="relative h-full"> <div className="relative h-full">
<AccountTable <AccountTable
ref={gridRef} ref={gridRef}
rowData={data} rowData={error ? [] : data}
onClickAsset={onClickAsset} onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit} onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw} onClickWithdraw={onClickWithdraw}
@ -46,6 +49,7 @@ export const AccountManager = ({
error={error} error={error}
loading={loading} loading={loading}
noDataMessage={t('No accounts')} noDataMessage={t('No accounts')}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -25,6 +25,7 @@ export const DealTicketContainer = ({
data: marketData, data: marketData,
error: marketDataError, error: marketDataError,
loading: marketDataLoading, loading: marketDataLoading,
reload,
} = useThrottledDataProvider( } = useThrottledDataProvider(
{ {
dataProvider: marketDataProvider, dataProvider: marketDataProvider,
@ -39,6 +40,7 @@ export const DealTicketContainer = ({
data={market && marketData} data={market && marketData}
loading={marketLoading || marketDataLoading} loading={marketLoading || marketDataLoading}
error={marketError || marketDataError} error={marketError || marketDataError}
reload={reload}
> >
{market && marketData ? ( {market && marketData ? (
<DealTicket <DealTicket

View File

@ -19,7 +19,7 @@ export const FillsManager = ({
}: FillsManagerProps) => { }: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true); const scrolledToTop = useRef(true);
const { data, error, loading, addNewRows, getRows } = useFillsList({ const { data, error, loading, addNewRows, getRows, reload } = useFillsList({
partyId, partyId,
marketId, marketId,
gridRef, gridRef,
@ -55,6 +55,7 @@ export const FillsManager = ({
data={data} data={data}
noDataMessage={t('No fills')} noDataMessage={t('No fills')}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -78,7 +78,7 @@ export const useFillsList = ({
const variables = useMemo(() => ({ partyId, marketId }), [partyId, marketId]); const variables = useMemo(() => ({ partyId, marketId }), [partyId, marketId]);
const { data, error, loading, load, totalCount } = useDataProvider< const { data, error, loading, load, totalCount, reload } = useDataProvider<
(TradeEdge | null)[], (TradeEdge | null)[],
Trade[] Trade[]
>({ >({
@ -95,5 +95,5 @@ export const useFillsList = ({
load, load,
newRows newRows
); );
return { data, error, loading, addNewRows, getRows }; return { data, error, loading, addNewRows, getRows, reload };
}; };

View File

@ -29,14 +29,19 @@ export const ProposalsList = () => {
proposalType: Types.ProposalType.TYPE_NEW_MARKET, proposalType: Types.ProposalType.TYPE_NEW_MARKET,
}; };
}, []); }, []);
const { data, loading, error } = useDataProvider({ const { data, loading, error, reload } = useDataProvider({
dataProvider: proposalsListDataProvider, dataProvider: proposalsListDataProvider,
variables, variables,
}); });
const filteredData = getNewMarketProposals(data || []); const filteredData = getNewMarketProposals(data || []);
const { columnDefs, defaultColDef } = useColumnDefs(); const { columnDefs, defaultColDef } = useColumnDefs();
return ( return (
<AsyncRenderer loading={loading} error={error} data={filteredData}> <AsyncRenderer
loading={loading}
error={error}
data={filteredData}
reload={reload}
>
<AgGrid <AgGrid
ref={gridRef} ref={gridRef}
domLayout="autoHeight" domLayout="autoHeight"

View File

@ -35,6 +35,7 @@ query LedgerEntries(
node { node {
...LedgerEntry ...LedgerEntry
} }
cursor
} }
pageInfo { pageInfo {
startCursor startCursor

View File

@ -14,7 +14,7 @@ export type LedgerEntriesQueryVariables = Types.Exact<{
}>; }>;
export type LedgerEntriesQuery = { __typename?: 'Query', ledgerEntries: { __typename?: 'AggregatedLedgerEntriesConnection', edges: Array<{ __typename?: 'AggregatedLedgerEntriesEdge', node: { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string } } | null>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } }; export type LedgerEntriesQuery = { __typename?: 'Query', ledgerEntries: { __typename?: 'AggregatedLedgerEntriesConnection', edges: Array<{ __typename?: 'AggregatedLedgerEntriesEdge', cursor: string, node: { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string } } | null>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } };
export const LedgerEntryFragmentDoc = gql` export const LedgerEntryFragmentDoc = gql`
fragment LedgerEntry on AggregatedLedgerEntry { fragment LedgerEntry on AggregatedLedgerEntry {
@ -43,6 +43,7 @@ export const LedgerEntriesDocument = gql`
node { node {
...LedgerEntry ...LedgerEntry
} }
cursor
} }
pageInfo { pageInfo {
startCursor startCursor

View File

@ -128,7 +128,11 @@ export const ledgerEntriesProvider = makeDerivedDataProvider<
const marketReceiver = markets.find( const marketReceiver = markets.find(
(market: Market) => market.id === entry.toAccountMarketId (market: Market) => market.id === entry.toAccountMarketId
); );
return { node: { ...entry, asset, marketSender, marketReceiver } }; const cursor = edge?.cursor;
return {
node: { ...entry, asset, marketSender, marketReceiver },
cursor,
};
}); });
} }
); );
@ -172,14 +176,13 @@ export const useLedgerEntriesDataProvider = ({
data: (AggregatedLedgerEntriesEdge | null)[] | null; data: (AggregatedLedgerEntriesEdge | null)[] | null;
totalCount?: number; totalCount?: number;
}) => { }) => {
dataRef.current = data;
totalCountRef.current = totalCount; totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef); return updateGridData(dataRef, data, gridRef);
}, },
[gridRef] [gridRef]
); );
const { data, error, loading, load, totalCount } = useDataProvider({ const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ledgerEntriesProvider, dataProvider: ledgerEntriesProvider,
update, update,
insert, insert,
@ -193,5 +196,5 @@ export const useLedgerEntriesDataProvider = ({
totalCountRef, totalCountRef,
load load
); );
return { loading, error, data, getRows }; return { loading, error, data, getRows, reload };
}; };

View File

@ -16,6 +16,7 @@ export const ledgerEntriesQuery = (
edges: ledgerEntries.map((node) => ({ edges: ledgerEntries.map((node) => ({
__typename: 'AggregatedLedgerEntriesEdge', __typename: 'AggregatedLedgerEntriesEdge',
node, node,
cursor: 'cursor-1',
})), })),
pageInfo: { pageInfo: {
startCursor: startCursor:

View File

@ -21,11 +21,12 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter | undefined>(); const [filter, setFilter] = useState<Filter | undefined>();
const { data, error, loading, getRows } = useLedgerEntriesDataProvider({ const { data, error, loading, getRows, reload } =
partyId, useLedgerEntriesDataProvider({
filter, partyId,
gridRef, filter,
}); gridRef,
});
const onFilterChanged = useCallback( const onFilterChanged = useCallback(
(event: FilterChangedEvent) => { (event: FilterChangedEvent) => {
@ -38,7 +39,11 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
}, },
[filter] [filter]
); );
const getRowId = useCallback(
({ data }: { data: Types.AggregatedLedgerEntry }) =>
`${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`,
[]
);
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LedgerTable <LedgerTable
@ -46,6 +51,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
rowModelType="infinite" rowModelType="infinite"
datasource={{ getRows }} datasource={{ getRows }}
onFilterChanged={onFilterChanged} onFilterChanged={onFilterChanged}
getRowId={getRowId}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<AsyncRenderer <AsyncRenderer
@ -54,6 +60,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
data={data} data={data}
noDataMessage={t('No entries')} noDataMessage={t('No entries')}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -85,7 +85,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
[marketId, updateOrderbookData] [marketId, updateOrderbookData]
); );
const { data, error, loading, flush } = useDataProvider({ const { data, error, loading, flush, reload } = useDataProvider({
dataProvider: marketDepthProvider, dataProvider: marketDepthProvider,
update, update,
variables, variables,
@ -156,6 +156,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
loading={loading || marketDataLoading || marketLoading} loading={loading || marketDataLoading || marketLoading}
error={error || marketDataError || marketError} error={error || marketDataError || marketError}
data={data} data={data}
reload={reload}
> >
<Orderbook <Orderbook
{...orderbookData} {...orderbookData}

View File

@ -69,14 +69,14 @@ export const MarketInfoContainer = ({
[marketId, yTimestamp] [marketId, yTimestamp]
); );
const { data, loading, error } = useDataProvider({ const { data, loading, error, reload } = useDataProvider({
dataProvider: marketInfoDataProvider, dataProvider: marketInfoDataProvider,
skipUpdates: true, skipUpdates: true,
variables, variables,
}); });
return ( return (
<AsyncRenderer data={data} loading={loading} error={error}> <AsyncRenderer data={data} loading={loading} error={error} reload={reload}>
{data && data.market ? ( {data && data.market ? (
<Info market={data.market} onSelect={(id) => onSelect?.(id)} /> <Info market={data.market} onSelect={(id) => onSelect?.(id)} />
) : ( ) : (

View File

@ -10,7 +10,7 @@ interface MarketsContainerProps {
} }
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
const { data, error, loading } = useDataProvider({ const { data, error, loading, reload } = useDataProvider({
dataProvider, dataProvider,
skipUpdates: true, skipUpdates: true,
}); });
@ -18,7 +18,8 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<MarketListTable <MarketListTable
rowData={data} rowData={error ? [] : data}
noRowsOverlayComponent={() => null}
onRowClicked={(rowEvent: RowClickedEvent) => { onRowClicked={(rowEvent: RowClickedEvent) => {
const { data, event } = rowEvent; const { data, event } = rowEvent;
// filters out clicks on the symbol column because it should display asset details // filters out clicks on the symbol column because it should display asset details
@ -36,6 +37,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
error={error} error={error}
data={data} data={data}
noDataMessage={t('No markets')} noDataMessage={t('No markets')}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -155,6 +155,7 @@ export const ordersProvider = makeDataProvider({
append, append,
first: 100, first: 100,
}, },
additionalContext: { isEnlargedTimeout: true },
}); });
export const ordersWithMarketProvider = makeDerivedDataProvider< export const ordersWithMarketProvider = makeDerivedDataProvider<

View File

@ -83,14 +83,15 @@ export const OrderListManager = ({
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId); const hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows } = useOrderListData({ const { data, error, loading, addNewRows, getRows, reload } =
partyId, useOrderListData({
marketId, partyId,
sort, marketId,
filter, sort,
gridRef, filter,
scrolledToTop, gridRef,
}); scrolledToTop,
});
const onBodyScrollEnd = (event: BodyScrollEndEvent) => { const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {
@ -128,7 +129,7 @@ export const OrderListManager = ({
return ( return (
<> <>
<div className="h-full relative grid grid-rows-[1fr,min-content]"> <div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full relative"> <div className="relative">
<OrderListTable <OrderListTable
ref={gridRef} ref={gridRef}
rowModelType="infinite" rowModelType="infinite"
@ -149,6 +150,8 @@ export const OrderListManager = ({
setEditOrder={setEditOrder} setEditOrder={setEditOrder}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
hasActiveOrder={hasActiveOrder}
blockLoadDebounceMillis={100}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<AsyncRenderer <AsyncRenderer
@ -157,6 +160,7 @@ export const OrderListManager = ({
data={data} data={data}
noDataMessage={t('No orders')} noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -119,7 +119,7 @@ export const useOrderListData = ({
[gridRef] [gridRef]
); );
const { data, error, loading, load, totalCount } = useDataProvider({ const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider, dataProvider: ordersWithMarketProvider,
update, update,
insert, insert,
@ -133,5 +133,5 @@ export const useOrderListData = ({
load, load,
newRows newRows
); );
return { loading, error, data, addNewRows, getRows }; return { loading, error, data, addNewRows, getRows, reload };
}; };

View File

@ -33,20 +33,24 @@ export type OrderListTableProps = OrderListProps & {
setEditOrder: (order: Order) => void; setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string) => void; onMarketClick?: (marketId: string) => void;
isReadOnly: boolean; isReadOnly: boolean;
hasActiveOrder?: boolean;
}; };
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>( export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => { ({ cancel, setEditOrder, onMarketClick, hasActiveOrder, ...props }, ref) => {
return ( return (
<AgGrid <AgGrid
ref={ref} ref={ref}
overlayNoRowsTemplate="No orders" overlayNoRowsTemplate={t('No orders')}
defaultColDef={{ defaultColDef={{
flex: 1, flex: 1,
resizable: true, resizable: true,
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
}} }}
style={{ width: '100%', height: '100%' }} style={{
width: '100%',
height: hasActiveOrder ? 'calc(100% - 46px)' : '100%',
}}
getRowId={({ data }) => data.id} getRowId={({ data }) => data.id}
{...props} {...props}
> >

View File

@ -18,7 +18,11 @@ export const PositionsManager = ({
isReadOnly, isReadOnly,
}: PositionsManagerProps) => { }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading } = usePositionsData(partyId, gridRef, true); const { data, error, loading, reload } = usePositionsData(
partyId,
gridRef,
true
);
const create = useVegaTransactionStore((store) => store.create); const create = useVegaTransactionStore((store) => store.create);
const onClose = ({ const onClose = ({
marketId, marketId,
@ -51,7 +55,7 @@ export const PositionsManager = ({
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<PositionsTable <PositionsTable
rowData={data} rowData={error ? [] : data}
ref={gridRef} ref={gridRef}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onClose={onClose} onClose={onClose}
@ -65,6 +69,7 @@ export const PositionsManager = ({
data={data} data={data}
noDataMessage={t('No positions')} noDataMessage={t('No positions')}
noDataCondition={(data) => !(data && data.length)} noDataCondition={(data) => !(data && data.length)}
reload={reload}
/> />
</div> </div>
</div> </div>

View File

@ -25,7 +25,7 @@ export const usePositionsData = (
}, },
[gridRef, clientSideModel] [gridRef, clientSideModel]
); );
const { data, error, loading } = useDataProvider({ const { data, error, loading, reload } = useDataProvider({
dataProvider: positionsMetricsProvider, dataProvider: positionsMetricsProvider,
update, update,
variables, variables,
@ -45,5 +45,6 @@ export const usePositionsData = (
error, error,
loading, loading,
getRows, getRows,
reload,
}; };
}; };

View File

@ -80,6 +80,7 @@ export const useDataProvider = <
} }
}, []); }, []);
const reload = useCallback((force = false) => { const reload = useCallback((force = false) => {
initialized.current = false;
if (reloadRef.current) { if (reloadRef.current) {
reloadRef.current(force); reloadRef.current(force);
} }

View File

@ -17,7 +17,7 @@ const getLastRow = (
lastRow = totalCount; lastRow = totalCount;
} }
} else if (blockLength < endRow - startRow) { } else if (blockLength < endRow - startRow) {
lastRow = blockLength; lastRow = blockLength + startRow;
} }
return lastRow; return lastRow;
}; };

View File

@ -82,7 +82,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
[] []
); );
const { data, error, loading, load, totalCount } = useDataProvider({ const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: tradesWithMarketProvider, dataProvider: tradesWithMarketProvider,
update, update,
insert, insert,
@ -107,7 +107,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
}; };
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={loading} error={error} data={data} reload={reload}>
<TradesTable <TradesTable
ref={gridRef} ref={gridRef}
rowModelType={data?.length ? 'infinite' : 'clientSide'} rowModelType={data?.length ? 'infinite' : 'clientSide'}

View File

@ -0,0 +1,23 @@
import { render, act, screen } from '@testing-library/react';
import { AsyncRenderer } from './async-renderer';
describe('AsyncRenderer', () => {
it('timeout error should render button', async () => {
const reload = jest.fn();
await act(() => {
render(
<AsyncRenderer
reload={reload}
error={new Error('Timeout exceeded')}
loading={false}
data={undefined}
/>
);
});
expect(screen.getByRole('button')).toBeInTheDocument();
await act(() => {
screen.getByRole('button').click();
});
expect(reload).toHaveBeenCalled();
});
});

View File

@ -1,6 +1,7 @@
import { Splash } from '../splash'; import { Splash } from '../splash';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { Button } from '../button';
interface AsyncRendererProps<T> { interface AsyncRendererProps<T> {
loading: boolean; loading: boolean;
@ -12,6 +13,7 @@ interface AsyncRendererProps<T> {
children?: ReactNode | null; children?: ReactNode | null;
render?: (data: T) => ReactNode; render?: (data: T) => ReactNode;
noDataCondition?(data?: T): boolean; noDataCondition?(data?: T): boolean;
reload?: () => void;
} }
export function AsyncRenderer<T = object>({ export function AsyncRenderer<T = object>({
@ -24,15 +26,30 @@ export function AsyncRenderer<T = object>({
noDataCondition, noDataCondition,
children, children,
render, render,
reload,
}: AsyncRendererProps<T>) { }: AsyncRendererProps<T>) {
if (error) { if (error) {
if (!data) { if (!data) {
return ( return (
<Splash> <div className="h-full flex items-center justify-center">
{errorMessage <div className="h-12 flex flex-col items-center">
? errorMessage <Splash>
: t(`Something went wrong: ${error.message}`)} {errorMessage
</Splash> ? errorMessage
: t(`Something went wrong: ${error.message}`)}
</Splash>
{reload && error.message === 'Timeout exceeded' && (
<Button
size="sm"
className="pointer-events-auto"
type="button"
onClick={reload}
>
{t('Try again')}
</Button>
)}
</div>
</div>
); );
} }
} }