Add quote lifecycle statistics chart
All checks were successful
deploy / deploy (push) Successful in 1m10s
All checks were successful
deploy / deploy (push) Successful in 1m10s
Proof: Added an ECharts stacked persisted quote lifecycle statistics chart with range controls, raised dashboard statistics loads to the existing 1000-row clamp, and covered the UI wiring with static regression assertions. Ran targeted dashboard tests, full npm test, and npm run operator-dashboard:build. Assumptions: Persisted quote lifecycle statistics remain the durable source of truth for the chart; live counters stay separate until the history rollup refresh records them. Still fake: Fee-aware PnL, quote size distributions, oracle-deviation analytics, and any already-pruned lifecycle detail remain out of scope; the chart can only show retained persisted statistics.
This commit is contained in:
parent
2e95c95246
commit
75d7f64627
6 changed files with 296 additions and 4 deletions
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "near-intents-monitor-poc",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"echarts": "^5.6.0",
|
||||
"kafkajs": "^2.2.4",
|
||||
"near-api-js": "^7.2.0",
|
||||
"pg": "^8.20.0",
|
||||
|
|
@ -1584,6 +1585,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.331",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
||||
|
|
@ -2564,6 +2575,12 @@
|
|||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
|
|
@ -2753,6 +2770,15 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"echarts": "^5.6.0",
|
||||
"kafkajs": "^2.2.4",
|
||||
"near-api-js": "^7.2.0",
|
||||
"pg": "^8.20.0",
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ async function handleApiRequest({ req, res, url, auth }) {
|
|||
if (req.method === 'GET' && url.pathname === '/api/strategy/quote-lifecycle/statistics') {
|
||||
const payload = await loadQuoteLifecycleStatisticsPayload({
|
||||
grains: url.searchParams.get('grains') || undefined,
|
||||
limit: Number(url.searchParams.get('limit') || 120),
|
||||
limit: Number(url.searchParams.get('limit') || 1000),
|
||||
});
|
||||
return sendJson(res, 200, payload);
|
||||
}
|
||||
|
|
@ -583,7 +583,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
|||
),
|
||||
safeSourceLoad(
|
||||
'quote_lifecycle_statistics',
|
||||
() => loadQuoteLifecycleStatistics(pool, { limit: 120 }),
|
||||
() => loadQuoteLifecycleStatistics(pool, { limit: 1000 }),
|
||||
[],
|
||||
sourceErrors,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BarChart, LineChart } from 'echarts/charts';
|
||||
import { DataZoomComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components';
|
||||
import { init as initEcharts, use as useEcharts } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
import EmptyState from '../components/EmptyState.jsx';
|
||||
import MetricCard from '../components/MetricCard.jsx';
|
||||
|
|
@ -13,6 +17,16 @@ import {
|
|||
updateSelectedLifecycleFromLookup,
|
||||
} from '../lib/quoteLifecycleInvestigator.js';
|
||||
|
||||
useEcharts([
|
||||
BarChart,
|
||||
LineChart,
|
||||
DataZoomComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
|
||||
const TRADING_PAIR_MODES = new Set(['maker', 'taker', 'both']);
|
||||
const COMPETITIVENESS_GROUP_ROW_COUNT = 12;
|
||||
|
|
@ -39,6 +53,21 @@ const QUOTE_LIFECYCLE_STAT_GRAINS = [
|
|||
['month', 'Month'],
|
||||
['all_time', 'All-time'],
|
||||
];
|
||||
const QUOTE_LIFECYCLE_STAT_RANGES = [
|
||||
['one_hour', '1h'],
|
||||
['six_hours', '6h'],
|
||||
['one_day', '24h'],
|
||||
['seven_days', '7d'],
|
||||
['thirty_days', '30d'],
|
||||
['all_loaded', 'Loaded'],
|
||||
];
|
||||
const QUOTE_LIFECYCLE_STAT_RANGE_DURATIONS_MS = {
|
||||
one_hour: 60 * 60 * 1000,
|
||||
six_hours: 6 * 60 * 60 * 1000,
|
||||
one_day: 24 * 60 * 60 * 1000,
|
||||
seven_days: 7 * 24 * 60 * 60 * 1000,
|
||||
thirty_days: 30 * 24 * 60 * 60 * 1000,
|
||||
};
|
||||
const QUOTE_LIFECYCLE_STAT_BUCKETS = [
|
||||
['success', 'Success'],
|
||||
['submitted_no_reply', 'Submitted no reply'],
|
||||
|
|
@ -51,6 +80,18 @@ const QUOTE_LIFECYCLE_STAT_BUCKETS = [
|
|||
['observed_no_decision', 'Observed no decision'],
|
||||
['unavailable', 'Unavailable'],
|
||||
];
|
||||
const QUOTE_LIFECYCLE_STAT_BUCKET_COLORS = {
|
||||
success: '#1f7a5a',
|
||||
submitted_no_reply: '#0f5b84',
|
||||
submission_failed: '#b64328',
|
||||
awaiting_executor: '#b36a0c',
|
||||
approved_no_command: '#6d58a8',
|
||||
rejected_by_strategy: '#7b8794',
|
||||
blocked_before_submit: '#9b4d2f',
|
||||
not_filled: '#566f3d',
|
||||
observed_no_decision: '#4b5563',
|
||||
unavailable: '#9ca3af',
|
||||
};
|
||||
|
||||
async function copyIdentifier(value) {
|
||||
if (!value || !navigator?.clipboard?.writeText) return;
|
||||
|
|
@ -78,7 +119,7 @@ async function fetchQuoteLifecycleLookup(identifier) {
|
|||
}
|
||||
|
||||
async function fetchQuoteLifecycleStatistics() {
|
||||
const response = await fetch('/api/strategy/quote-lifecycle/statistics?limit=160', {
|
||||
const response = await fetch('/api/strategy/quote-lifecycle/statistics?limit=1000', {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
|
|
@ -258,6 +299,175 @@ function latestStatisticRow(rows, grain) {
|
|||
))[0] || null;
|
||||
}
|
||||
|
||||
function statisticRowsForGrain(rows, grain) {
|
||||
return (rows || [])
|
||||
.filter((row) => row?.grain === grain)
|
||||
.sort((left, right) => (statisticRowWindowMarker(left) || 0) - (statisticRowWindowMarker(right) || 0));
|
||||
}
|
||||
|
||||
function statisticRowWindowMarker(row) {
|
||||
const end = Date.parse(row?.window_end || '');
|
||||
if (Number.isFinite(end)) return end;
|
||||
const start = Date.parse(row?.window_start || '');
|
||||
if (Number.isFinite(start)) return start;
|
||||
const computed = Date.parse(row?.computed_at || '');
|
||||
return Number.isFinite(computed) ? computed : null;
|
||||
}
|
||||
|
||||
function statisticRowsForRange(rows, grain, range, now = Date.now()) {
|
||||
const grainRows = statisticRowsForGrain(rows, grain);
|
||||
if (grain === 'all_time' || range === 'all_loaded') return grainRows;
|
||||
|
||||
const durationMs = QUOTE_LIFECYCLE_STAT_RANGE_DURATIONS_MS[range];
|
||||
if (!durationMs) return grainRows;
|
||||
const cutoff = now - durationMs;
|
||||
const rangedRows = grainRows.filter((row) => {
|
||||
const marker = statisticRowWindowMarker(row);
|
||||
return marker == null || marker >= cutoff;
|
||||
});
|
||||
return rangedRows.length ? rangedRows : grainRows.slice(-1);
|
||||
}
|
||||
|
||||
function formatStatisticChartLabel(row) {
|
||||
if (!row) return 'Unavailable';
|
||||
if (row.grain === 'all_time') return 'All-time';
|
||||
const at = Date.parse(row.window_start || row.window_end || '');
|
||||
if (!Number.isFinite(at)) return 'Unavailable';
|
||||
const date = new Date(at);
|
||||
const options = row.grain === 'five_minute' || row.grain === 'hour'
|
||||
? { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' }
|
||||
: row.grain === 'day' || row.grain === 'week'
|
||||
? { month: 'short', day: '2-digit', timeZone: 'UTC' }
|
||||
: { year: 'numeric', month: 'short', timeZone: 'UTC' };
|
||||
return new Intl.DateTimeFormat('en-GB', options).format(date).replace(',', '');
|
||||
}
|
||||
|
||||
function buildQuoteLifecycleStatisticsChartOption(rows) {
|
||||
const labels = rows.map(formatStatisticChartLabel);
|
||||
const stateSeries = QUOTE_LIFECYCLE_STAT_BUCKETS.map(([bucket, label]) => ({
|
||||
name: label,
|
||||
type: 'bar',
|
||||
stack: 'quote-states',
|
||||
emphasis: { focus: 'series' },
|
||||
data: rows.map((row) => statisticBucketCount(row, bucket)),
|
||||
itemStyle: {
|
||||
color: QUOTE_LIFECYCLE_STAT_BUCKET_COLORS[bucket],
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
animationDuration: 220,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
valueFormatter: (value) => formatInteger(value),
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textStyle: { color: '#60716a' },
|
||||
pageIconColor: '#1f7a5a',
|
||||
pageTextStyle: { color: '#60716a' },
|
||||
},
|
||||
grid: {
|
||||
left: 48,
|
||||
right: 18,
|
||||
top: 74,
|
||||
bottom: rows.length > 18 ? 64 : 38,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels,
|
||||
axisLabel: {
|
||||
color: '#60716a',
|
||||
hideOverlap: true,
|
||||
rotate: rows.length > 18 ? 35 : 0,
|
||||
},
|
||||
axisLine: { lineStyle: { color: 'rgba(24, 33, 30, 0.22)' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
splitLine: { lineStyle: { color: 'rgba(24, 33, 30, 0.1)' } },
|
||||
axisLabel: {
|
||||
color: '#60716a',
|
||||
formatter: (value) => formatInteger(value),
|
||||
},
|
||||
},
|
||||
dataZoom: rows.length > 24
|
||||
? [
|
||||
{ type: 'inside', throttle: 50 },
|
||||
{
|
||||
type: 'slider',
|
||||
height: 18,
|
||||
bottom: 12,
|
||||
borderColor: 'rgba(24, 33, 30, 0.14)',
|
||||
fillerColor: 'rgba(31, 122, 90, 0.16)',
|
||||
handleStyle: { color: '#1f7a5a' },
|
||||
textStyle: { color: '#60716a' },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
series: [
|
||||
...stateSeries,
|
||||
{
|
||||
name: 'Total',
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
symbolSize: 5,
|
||||
data: rows.map((row) => Number(row?.quote_count || 0)),
|
||||
lineStyle: { width: 2, color: '#18211e' },
|
||||
itemStyle: { color: '#18211e' },
|
||||
emphasis: { focus: 'series' },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function QuoteLifecycleStatisticsChart({ grain, range, rows }) {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstanceRef = useRef(null);
|
||||
const option = useMemo(() => buildQuoteLifecycleStatisticsChartOption(rows), [rows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return undefined;
|
||||
const chart = initEcharts(chartRef.current, null, { renderer: 'canvas' });
|
||||
chartInstanceRef.current = chart;
|
||||
|
||||
function resizeChart() {
|
||||
chart.resize();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeChart);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
chart.dispose();
|
||||
chartInstanceRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartInstanceRef.current) return;
|
||||
chartInstanceRef.current.clear();
|
||||
chartInstanceRef.current.setOption(option);
|
||||
chartInstanceRef.current.resize();
|
||||
}, [option]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={`Quote lifecycle statistics chart, ${plainCodeLabel(grain)}, ${plainCodeLabel(range)}`}
|
||||
className="quote-statistics-chart"
|
||||
ref={chartRef}
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function formatStatisticWindow(row) {
|
||||
if (!row) return 'Unavailable';
|
||||
if (row.grain === 'all_time') return 'All-time';
|
||||
|
|
@ -927,8 +1137,10 @@ function QuoteLifecycleStatisticsPanel({
|
|||
grain,
|
||||
liveRows,
|
||||
onGrainChange,
|
||||
onRangeChange,
|
||||
onRefresh,
|
||||
persistedRows,
|
||||
range,
|
||||
state,
|
||||
}) {
|
||||
const allTime = latestStatisticRow(persistedRows, 'all_time');
|
||||
|
|
@ -939,6 +1151,7 @@ function QuoteLifecycleStatisticsPanel({
|
|||
const liveCurrent = latestStatisticRow(liveRows, grain);
|
||||
const persistedGeneratedAt = persistedRows?.[0]?.computed_at || null;
|
||||
const liveGeneratedAt = liveRows?.[0]?.computed_at || null;
|
||||
const chartRows = statisticRowsForRange(persistedRows, grain, range);
|
||||
const rows = fixedRows(selectedRows, grain === 'all_time' ? 1 : 8);
|
||||
|
||||
return (
|
||||
|
|
@ -952,6 +1165,14 @@ function QuoteLifecycleStatisticsPanel({
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field quote-statistics-range-field">
|
||||
<span>Chart range</span>
|
||||
<select disabled={grain === 'all_time'} value={range} onChange={(event) => onRangeChange(event.target.value)}>
|
||||
{QUOTE_LIFECYCLE_STAT_RANGES.map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button className="button secondary" disabled={state === 'loading'} onClick={onRefresh} type="button">
|
||||
{state === 'loading' ? 'Refreshing stats' : 'Refresh stats'}
|
||||
</button>
|
||||
|
|
@ -977,6 +1198,11 @@ function QuoteLifecycleStatisticsPanel({
|
|||
<strong>{formatInteger((selectedCurrent?.missing_identifier_count || 0) + (selectedCurrent?.missing_timestamp_count || 0))}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{chartRows.length ? (
|
||||
<QuoteLifecycleStatisticsChart grain={grain} range={range} rows={chartRows} />
|
||||
) : (
|
||||
<div className="quote-statistics-chart-empty">No persisted quote statistics are available for this range.</div>
|
||||
)}
|
||||
<TableFrame className="quote-statistics-frame">
|
||||
<table className="quote-statistics-table">
|
||||
<thead>
|
||||
|
|
@ -1038,6 +1264,7 @@ function QuoteLifecycleTable({ items, statistics }) {
|
|||
const [showStrategyRejected, setShowStrategyRejected] = useState(true);
|
||||
const [problemFilter, setProblemFilter] = useState('all');
|
||||
const [statGrain, setStatGrain] = useState('five_minute');
|
||||
const [statRange, setStatRange] = useState('one_day');
|
||||
const [statsState, setStatsState] = useState('idle');
|
||||
const [statsError, setStatsError] = useState(null);
|
||||
const [statisticsSnapshot, setStatisticsSnapshot] = useState(() => statistics || null);
|
||||
|
|
@ -1232,8 +1459,10 @@ function QuoteLifecycleTable({ items, statistics }) {
|
|||
grain={statGrain}
|
||||
liveRows={liveStatisticRows}
|
||||
onGrainChange={setStatGrain}
|
||||
onRangeChange={setStatRange}
|
||||
onRefresh={() => refreshStatistics().catch(() => {})}
|
||||
persistedRows={persistedStatisticRows}
|
||||
range={statRange}
|
||||
state={statsState}
|
||||
/>
|
||||
<form className="quote-lifecycle-lookup" onSubmit={submitLookup}>
|
||||
|
|
|
|||
|
|
@ -631,7 +631,12 @@ table.lifecycle-table th:nth-child(5) {
|
|||
min-width: 180px;
|
||||
}
|
||||
|
||||
.quote-statistics-grain-field span {
|
||||
.quote-statistics-range-field {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.quote-statistics-grain-field span,
|
||||
.quote-statistics-range-field span {
|
||||
font-size: 0.84rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
|
@ -663,6 +668,22 @@ table.lifecycle-table th:nth-child(5) {
|
|||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.quote-statistics-chart {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.quote-statistics-chart-empty {
|
||||
min-height: 160px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.34);
|
||||
}
|
||||
|
||||
.quote-statistics-table {
|
||||
min-width: 980px;
|
||||
table-layout: fixed;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const strategySource = readFileSync(new URL('../src/operator-dashboard/static/pages/StrategyPage.jsx', import.meta.url), 'utf8');
|
||||
const packageSource = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
||||
const fundsSource = readFileSync(new URL('../src/operator-dashboard/static/pages/FundsPage.jsx', import.meta.url), 'utf8');
|
||||
const stylesSource = readFileSync(new URL('../src/operator-dashboard/static/styles.css', import.meta.url), 'utf8');
|
||||
const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static/components/ServiceCard.jsx', import.meta.url), 'utf8');
|
||||
|
|
@ -41,9 +42,11 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
|
|||
assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\//);
|
||||
assert.match(strategySource, /QUOTE_LIFECYCLE_PROBLEM_FILTERS/);
|
||||
assert.match(strategySource, /QUOTE_LIFECYCLE_STAT_GRAINS/);
|
||||
assert.match(strategySource, /QUOTE_LIFECYCLE_STAT_RANGES/);
|
||||
assert.match(strategySource, /QUOTE_LIFECYCLE_STAT_BUCKETS/);
|
||||
assert.match(strategySource, /fetchQuoteLifecycleStatistics/);
|
||||
assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\/statistics/);
|
||||
assert.match(strategySource, /statistics\?limit=1000/);
|
||||
assert.match(strategySource, /five_minute/);
|
||||
assert.match(strategySource, /all_time/);
|
||||
assert.match(strategySource, /submitted_no_reply/);
|
||||
|
|
@ -51,6 +54,16 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
|
|||
assert.match(strategySource, /success/);
|
||||
assert.match(strategySource, /quote_lifecycle_statistics/);
|
||||
assert.match(strategySource, /Refresh stats/);
|
||||
assert.match(strategySource, /Chart range/);
|
||||
assert.match(strategySource, /QuoteLifecycleStatisticsChart/);
|
||||
assert.match(strategySource, /echarts\/core/);
|
||||
assert.match(strategySource, /BarChart/);
|
||||
assert.match(strategySource, /LineChart/);
|
||||
assert.match(strategySource, /DataZoomComponent/);
|
||||
assert.match(strategySource, /stack: 'quote-states'/);
|
||||
assert.match(strategySource, /name: 'Total'/);
|
||||
assert.match(strategySource, /quote-statistics-chart/);
|
||||
assert.match(packageSource, /"echarts": "\^5\.6\.0"/);
|
||||
assert.match(strategySource, /awaiting_executor/);
|
||||
assert.match(strategySource, /quote_not_found_or_finished/);
|
||||
assert.match(strategySource, /approved_no_command/);
|
||||
|
|
@ -65,6 +78,8 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
|
|||
assert.match(strategySource, /quote-lifecycle-placeholder-row/);
|
||||
assert.match(stylesSource, /\.quote-investigator-panel/);
|
||||
assert.match(stylesSource, /\.quote-statistics-panel/);
|
||||
assert.match(stylesSource, /\.quote-statistics-chart/);
|
||||
assert.match(stylesSource, /\.quote-statistics-range-field/);
|
||||
assert.match(stylesSource, /\.quote-statistics-table/);
|
||||
assert.match(stylesSource, /\.quote-lifecycle-lookup/);
|
||||
assert.match(stylesSource, /\.quote-lifecycle-table tbody tr\.quote-lifecycle-row\.is-selected/);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue