From 2293007afc1aa50738520927ef0bb322ea9c18d7 Mon Sep 17 00:00:00 2001 From: Josef Date: Fri, 2 Aug 2024 18:17:51 +0200 Subject: [PATCH] feat: loading spinner and streaming for assets table (#1625) * feat: loading spinner for assets table * feat: streaming for assets table * feat: update mock in test --- .../dashboard/assets-table/index.tsx | 16 +++++++-- apps/minifront/src/fetchers/balances/index.ts | 12 +++---- apps/minifront/src/state/shared.ts | 35 +++++++++++++++++-- .../minifront/src/state/staking/index.test.ts | 21 +++++------ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/apps/minifront/src/components/dashboard/assets-table/index.tsx b/apps/minifront/src/components/dashboard/assets-table/index.tsx index b96e26d74f..0e2c0f70ef 100644 --- a/apps/minifront/src/components/dashboard/assets-table/index.tsx +++ b/apps/minifront/src/components/dashboard/assets-table/index.tsx @@ -20,6 +20,7 @@ import { BalancesByAccount, groupByAccount, useBalancesResponses } from '../../. import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types'; import { shouldDisplay } from '../../../fetchers/balances/should-display'; import { sortByPriorityScore } from '../../../fetchers/balances/by-priority-score'; +import { Oval } from 'react-loader-spinner'; const getTradeLink = (balance: BalancesResponse): string => { const metadata = getMetadataFromBalancesResponseOptional(balance); @@ -40,7 +41,18 @@ export default function AssetsTable() { shouldReselect: (before, after) => before?.data !== after.data, }); - if (balancesByAccount?.length === 0) { + /** Are assets still loading */ + const isLoading = balancesByAccount === undefined; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (balancesByAccount.length === 0) { return (

@@ -57,7 +69,7 @@ export default function AssetsTable() { return (

- {balancesByAccount?.map(account => ( + {balancesByAccount.map(account => ( diff --git a/apps/minifront/src/fetchers/balances/index.ts b/apps/minifront/src/fetchers/balances/index.ts index c35fbfd0ec..e1e1ffff9b 100644 --- a/apps/minifront/src/fetchers/balances/index.ts +++ b/apps/minifront/src/fetchers/balances/index.ts @@ -6,14 +6,15 @@ import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/a import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js'; import { viewClient } from '../../clients'; -interface BalancesProps { +export interface BalancesProps { accountFilter?: AddressIndex; assetIdFilter?: AssetId; } -export const getBalances = ({ accountFilter, assetIdFilter }: BalancesProps = {}): Promise< - BalancesResponse[] -> => { +export const getBalancesStream = ({ + accountFilter, + assetIdFilter, +}: BalancesProps = {}): AsyncIterable => { const req = new BalancesRequest(); if (accountFilter) { req.accountFilter = accountFilter; @@ -22,6 +23,5 @@ export const getBalances = ({ accountFilter, assetIdFilter }: BalancesProps = {} req.assetIdFilter = assetIdFilter; } - const iterable = viewClient.balances(req); - return Array.fromAsync(iterable); + return viewClient.balances(req); }; diff --git a/apps/minifront/src/state/shared.ts b/apps/minifront/src/state/shared.ts index 276e2403fd..5dd35959a6 100644 --- a/apps/minifront/src/state/shared.ts +++ b/apps/minifront/src/state/shared.ts @@ -2,12 +2,13 @@ import { ZQueryState, createZQuery } from '@penumbra-zone/zquery'; import { SliceCreator, useStore } from '.'; import { getStakingTokenMetadata } from '../fetchers/registry'; import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js'; -import { getBalances } from '../fetchers/balances'; +import { getBalancesStream } from '../fetchers/balances'; import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb.js'; import { getAllAssets } from '../fetchers/assets'; import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js'; import { getAddress, getAddressIndex } from '@penumbra-zone/getters/address-view'; import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types'; +import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; export const { stakingTokenMetadata, useStakingTokenMetadata } = createZQuery({ name: 'stakingTokenMetadata', @@ -22,9 +23,37 @@ export const { stakingTokenMetadata, useStakingTokenMetadata } = createZQuery({ }, }); +const getHash = (bal: BalancesResponse) => uint8ArrayToHex(bal.toBinary()); + export const { balancesResponses, useBalancesResponses } = createZQuery({ name: 'balancesResponses', - fetch: getBalances, + fetch: getBalancesStream, + stream: () => { + const balanceResponseIdsToKeep = new Set(); + + return { + onValue: ( + prevState: BalancesResponse[] | undefined = [], + balanceResponse: BalancesResponse, + ) => { + balanceResponseIdsToKeep.add(getHash(balanceResponse)); + + const existingIndex = prevState.findIndex(bal => getHash(bal) === getHash(balanceResponse)); + + // Update any existing items in place, rather than appending + // duplicates. + if (existingIndex >= 0) { + return prevState.toSpliced(existingIndex, 1, balanceResponse); + } else { + return [...prevState, balanceResponse]; + } + }, + + onEnd: (prevState = []) => + // Discard any balances from a previous stream. + prevState.filter(balanceResponse => balanceResponseIdsToKeep.has(getHash(balanceResponse))), + }; + }, getUseStore: () => useStore, get: state => state.shared.balancesResponses, set: setter => { @@ -50,7 +79,7 @@ export const { assets, useAssets } = createZQuery({ export interface SharedSlice { assets: ZQueryState; - balancesResponses: ZQueryState; + balancesResponses: ZQueryState>; stakingTokenMetadata: ZQueryState; } diff --git a/apps/minifront/src/state/staking/index.test.ts b/apps/minifront/src/state/staking/index.test.ts index d9fdb42325..7f1b98620a 100644 --- a/apps/minifront/src/state/staking/index.test.ts +++ b/apps/minifront/src/state/staking/index.test.ts @@ -72,9 +72,10 @@ vi.mock('../../fetchers/registry', async () => ({ })); vi.mock('../../fetchers/balances', () => ({ - getBalances: vi.fn(async () => - Promise.resolve([ - { + getBalancesStream: vi.fn(() => ({ + [Symbol.asyncIterator]: async function* () { + await new Promise(resolve => setTimeout(resolve, 0)); + yield { balanceView: new ValueView({ valueView: { case: 'knownAssetId', @@ -101,8 +102,8 @@ vi.mock('../../fetchers/balances', () => ({ }, }, }), - }, - { + }; + yield { balanceView: new ValueView({ valueView: { case: 'knownAssetId', @@ -129,8 +130,8 @@ vi.mock('../../fetchers/balances', () => ({ }, }, }), - }, - { + }; + yield { balanceView: new ValueView({ valueView: { case: 'knownAssetId', @@ -153,9 +154,9 @@ vi.mock('../../fetchers/balances', () => ({ }, }, }), - }, - ]), - ), + }; + }, + })), })); const mockViewClient = vi.hoisted(() => ({