Skip to content

Commit 1cd2a97

Browse files
committed
(WIP) Migrate off DataTable - re-implement as basic <table>
- TODO: internalize some Cell components; sort; search - FCP improved from > 7s to < 1s
1 parent eda5e7e commit 1cd2a97

1 file changed

Lines changed: 89 additions & 197 deletions

File tree

src/lib/top-vaults/TopVaultsTable.svelte

Lines changed: 89 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@
66
import CryptoAddressWidget from '$lib/components/CryptoAddressWidget.svelte';
77
import TextInput from '$lib/components/TextInput.svelte';
88
import Timestamp from '$lib/components/Timestamp.svelte';
9-
import DataTable from '$lib/components/datatable/DataTable.svelte';
109
import TopVaultsOptIn from './TopVaultsOptIn.svelte';
1110
import ChainCell from './ChainCell.svelte';
1211
import VaultCell from './VaultCell.svelte';
13-
import MultiValCell, { multiValCompareFn } from './MultiValCell.svelte';
12+
import MultiValCell from './MultiValCell.svelte';
1413
import FeesCell from './FeesCell.svelte';
1514
import DepositEventsCell from './DepositEventsCell.svelte';
1615
import RiskCell from './RiskCell.svelte';
17-
import { createTable } from 'svelte-headless-table';
18-
import { addSortBy, addHiddenColumns, addTableFilter } from 'svelte-headless-table/plugins';
19-
import { createRender } from '$lib/components/datatable/utils';
20-
import { readable } from 'svelte/store';
2116
import { formatDollar, formatNumber, formatPercent, formatValue } from '$lib/helpers/formatters';
2217
2318
interface Props {
@@ -30,195 +25,7 @@
3025
const formatReturn = (v: number | null) => formatPercent(v, 2);
3126
const formatTvl = (v: number | null) => formatDollar(v, 2);
3227
33-
const vaultsStore = readable(topVaults.vaults);
34-
35-
const table = createTable(vaultsStore, {
36-
hide: addHiddenColumns({ initialHiddenColumnIds: chain ? ['chain'] : [] }),
37-
sort: addSortBy({
38-
initialSortKeys: [{ id: 'one_month_return_ann', order: 'desc' }],
39-
toggleOrder: ['desc', 'asc']
40-
}),
41-
filter: addTableFilter({
42-
fn: ({ filterValue, value }) => value.toLocaleLowerCase().includes(filterValue.toLocaleLowerCase())
43-
})
44-
});
45-
46-
const tableColumns = table.createColumns([
47-
table.display({
48-
id: 'index',
49-
header: '',
50-
cell: () => '' // populated with row index via `rowNumber` CSS counter
51-
}),
52-
table.column({
53-
id: 'chain',
54-
header: '',
55-
accessor: ({ chain_id }) => {
56-
const chain = getChain(chain_id);
57-
const label = chain?.name ?? `Chain ${chain_id}`;
58-
return { chain_id, chain, label };
59-
},
60-
cell: ({ value }) => createRender(ChainCell, value),
61-
plugins: {
62-
sort: {
63-
getSortValue: ({ label }) => label,
64-
invert: true
65-
},
66-
filter: {
67-
getFilterValue: ({ chain_id, chain }) => `${chain_id} ${chain?.name}`
68-
}
69-
}
70-
}),
71-
table.column({
72-
id: 'vault',
73-
header: 'Vault',
74-
accessor: ({ name, protocol }) => ({ name, protocol }),
75-
cell: ({ value }) => createRender(VaultCell, value),
76-
plugins: {
77-
sort: {
78-
getSortValue: ({ name }) => name.trim().toLowerCase(),
79-
invert: true
80-
},
81-
filter: {
82-
getFilterValue: ({ name, protocol }) => `${name} ${protocol}`
83-
}
84-
}
85-
}),
86-
table.column({
87-
id: 'one_month_return_ann',
88-
accessor: (vault) => [vault.one_month_cagr_net, vault.one_month_cagr],
89-
header: '1M return ann.<br/>(net/&ZeroWidthSpace;gross)',
90-
cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }),
91-
plugins: {
92-
sort: { compareFn: multiValCompareFn },
93-
filter: { exclude: true }
94-
}
95-
}),
96-
table.column({
97-
id: 'three_months_return_ann',
98-
accessor: (vault) => [vault.three_months_cagr_net, vault.three_months_cagr],
99-
header: '3M return ann.<br/>(net/&ZeroWidthSpace;gross)',
100-
cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }),
101-
plugins: {
102-
sort: { compareFn: multiValCompareFn },
103-
filter: { exclude: true }
104-
}
105-
}),
106-
table.column({
107-
id: 'lifetime_return_ann',
108-
accessor: (vault) => [vault.cagr_net, vault.cagr],
109-
header: 'Lifetime return ann.<br/>(net/&ZeroWidthSpace;gross)',
110-
cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }),
111-
plugins: {
112-
sort: { compareFn: multiValCompareFn },
113-
filter: { exclude: true }
114-
}
115-
}),
116-
table.column({
117-
id: 'lifetime_return_abs',
118-
accessor: (vault) => [vault.lifetime_return_net, vault.lifetime_return],
119-
header: 'Lifetime return abs.<br/>(net/&ZeroWidthSpace;gross)',
120-
cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }),
121-
plugins: {
122-
sort: { compareFn: multiValCompareFn },
123-
filter: { exclude: true }
124-
}
125-
}),
126-
table.column({
127-
accessor: 'three_months_sharpe',
128-
header: '3M Sharpe',
129-
cell: ({ value }) => formatNumber(value, 1),
130-
plugins: { filter: { exclude: true } }
131-
}),
132-
table.column({
133-
accessor: 'three_months_volatility',
134-
header: '3M vola&shy;tility',
135-
cell: ({ value }) => formatPercent(value),
136-
plugins: { filter: { exclude: true } }
137-
}),
138-
table.column({
139-
accessor: 'denomination',
140-
header: 'Denom&shy;ination',
141-
cell: ({ value }) => formatValue(value),
142-
plugins: { sort: { invert: true } }
143-
}),
144-
table.column({
145-
id: 'tvl',
146-
accessor: (vault) => [vault.current_nav, vault.peak_nav],
147-
header: 'TVL USD<br/>(current/&ZeroWidthSpace;peak)',
148-
cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatTvl }),
149-
plugins: {
150-
sort: { compareFn: multiValCompareFn },
151-
filter: { exclude: true }
152-
}
153-
}),
154-
table.column({
155-
id: 'age',
156-
accessor: 'years',
157-
header: 'Age (years)',
158-
cell: ({ value }) => formatNumber(value, 1),
159-
plugins: { filter: { exclude: true } }
160-
}),
161-
// `last_deposit` is missing from latest data schema so dropping from table for now
162-
// table.column({
163-
// header: 'Last deposit',
164-
// accessor: 'last_deposit',
165-
// cell: ({ value }) => createRender(LastDepositCell, { last_deposit: value }),
166-
// plugins: { filter: { exclude: true } }
167-
// }),
168-
table.column({
169-
id: 'fees',
170-
header: 'Fees<br>(mgmt/&ZeroWidthSpace;perf)',
171-
accessor: ({ mgmt_fee, perf_fee }) => ({ mgmt_fee, perf_fee }),
172-
cell: ({ value }) => createRender(FeesCell, value),
173-
plugins: {
174-
sort: {
175-
// sort by perf_fee then mgmt_fee (scaled down as tie-break), nulls last
176-
getSortValue: ({ perf_fee, mgmt_fee }) => (perf_fee ?? 1) + (mgmt_fee ?? 1) / 100,
177-
invert: true
178-
},
179-
filter: { exclude: true }
180-
}
181-
}),
182-
table.column({
183-
accessor: 'event_count',
184-
header: 'Deposit events',
185-
cell: ({ value }) => createRender(DepositEventsCell, { value }),
186-
plugins: { filter: { exclude: true } }
187-
}),
188-
table.column({
189-
accessor: ({ risk, risk_numeric }) => ({ risk, risk_numeric }),
190-
header: 'Protocol technical risk',
191-
cell: ({ value }) => createRender(RiskCell, value),
192-
plugins: {
193-
sort: {
194-
getSortValue: (v) => v.risk_numeric ?? Infinity,
195-
invert: true
196-
}
197-
}
198-
}),
199-
table.column({
200-
id: 'address',
201-
header: 'Vault address',
202-
accessor: ({ address, chain_id }) => ({ address, chain_id }),
203-
cell: ({ value: { address, chain_id } }) =>
204-
createRender(CryptoAddressWidget, {
205-
class: 'vault-address',
206-
size: 'sm',
207-
address,
208-
href: getExplorerUrl(getChain(chain_id), address)
209-
}),
210-
plugins: {
211-
sort: { disable: true },
212-
filter: {
213-
getFilterValue: ({ address }) => address
214-
}
215-
}
216-
})
217-
]);
218-
219-
const tableViewModel = table.createViewModel(tableColumns);
220-
const { pluginStates } = tableViewModel;
221-
const filterValue = pluginStates.filter.filterValue;
28+
let filterValue = $state('');
22229
</script>
22330

22431
<div class="top-vaults">
@@ -232,11 +39,96 @@
23239
<span>{topVaults.vaults.length} {chain?.name ?? 'total'} vaults</span>
23340
<span>Updated <Timestamp date={topVaults.generated_at} relative /></span>
23441
</div>
235-
<TextInput bind:value={$filterValue} type="search" placeholder="Search vaults" />
42+
<TextInput bind:value={filterValue} type="search" placeholder="Search vaults" />
23643
</div>
23744

23845
<div class="table-wrapper">
239-
<DataTable class="top-vaults-table" {tableViewModel} />
46+
<table class="top-vaults-table">
47+
<thead>
48+
<tr>
49+
<th class="index"></th>
50+
<th class="chain"></th>
51+
<th class="vault">Vault</th>
52+
<th class="one_month_return_ann">1M return ann.<br />(net/&ZeroWidthSpace;gross)</th>
53+
<th class="three_months_return_ann">3M return ann.<br />(net/&ZeroWidthSpace;gross)</th>
54+
<th class="lifetime_return_ann">Lifetime return ann.<br />(net/&ZeroWidthSpace;gross)</th>
55+
<th class="lifetime_return_abs">Lifetime return abs.<br />(net/&ZeroWidthSpace;gross)</th>
56+
<th class="three_months_sharpe">3m Sharpe</th>
57+
<th class="three_months_volatility">3M Vola-tility</th>
58+
<th class="denomination">Denom-ination</th>
59+
<th class="tvl">TVL USD<br />(current/&ZeroWidthSpace;peak)</th>
60+
<th class="age">Age (Years)</th>
61+
<th class="fees">Fees<br />(mgmt/&ZeroWidthSpace;perf)</th>
62+
<th class="event_count">Deposit Events</th>
63+
<th class="risk">Protocol Technical Risk</th>
64+
<th class="address">Vault Address</th>
65+
</tr>
66+
</thead>
67+
<tbody>
68+
{#each topVaults.vaults as vault (vault.id)}
69+
{@const chain = getChain(vault.chain_id)}
70+
<tr>
71+
<!-- index cell is populated with row index via `rowNumber` CSS counter -->
72+
<td class="index"></td>
73+
<td class="chain">
74+
<ChainCell {chain} label={chain?.name ?? `Chain ${vault.chain_id}`} />
75+
</td>
76+
<td class="vault">
77+
<VaultCell name={vault.name} protocol={vault.protocol} />
78+
</td>
79+
<td class="one_month_return_ann">
80+
<MultiValCell values={[vault.one_month_cagr_net, vault.one_month_cagr]} formatter={formatReturn} />
81+
</td>
82+
<td class="three_months_return_ann">
83+
<MultiValCell
84+
values={[vault.three_months_cagr_net, vault.three_months_cagr]}
85+
formatter={formatReturn}
86+
/>
87+
</td>
88+
<td class="lifetime_return_ann">
89+
<MultiValCell values={[vault.cagr_net, vault.cagr]} formatter={formatReturn} />
90+
</td>
91+
<td class="lifetime_return_abs">
92+
<MultiValCell values={[vault.lifetime_return_net, vault.lifetime_return]} formatter={formatReturn} />
93+
</td>
94+
<td class="three_months_sharpe">
95+
{formatNumber(vault.three_months_sharpe, 1)}
96+
</td>
97+
<td class="three_months_volatility">
98+
{formatPercent(vault.three_months_volatility, 1)}
99+
</td>
100+
<td class="denomination">
101+
{formatValue(vault.denomination)}
102+
</td>
103+
<td class="tvl">
104+
<MultiValCell values={[vault.current_nav, vault.peak_nav]} formatter={formatTvl} />
105+
</td>
106+
<td class="age">
107+
{formatNumber(vault.years, 1)}
108+
</td>
109+
<td class="fees">
110+
<FeesCell mgmt_fee={vault.mgmt_fee} perf_fee={vault.perf_fee} />
111+
</td>
112+
<td class="event_count">
113+
<DepositEventsCell value={vault.event_count} />
114+
</td>
115+
<td class="risk">
116+
<RiskCell risk={vault.risk} risk_numeric={vault.risk_numeric} />
117+
</td>
118+
<td class="address">
119+
<CryptoAddressWidget
120+
class="vault-address"
121+
size="sm"
122+
address={vault.address}
123+
href={getExplorerUrl(chain, vault.address)}
124+
/>
125+
</td>
126+
</tr>
127+
{/each}
128+
</tbody>
129+
</table>
130+
131+
<!-- <DataTable class="top-vaults-table" {tableViewModel} /> -->
240132
</div>
241133
{/if}
242134
</div>

0 commit comments

Comments
 (0)