gloomberb 0.18.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
|
@@ -242,6 +242,7 @@ export function relationshipTemplateSymbols(
|
|
|
242
242
|
try {
|
|
243
243
|
return normalizeRelationshipSymbols(parseTickerListInput(raw));
|
|
244
244
|
} catch {
|
|
245
|
+
// Intentional ignore: invalid template input disables creation through canCreate and creates no pane.
|
|
245
246
|
return null;
|
|
246
247
|
}
|
|
247
248
|
}
|
|
@@ -34,6 +34,7 @@ function coerceStoredSymbols(value: unknown): string[] {
|
|
|
34
34
|
try {
|
|
35
35
|
return parseCorrelationSymbolsInput(text, MAX_CORRELATION_TICKERS);
|
|
36
36
|
} catch {
|
|
37
|
+
// Intentional ignore: malformed legacy symbols fall back to saved text or the default preset.
|
|
37
38
|
return [];
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -82,8 +82,8 @@ function CotBoard({ width, height, focused, family, initialCode }: PaneProps & {
|
|
|
82
82
|
}, [data, query, scope, sort]);
|
|
83
83
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
84
84
|
// The class, scope and search controls live in the board's query bar, so
|
|
85
|
-
// their keys
|
|
86
|
-
const boardShown =
|
|
85
|
+
// their keys work while it is on screen: over the board and while a class loads.
|
|
86
|
+
const boardShown = !open;
|
|
87
87
|
useShortcut((event) => {
|
|
88
88
|
if (!focused || open || event.targetEditable || event.ctrl || event.meta || event.alt || event.super) return;
|
|
89
89
|
if (event.name === "r") { event.preventDefault(); void resource.reload(); }
|
|
@@ -99,8 +99,9 @@ function CotBoard({ width, height, focused, family, initialCode }: PaneProps & {
|
|
|
99
99
|
usePaneNoticeFooter({ registrationId: "cot:board-notices", focused, enabled: !open, notices: data?.gaps ?? [] });
|
|
100
100
|
usePaneStatusFooter({ registrationId: "cot:board", enabled: !open, loading: resource.loading, error: resource.error,
|
|
101
101
|
info: data ? [{ id: "as-of", parts: [{ text: `CFTC futures only · ${data.asOf ?? "--"} · contracts`, tone: "muted" }] }] : [] });
|
|
102
|
-
|
|
102
|
+
// A class change reloads under the query bar, which stays.
|
|
103
103
|
return <DataTableStackView<CotBoardRow> columns={BOARD_COLUMNS} items={rows} focused={focused && !searching}
|
|
104
|
+
status={data ? undefined : { loading: resource.loading, error: resource.error, empty: !resource.loading && !resource.error, subject: "COT history" }}
|
|
104
105
|
rootWidth={width} rootHeight={height} selection={{ kind: "id", selectedId: selected, getId: (row) => row.contractCode, onChange: setSelected }}
|
|
105
106
|
getItemKey={(row) => row.contractCode} onActivate={(row) => setOpen(row.contractCode)} freezeFirstColumn
|
|
106
107
|
sortable sortColumnId={sort.columnId} sortDirection={sort.direction} onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id, { firstDirection: "desc" }))}
|
|
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
|
|
|
2
2
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
3
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
4
|
import type { CreditDocumentsPayload, CreditHeadroom, CreditInstrument, CreditScreenPayload, CreditScreenRow } from "../../../api-client/credit-documents";
|
|
5
|
-
import { ChartTableHeader, DataTableView, EmptyState, PageStackView,
|
|
5
|
+
import { ChartTableHeader, DataTableView, EmptyState, PageStackView, QueryBar, useChartTableSelection, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint } from "../../../components";
|
|
6
6
|
import { formatCompactAxis } from "../../../components/chart-table";
|
|
7
7
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
8
8
|
import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
@@ -170,18 +170,19 @@ function CreditView({ symbol, width, height, focused, accessKey, needsVerificati
|
|
|
170
170
|
return { text, value, color: column.id === columns[0]!.id ? colors.textBright : colors.text };
|
|
171
171
|
};
|
|
172
172
|
return <Box width={width} height={height} flexDirection="column">{strip}
|
|
173
|
-
<
|
|
174
|
-
empty={!available && currentError === CREDIT_UNAVAILABLE} emptyTitle={CREDIT_UNAVAILABLE} subject="credit documents">
|
|
175
|
-
{available ? <PageStackView focused={focused && !openFactId} detailOpen={!!openInstrument} onBack={() => setOpen(null)} detailTitle={openInstrument?.name}
|
|
173
|
+
<PageStackView focused={focused && !openFactId} detailOpen={!!openInstrument} onBack={() => setOpen(null)} detailTitle={openInstrument?.name}
|
|
176
174
|
detailContent={openInstrument && activeSymbol ? <CreditInstrumentDetail key={`${activeSymbol}:${openInstrument.id}`} symbol={activeSymbol} instrument={openInstrument} width={width} height={bodyHeight - 1} focused={focused} accessKey={accessKey} view={view} setView={setView} covenant={activeCovenant} openId={openFactId} setOpen={setOpenFact} captured={instrumentSnapshot?.id === openInstrument.id ? instrumentSnapshot : null} pro={data?.access === "full"} onUpgrade={openUpgrade} /> : null}
|
|
177
|
-
rootContent={<DataTableView<Row> columns={columns} items={
|
|
175
|
+
rootContent={<DataTableView<Row> columns={columns} items={available ? items : []}
|
|
176
|
+
// The Screen tab and its thresholds load under the query bar, which stays.
|
|
177
|
+
status={available ? undefined : { loading, error: currentError !== CREDIT_UNAVAILABLE ? currentError : null,
|
|
178
|
+
empty: currentError === CREDIT_UNAVAILABLE, emptyTitle: CREDIT_UNAVAILABLE, subject: "credit documents" }}
|
|
179
|
+
sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((current) => ({column,direction:current.column === column && current.direction === "asc" ? "desc" : "asc"}))} focused={focused && !openInstrument} rootWidth={width} rootHeight={bodyHeight} rootBefore={rootBefore}
|
|
178
180
|
getItemKey={rowId} selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelected }}
|
|
179
181
|
onActivate={(row) => row.locked ? openUpgrade() : setOpen(row.screen ? row.id : row.instrumentId ?? null)}
|
|
180
182
|
renderCell={renderCell}
|
|
181
183
|
emptyStateTitle={tab === "screen" ? "No supported credit signals match these thresholds." : tab === "covenants" ? "No financial maintenance covenants documented." : tab === "maturities" ? "No supported instrument maturity amounts." : CREDIT_UNAVAILABLE}
|
|
182
184
|
emptyStateHint={tab === "screen" && (headroom < 100 || months < 36) ? "Raise either threshold to widen the screen." : undefined}
|
|
183
185
|
selectedTextOverridesCellColor showHorizontalScrollbar freezeFirstColumn resetScrollKey={`${symbol}:${tab}:${selectedCurrency}`}
|
|
184
|
-
bodyAfter={locked > 0 && desktop ? <LockedOverlay rows={Math.min(3, locked)} text={lockedText} onPress={openUpgrade} /> : undefined} />} />
|
|
185
|
-
</PaneStatusBody>
|
|
186
|
+
bodyAfter={locked > 0 && desktop ? <LockedOverlay rows={Math.min(3, locked)} text={lockedText} onPress={openUpgrade} /> : undefined} />} />
|
|
186
187
|
</Box>;
|
|
187
188
|
}
|
|
@@ -410,8 +410,9 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
410
410
|
const detailOpen =
|
|
411
411
|
tab === "maturities" ? !!openBucketRow : tab === "history" && !!openHistoryRow;
|
|
412
412
|
const tabsFocused = focused && !detailOpen;
|
|
413
|
+
const signInWall = !data && isCloudSessionRequired(resource.error);
|
|
413
414
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs(
|
|
414
|
-
|
|
415
|
+
symbol && !signInWall
|
|
415
416
|
? { tabs: TABS, activeValue: tab, onSelect: setTab, focused: tabsFocused, dense: true }
|
|
416
417
|
: null,
|
|
417
418
|
);
|
|
@@ -522,7 +523,7 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
522
523
|
message="Select a ticker to view debt maturities."
|
|
523
524
|
/>
|
|
524
525
|
);
|
|
525
|
-
if (
|
|
526
|
+
if (signInWall)
|
|
526
527
|
return (
|
|
527
528
|
<SignInWall
|
|
528
529
|
placement="debt-maturities-signin"
|
|
@@ -532,6 +533,7 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
532
533
|
);
|
|
533
534
|
return (
|
|
534
535
|
<Box width={width} height={height} flexDirection="column">
|
|
536
|
+
{tabStrip}
|
|
535
537
|
<PaneStatusBody
|
|
536
538
|
loading={resource.loading && !data}
|
|
537
539
|
error={!data ? resource.error : null}
|
|
@@ -542,7 +544,6 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
542
544
|
>
|
|
543
545
|
{latest ? (
|
|
544
546
|
<>
|
|
545
|
-
{tabStrip}
|
|
546
547
|
{tab === "filing" ? (
|
|
547
548
|
<FilingDetail latest={latest} quarter={quarter} width={width} height={bodyHeight} />
|
|
548
549
|
) : tab === "maturities" ? (
|
|
@@ -129,7 +129,7 @@ export function DoePane({ width, height, focused }: PaneProps) {
|
|
|
129
129
|
const data = resource.data?.payload ?? null;
|
|
130
130
|
// The server not serving the board yet is a state of the data, not a failure.
|
|
131
131
|
const notAvailable = !data && resource.error === DOE_NOT_AVAILABLE;
|
|
132
|
-
const { strip, rows: tabRows } = usePaneTabs(
|
|
132
|
+
const { strip, rows: tabRows } = usePaneTabs({ tabs: [...DOE_TABS], activeValue: tab, onSelect: setTab, focused, dense: true });
|
|
133
133
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
134
134
|
usePaneRefreshKey(() => void resource.reload(), { focused });
|
|
135
135
|
usePaneNoticeFooter({ registrationId: "doe:notices", focused,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
parseScreenDefinition,
|
|
22
22
|
screenColumnLabel,
|
|
23
23
|
screenDefinitionSyntax,
|
|
24
|
+
screenCurrencyEmpty,
|
|
24
25
|
screenLabel,
|
|
25
26
|
screenYtdBase,
|
|
26
27
|
withheldMetricNote,
|
|
@@ -141,6 +142,14 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
141
142
|
const kpiGaps = !withheld && missing > 0 && (KPI_FIELDS as readonly string[]).includes(metric)
|
|
142
143
|
? [`${missing} of ${rows.length} ${rows.length === 1 ? "row has" : "rows have"} no ${screenLabel(metric)}. ${KPI_COVERAGE_NOTE}`]
|
|
143
144
|
: [];
|
|
145
|
+
// A CAD screen of listings quoted in USD is empty for its currency, not for want of companies: say so, and give the screen in USD.
|
|
146
|
+
const currencyEmpty = rows.length === 0 ? screenCurrencyEmpty(data.definition, data.universe) : null;
|
|
147
|
+
const empty = currencyEmpty
|
|
148
|
+
? [
|
|
149
|
+
`${currencyEmpty.title} ${currencyEmpty.detail}`,
|
|
150
|
+
`In the base currency, ${currencyEmpty.base.currency}: gloomberb fn EQS --definition '${spacedJson(currencyEmpty.base)}'${metric === "marketCap" ? "" : ` --metric ${metric}`}`,
|
|
151
|
+
].join("\n")
|
|
152
|
+
: null;
|
|
144
153
|
// Payload status also counts ordinary per-field sparsity; only generation
|
|
145
154
|
// warnings are failures, and more pages are not an error.
|
|
146
155
|
return {
|
|
@@ -151,6 +160,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
151
160
|
sections: [
|
|
152
161
|
{
|
|
153
162
|
title: "Matching equities",
|
|
163
|
+
...(empty ? { empty } : {}),
|
|
154
164
|
columns: [
|
|
155
165
|
{ key: "symbol", header: "Symbol" },
|
|
156
166
|
{ key: "exchange", header: "Exchange" },
|
|
@@ -192,6 +192,34 @@ export const DEFAULT_SCREEN: ScreenDefinition = {
|
|
|
192
192
|
criteria: [{ field: "marketCap", op: "gte", value: 10_000_000_000 }],
|
|
193
193
|
sort: { field: "marketCap", direction: "desc" },
|
|
194
194
|
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Why a screen with a currency found nothing, when the currency may be why:
|
|
198
|
+
* `currency` keeps the listings quoted in it and converts nothing, so a CAD
|
|
199
|
+
* screen of listings quoted in USD reads as no company at all. `title` says
|
|
200
|
+
* which listings it kept, `detail` that it converts nothing and what the
|
|
201
|
+
* covered listings are quoted in, and `base` is the same screen in the base
|
|
202
|
+
* currency the default screen uses (USD), to run instead. Null for a screen
|
|
203
|
+
* in that currency or with none.
|
|
204
|
+
*/
|
|
205
|
+
export function screenCurrencyEmpty(
|
|
206
|
+
definition: ScreenDefinition,
|
|
207
|
+
universe: Pick<ScreenPayload["universe"], "covered" | "currencies">,
|
|
208
|
+
): { title: string; detail: string; base: ScreenDefinition } | null {
|
|
209
|
+
const currency = definition.currency;
|
|
210
|
+
const baseCurrency = DEFAULT_SCREEN.currency!;
|
|
211
|
+
if (!currency || currency === baseCurrency) return null;
|
|
212
|
+
const quoted = universe.currencies.filter(Boolean);
|
|
213
|
+
const none = quoted.length > 0 && !quoted.includes(currency);
|
|
214
|
+
return {
|
|
215
|
+
title: none
|
|
216
|
+
? `None of the ${universe.covered.toLocaleString("en-US")} covered listings is quoted in ${currency}.`
|
|
217
|
+
: `No listing quoted in ${currency} matches.`,
|
|
218
|
+
detail: `Currency ${currency} keeps only the listings quoted in ${currency} and converts nothing${none ? `; the covered listings are quoted in ${quoted.join(", ")}` : ""}.`,
|
|
219
|
+
base: { ...definition, currency: baseCurrency },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
195
223
|
export const screenRowId = (row: Pick<ScreenRow, "symbol" | "exchange">) =>
|
|
196
224
|
`${canonicalExchange(row.exchange)}:${row.symbol}`;
|
|
197
225
|
export const screenLabel = (field: string) =>
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
resultFields,
|
|
70
70
|
SIGN_COLORED,
|
|
71
71
|
screenColumnLabel,
|
|
72
|
+
screenCurrencyEmpty,
|
|
72
73
|
screenLabel,
|
|
73
74
|
screenRowId,
|
|
74
75
|
screenYtdBase,
|
|
@@ -354,6 +355,8 @@ function EquityScreenView({
|
|
|
354
355
|
const extendedSessions = screenExtendedSessions(rows);
|
|
355
356
|
// YTD names the close it is measured from, `31 Dec`.
|
|
356
357
|
const ytdBase = screenYtdBase(snapshotRows, data?.snapshot?.assembledAt);
|
|
358
|
+
// A screen its currency emptied says so, not that no company matches.
|
|
359
|
+
const currencyEmpty = data && data.rows.length === 0 ? screenCurrencyEmpty(data.definition, data.universe) : null;
|
|
357
360
|
const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase);
|
|
358
361
|
const apply = (next: ScreenDefinition) => {
|
|
359
362
|
try {
|
|
@@ -889,11 +892,8 @@ function EquityScreenView({
|
|
|
889
892
|
</PaneStatusBody>
|
|
890
893
|
)
|
|
891
894
|
) : (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
error={!data ? results.error : null}
|
|
895
|
-
subject="equity screen"
|
|
896
|
-
>
|
|
895
|
+
// The metric stays above the results while a new sort loads.
|
|
896
|
+
<>
|
|
897
897
|
{!opened ? (
|
|
898
898
|
<QueryBar
|
|
899
899
|
width={width}
|
|
@@ -918,101 +918,108 @@ function EquityScreenView({
|
|
|
918
918
|
}]}
|
|
919
919
|
/>
|
|
920
920
|
) : null}
|
|
921
|
-
<
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
return move == null
|
|
957
|
-
? { text: "--", color: colors.textDim }
|
|
958
|
-
: { text: formatPercentRaw(move), color: priceColor(move, colors) };
|
|
921
|
+
<PaneStatusBody
|
|
922
|
+
loading={results.loading && !data}
|
|
923
|
+
error={!data ? results.error : null}
|
|
924
|
+
subject="equity screen"
|
|
925
|
+
>
|
|
926
|
+
<DataTableStackView
|
|
927
|
+
columns={columns}
|
|
928
|
+
items={rows}
|
|
929
|
+
focused={focused && !saveForm}
|
|
930
|
+
rootWidth={width}
|
|
931
|
+
rootHeight={bodyHeight - 1}
|
|
932
|
+
scrollRef={tableScroll}
|
|
933
|
+
onBodyScrollActivity={loadMore}
|
|
934
|
+
resetScrollKey={JSON.stringify(definition)}
|
|
935
|
+
visibleRangeKey={JSON.stringify(definition)}
|
|
936
|
+
onVisibleRangeChange={setVisibleRange}
|
|
937
|
+
getItemKey={screenRowId}
|
|
938
|
+
selection={{
|
|
939
|
+
kind: "id",
|
|
940
|
+
selectedId: selectedId ?? (rows[0] ? screenRowId(rows[0]) : null),
|
|
941
|
+
getId: screenRowId,
|
|
942
|
+
onChange: setSelectedId,
|
|
943
|
+
}}
|
|
944
|
+
onActivate={(row) => setOpenId(screenRowId(row))}
|
|
945
|
+
detailOpen={!!opened}
|
|
946
|
+
onBack={() => setOpenId(null)}
|
|
947
|
+
detailTitle={opened?.name ?? opened?.symbol}
|
|
948
|
+
detailContent={
|
|
949
|
+
opened ? (
|
|
950
|
+
<ScreenDetail
|
|
951
|
+
row={opened}
|
|
952
|
+
width={width}
|
|
953
|
+
height={bodyHeight - 2}
|
|
954
|
+
/>
|
|
955
|
+
) : null
|
|
959
956
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
957
|
+
renderCell={(row, column) => {
|
|
958
|
+
if (column.id.startsWith(EXTENDED_COLUMN_PREFIX)) {
|
|
959
|
+
// The extended print's move from the close in PRICE and CHG%.
|
|
960
|
+
const move = screenExtendedMove(row, column.id.slice(EXTENDED_COLUMN_PREFIX.length) as ExtendedSession);
|
|
961
|
+
return move == null
|
|
962
|
+
? { text: "--", color: colors.textDim }
|
|
963
|
+
: { text: formatPercentRaw(move), color: priceColor(move, colors) };
|
|
964
|
+
}
|
|
965
|
+
const field = column.id.startsWith("m:")
|
|
966
|
+
? (column.id.slice(2) as NumericField)
|
|
967
|
+
: null;
|
|
968
|
+
if (field) {
|
|
969
|
+
const observation = row.metrics[field];
|
|
970
|
+
const change = SIGN_COLORED.has(field) && observation.value != null
|
|
971
|
+
// A move that prints 0.0 stays neutral.
|
|
972
|
+
? priceColor(Math.abs(observation.value) < 0.05 ? 0 : observation.value, colors)
|
|
973
|
+
: undefined;
|
|
974
|
+
return {
|
|
975
|
+
text: formatScreenValue(field, observation.value),
|
|
976
|
+
color: observation.state === "stale" ? colors.warning : change,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const focus = row.metrics[metric];
|
|
980
|
+
if (column.id === "percentile")
|
|
981
|
+
return { text: rank(focus.percentile.value) };
|
|
982
|
+
if (column.id === "date")
|
|
983
|
+
return focus.state === "stale"
|
|
984
|
+
? { text: metricDate(focus).text, color: colors.warning }
|
|
985
|
+
: { text: "" };
|
|
986
|
+
if (column.id === "name") return { text: row.name ?? "--" };
|
|
987
|
+
return { text: String(row[column.id as "symbol"] ?? "--") };
|
|
988
|
+
}}
|
|
989
|
+
sortColumnId={
|
|
990
|
+
(NUMERIC_FIELDS as readonly string[]).includes(definition.sort.field)
|
|
991
|
+
? `m:${definition.sort.field}`
|
|
992
|
+
: definition.sort.field
|
|
973
993
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
sort: {
|
|
1004
|
-
field,
|
|
1005
|
-
direction:
|
|
1006
|
-
definition.sort.field === field &&
|
|
1007
|
-
definition.sort.direction === "desc"
|
|
1008
|
-
? "asc"
|
|
1009
|
-
: "desc",
|
|
1010
|
-
},
|
|
1011
|
-
});
|
|
1012
|
-
}}
|
|
1013
|
-
emptyStateTitle="No equities match this screen."
|
|
1014
|
-
/>
|
|
1015
|
-
</PaneStatusBody>
|
|
994
|
+
sortDirection={definition.sort.direction}
|
|
995
|
+
onHeaderClick={(id) => {
|
|
996
|
+
const field = id.startsWith("m:")
|
|
997
|
+
? (id.slice(2) as NumericField)
|
|
998
|
+
: id === "percentile" || id === "date"
|
|
999
|
+
? metric
|
|
1000
|
+
: ["symbol", "sector", "exchange"].includes(id)
|
|
1001
|
+
? (id as ScreenField)
|
|
1002
|
+
: null;
|
|
1003
|
+
if (!field) return;
|
|
1004
|
+
if ((NUMERIC_FIELDS as readonly string[]).includes(field))
|
|
1005
|
+
setMetric(field as NumericField);
|
|
1006
|
+
apply({
|
|
1007
|
+
...definition,
|
|
1008
|
+
sort: {
|
|
1009
|
+
field,
|
|
1010
|
+
direction:
|
|
1011
|
+
definition.sort.field === field &&
|
|
1012
|
+
definition.sort.direction === "desc"
|
|
1013
|
+
? "asc"
|
|
1014
|
+
: "desc",
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
}}
|
|
1018
|
+
emptyStateTitle={currencyEmpty?.title ?? "No equities match this screen."}
|
|
1019
|
+
emptyStateHint={currencyEmpty ? `${currencyEmpty.detail} Choose ${currencyEmpty.base.currency} to screen in the base currency.` : undefined}
|
|
1020
|
+
/>
|
|
1021
|
+
</PaneStatusBody>
|
|
1022
|
+
</>
|
|
1016
1023
|
);
|
|
1017
1024
|
return (
|
|
1018
1025
|
<Box width={width} height={height} flexDirection="column">
|
|
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef } from "react";
|
|
|
2
2
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
3
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
4
|
import type { ExposureEvidence, ExposurePayload } from "../../../api-client/exposure";
|
|
5
|
-
import { ActionRow, Button, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView,
|
|
5
|
+
import { ActionRow, Button, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, QueryBar, SectionHeading, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type StatItem } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
7
7
|
import { useAppSelector, useAsyncResource, useAutoRefresh, useMarketData, usePaneInstance, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
8
8
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
@@ -251,20 +251,21 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
251
251
|
<PageStackView focused={focused && !editing} detailOpen={detailOpen} onBack={() => { setDetail(null); setEditing(false); }}
|
|
252
252
|
detailTitle={editing ? "Scenario and holdings" : detail ? `${detail.symbol} · ${detail.label}` : "What we could not see"}
|
|
253
253
|
detailContent={editing && !savedSnapshot.error ? <ExposureEditor initial={{ ...inputs, scenario: scenario ?? inputs.scenario }} initialCustom={savedScenario.error ? customSetting : undefined} sources={sources} focused={focused} width={width} onCancel={() => setEditing(false)} onSave={value => { if (repairingCustomSetting.current && customSetting) setCustomSetting(JSON.stringify(value.scenario)); setInputs(value); setScenarioId("custom"); setEditing(false); setDetail(null); }} /> : data ? <ExposureDetail row={detail} data={data} width={width} /> : null}
|
|
254
|
-
rootContent={
|
|
255
|
-
actions={
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
rootContent={!data && !resource.loading && !resource.error && !inputError
|
|
255
|
+
? <EmptyState title="Choose holdings and a scenario." actions={<ActionRow label="Configure exposure" onPress={() => setEditing(true)} />} />
|
|
256
|
+
// A scenario change reloads under the query bar, which stays.
|
|
257
|
+
: <DataTableView<ExposureRow> columns={columns} items={data ? items : []} getItemKey={r => r.id} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen}
|
|
258
|
+
status={data && !inputError ? undefined : { loading: !inputError && resource.loading, error: inputError ?? resource.error, subject: "exposure analysis",
|
|
259
|
+
actions: savedSnapshot.error ? <Button label="Reset saved snapshot" onPress={() => { setSnapshotSetting(null); setEditing(false); setDetail(null); }} />
|
|
260
|
+
: savedScenario.error ? <><Button label="Edit scenario" onPress={() => setEditing(true)} /><Button label="Reset custom scenario" onPress={() => { setCustomSetting(""); setInputs({ ...inputs, scenario: DEFAULT_SCENARIO }); setScenarioId(DEFAULT_SCENARIO.id!); setEditing(false); setDetail(null); }} /></> : undefined }}
|
|
258
261
|
selection={{ kind: "id", selectedId: selected?.id ?? null, getId: r => r.id, onChange: (id) => { if (!id.startsWith("locked:")) setSelected(id); } }}
|
|
259
262
|
onActivate={row => row.id.startsWith("locked:") ? upgrade() : setDetail(row.id)} selectedTextOverridesCellColor showHorizontalScrollbar
|
|
260
263
|
sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={column => setSort(old => ({ column, direction: old.column === column && old.direction === "desc" ? "asc" : "desc" }))}
|
|
261
264
|
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={query} figures={figures}
|
|
262
|
-
chart={tab === "paths" ? !selected?.components[0]?.path.length ? null : { render: size => <ExposurePathDiagram component={selected?.components[0] ?? null} {...size} />, minRows: 5, maxRows: 5, strip: null }
|
|
265
|
+
chart={!data ? null : tab === "paths" ? !selected?.components[0]?.path.length ? null : { render: size => <ExposurePathDiagram component={selected?.components[0] ?? null} {...size} />, minRows: 5, maxRows: 5, strip: null }
|
|
263
266
|
: rows.some(r => r.exposure) ? { render: size => <ExposureRanges rows={rows} showTargets={tab === "portfolio" || data.scenario.shocks.length > 1} selectedId={selected?.id ?? null} onSelect={setSelected} onOpen={setDetail} {...size} />, minRows: 1, maxRows: Math.max(1, Math.min(7, rows.filter(r => r.exposure && r.shockId === selected?.shockId && r.basis === selected?.basis && r.period === selected?.period).length)), strip: null } : null} />}
|
|
264
267
|
renderCell={renderCell}
|
|
265
268
|
emptyStateTitle={tab === "drivers" ? "No supported company drivers are available for these holdings." : tab === "paths" ? "No supported exposure paths for this scenario." : "No quantified exposure in this view."}
|
|
266
|
-
bodyAfter={lockedRows && desktop ? <LockedOverlay rows={lockedRows} text="Pro: full holdings and deeper exposure paths" onPress={upgrade} role="exposure-upgrade" /> : undefined} />
|
|
267
|
-
: !resource.loading && !resource.error ? <EmptyState title="Choose holdings and a scenario." actions={<ActionRow label="Configure exposure" onPress={() => setEditing(true)} />} /> : null}
|
|
268
|
-
</PaneStatusBody>} />
|
|
269
|
+
bodyAfter={lockedRows && desktop ? <LockedOverlay rows={lockedRows} text="Pro: full holdings and deeper exposure paths" onPress={upgrade} role="exposure-upgrade" /> : undefined} />} />
|
|
269
270
|
</Box>;
|
|
270
271
|
}
|
|
@@ -79,9 +79,11 @@ function boardColumns(width: number, options: { windows: GpuChangeKey[]; trend:
|
|
|
79
79
|
];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function GpuBoard({ rows, models, asOf, model, setModel, selectedId, select, emptyTitle, width, height, focused }: {
|
|
82
|
+
export function GpuBoard({ rows, models, asOf, model, setModel, selectedId, select, emptyTitle, width, height, focused, pending = false }: {
|
|
83
83
|
rows: GpuBoardRow[]; models: readonly string[]; asOf: string | null; model: string; setModel: (value: string) => void; selectedId: string; select: (row: GpuBoardRow, open?: boolean) => void;
|
|
84
84
|
emptyTitle: string; width: number; height: number; focused: boolean;
|
|
85
|
+
/** The picked GPU's free preview is still loading. */
|
|
86
|
+
pending?: boolean;
|
|
85
87
|
}) {
|
|
86
88
|
const colors = useThemeColors();
|
|
87
89
|
const sections = useMemo(() => gpuBoardSections(rows, model), [rows, model]);
|
|
@@ -153,6 +155,7 @@ export function GpuBoard({ rows, models, asOf, model, setModel, selectedId, sele
|
|
|
153
155
|
|
|
154
156
|
const query = <GpuModelQuery models={models} model={model} setModel={setModel} width={width} />;
|
|
155
157
|
return <DataTableView<BoardRow> columns={columns} items={items} rootWidth={width} rootHeight={height} focused={focused}
|
|
158
|
+
status={pending && !items.length ? { loading: true, subject: "GPU rental prices" } : undefined}
|
|
156
159
|
rootBefore={ladder.length ? <ChartTableHeader width={width} height={height} tableRows={items.length} tableColumns={columns} query={query}
|
|
157
160
|
chart={{ maxRows: ladder.length + 1, minRows: Math.min(3, ladder.length + 1), strip: null,
|
|
158
161
|
render: (size) => <GpuPriceLadder rows={ladder} width={size.width} height={size.height}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import type { GpuEvent } from "../../../api-client/gpu";
|
|
3
3
|
import {
|
|
4
|
-
Badge, buildSectionedRows, DataTableView, EMPTY_TABLE_CELL, isSectionedItemRow,
|
|
4
|
+
Badge, buildSectionedRows, DataTableView, EMPTY_TABLE_CELL, isSectionedItemRow, renderSectionedRowHeader,
|
|
5
5
|
usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn, type SectionedRow,
|
|
6
6
|
} from "../../../components";
|
|
7
7
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -82,13 +82,13 @@ export function GpuChanges({ models, model, setModel, reloadBoard, width, height
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
// A GPU picked from the filter loads under it; the filter stays.
|
|
86
|
+
return <DataTableView<ChangeRow> columns={columns} items={items} rootWidth={width} rootHeight={height} focused={focused}
|
|
87
|
+
rootBefore={<GpuModelQuery models={models} model={model} setModel={setModel} width={width} />}
|
|
88
|
+
status={resource.data ? undefined : { loading: resource.loading, error: resource.error, subject: "GPU price changes" }}
|
|
89
|
+
selection={{ kind: "id", selectedId: selected && items.some((item) => item.key === selected) ? selected : firstId, getId: rowKey,
|
|
90
|
+
onChange: (id) => setSelected(String(id)) }}
|
|
91
|
+
isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
|
|
92
|
+
getItemKey={rowKey} onActivate={(row) => setSelected(row.key)} sortColumnId={null} sortDirection="desc"
|
|
93
|
+
selectedTextOverridesCellColor renderCell={renderCell} emptyStateTitle={resource.data?.payload.access?.locked ? "Full change history is available with Pro." : "No price or availability changes recorded yet."} />;
|
|
94
94
|
}
|
|
@@ -39,9 +39,11 @@ function equityColumns(width: number): DataTableColumn[] {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Related shares by what ties them to GPU rents, each beside the list price that ties it. */
|
|
42
|
-
export function GpuEquities({ board, models, locked, model, setModel, reloadBoard, width, height, focused }: {
|
|
42
|
+
export function GpuEquities({ board, models, locked, model, setModel, reloadBoard, width, height, focused, pending = false }: {
|
|
43
43
|
board: GpuBoardRow[]; models: readonly string[]; locked: boolean; model: string; setModel: (value: string) => void; reloadBoard: () => void;
|
|
44
44
|
width: number; height: number; focused: boolean;
|
|
45
|
+
/** The picked GPU's free preview is still loading. */
|
|
46
|
+
pending?: boolean;
|
|
45
47
|
}) {
|
|
46
48
|
const colors = useThemeColors();
|
|
47
49
|
const provider = useAssetData();
|
|
@@ -110,6 +112,7 @@ export function GpuEquities({ board, models, locked, model, setModel, reloadBoar
|
|
|
110
112
|
}, [colors, desktop]);
|
|
111
113
|
return <DataTableView<Row> columns={equityColumns(width)} items={items} rootWidth={width} rootHeight={height} focused={focused}
|
|
112
114
|
rootBefore={<GpuModelQuery models={models} model={model || "H100"} setModel={setModel} width={width} allowAll={false} />}
|
|
115
|
+
status={pending ? { loading: true, subject: "GPU rental prices" } : undefined}
|
|
113
116
|
selection={{ kind: "id", selectedId: selected, getId: rowKey, onChange: (id) => setSelected(String(id)) }} getItemKey={rowKey}
|
|
114
117
|
isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
|
|
115
118
|
getRowVersion={(row) => isSectionedItemRow(row) ? [quotes.get(row.item.symbol)?.quote?.lastUpdated, quotes.get(row.item.symbol)?.loading,
|
|
@@ -92,9 +92,11 @@ function HistoryDetail({ row, peers, width, height, refreshVersion }: {
|
|
|
92
92
|
</PaneStatusBody>;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function GpuHistory({ rows, models: boardModels, model, setModel, selectedId, onSelect, reloadBoard, width, height, focused }: {
|
|
95
|
+
export function GpuHistory({ rows, models: boardModels, model, setModel, selectedId, onSelect, reloadBoard, width, height, focused, pending = false }: {
|
|
96
96
|
rows: GpuBoardRow[]; models: readonly string[]; model: string; setModel: (value: string) => void; selectedId: string; onSelect: (id: string) => void;
|
|
97
97
|
reloadBoard: () => void; width: number; height: number; focused: boolean;
|
|
98
|
+
/** The picked GPU's free preview is still loading. */
|
|
99
|
+
pending?: boolean;
|
|
98
100
|
}) {
|
|
99
101
|
const colors = useThemeColors();
|
|
100
102
|
const [basis, setBasis] = usePluginPaneState<string>("historyBasis", "all");
|
|
@@ -133,7 +135,7 @@ export function GpuHistory({ rows, models: boardModels, model, setModel, selecte
|
|
|
133
135
|
searchPlaceholder="Provider or variant" columns={listColumns} rows={listRows} getRowId={listRowId}
|
|
134
136
|
filters={[{ id: "gpu", label: "GPU", value: activeModel, options: models.map((value) => ({ value, label: value })), onChange: setModel }]}
|
|
135
137
|
isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
|
|
136
|
-
renderCell={renderCell} emptyStateTitle="No series match."
|
|
138
|
+
renderCell={renderCell} emptyStateTitle="No series match." status={pending && !listRows.length ? { loading: true, subject: "GPU rental prices" } : undefined}
|
|
137
139
|
renderDetail={(size) => list.selected ? <HistoryDetail key={list.selected.id} row={list.selected} peers={modelRows} refreshVersion={refreshVersion} {...size} />
|
|
138
140
|
: <PaneStatusBody empty emptyTitle={`No ${gpuBasisLabel(basis === "all" ? "list" : basis as GpuBasis)} series yet.`} />} />;
|
|
139
141
|
}
|
|
@@ -61,13 +61,15 @@ function GpuView({ width, height, focused }: PaneProps) {
|
|
|
61
61
|
usePaneFooter("gpu:actions", () => ({ info: data?.access?.preview ? [{ id: "preview", parts: [{ text: "Free preview", tone: "muted" }] }] : [],
|
|
62
62
|
hints: [...(data?.access?.locked ? [{ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Unlock full GPU history with Pro", onPress: upgrade }] : []), { id: "buildout", key: "t", label: "BO", title: "Open TheBuildout",
|
|
63
63
|
onPress: () => createPaneFromTemplate("buildout-pane") }] }), [createPaneFromTemplate, data?.access, upgrade]);
|
|
64
|
-
const { strip, rows: tabRows } = usePaneTabs(
|
|
65
|
-
|
|
64
|
+
const { strip, rows: tabRows } = usePaneTabs(!signIn ? { tabs: [...GPU_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
|
|
65
|
+
// Another GPU's free preview waits in the table under the GPU filter, so picking a model never takes the filter away.
|
|
66
|
+
const pending = !!previewKey && preview.loading && !preview.data;
|
|
67
|
+
const body = { width, height: Math.max(3, height - tabRows), focused, pending };
|
|
66
68
|
const reloadBoard = useCallback(() => { void resource.reload(); void preview.reload(); }, [resource.reload, preview.reload]);
|
|
67
69
|
if (signIn) return <SignInWall placement="gpu" width={width} height={height} action="browse GPU rental prices" needsVerification={session.needsVerification} />;
|
|
68
70
|
return <Box width={width} height={height} flexDirection="column">
|
|
69
71
|
{strip}
|
|
70
|
-
<PaneStatusBody loading={
|
|
72
|
+
<PaneStatusBody loading={resource.loading && !data} error={!data && !notAvailable ? resource.error : null}
|
|
71
73
|
empty={notAvailable} emptyTitle={GPU_NOT_AVAILABLE} subject="GPU rental prices">
|
|
72
74
|
{data ? tab === "board" ? <GpuBoard rows={rows} models={models} asOf={data.asOf} model={activeModel} setModel={setModel} selectedId={selectedId} emptyTitle={emptyTitle}
|
|
73
75
|
select={(row, open) => { setSelectedId(row.id); if (open) setTab("history"); }} {...body} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataProvider, MarketDataRequestContext } from "../../../types/data-provider";
|
|
2
2
|
import type { HolderData, TickerFinancials } from "../../../types/financials";
|
|
3
3
|
import { holderStakeMarketCap, holderValueCurrency } from "./format";
|
|
4
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
5
|
|
|
5
6
|
export async function loadHolderData(
|
|
6
7
|
provider: DataProvider,
|
|
@@ -34,7 +35,7 @@ export async function loadHolderSnapshot(
|
|
|
34
35
|
): Promise<{ data: HolderData; marketCap?: number; sharesOutstanding?: number | null }> {
|
|
35
36
|
const [data, financials] = await Promise.all([
|
|
36
37
|
loadHolderData(provider, symbol, exchange),
|
|
37
|
-
provider.getTickerFinancials(symbol, exchange).catch((
|
|
38
|
+
provider.getTickerFinancials(symbol, exchange).catch(logFailure("holders.client", "Read holder reference financials failed", null)),
|
|
38
39
|
]);
|
|
39
40
|
const quote = financials?.quote;
|
|
40
41
|
return {
|