From 5a8c7ecc5899e337e94ce1d9e7ddbebf350c180e Mon Sep 17 00:00:00 2001 From: matitos Date: Mon, 21 Sep 2026 18:26:01 +0000 Subject: [PATCH] Show a loading state in the strategy dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While the strategy list is being fetched the dropdown now shows a disabled "Loading strategies…" option instead of an empty select, and "No strategies available" once loading finishes with no results. Applies to the strategy, backtest and sweep panels via a loading prop. Adds tests. --- frontend/src/App.vue | 13 +++++-- frontend/src/components/BacktestPanel.vue | 2 ++ frontend/src/components/StrategyForm.vue | 12 ++++++- frontend/src/components/StrategyPanel.vue | 2 ++ frontend/src/components/SweepPanel.vue | 6 +++- frontend/src/strategy-form.test.js | 42 +++++++++++++++++++++++ 6 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 frontend/src/strategy-form.test.js diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d4e4bb3..995cb79 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -29,6 +29,7 @@ const status = ref({ }) const strategies = ref([]) +const strategiesLoading = ref(true) const presets = ref([]) const positions = ref([]) const orders = ref([]) @@ -71,7 +72,12 @@ async function refreshAll() { } async function loadStrategies() { - strategies.value = await api.getStrategies() + strategiesLoading.value = true + try { + strategies.value = await api.getStrategies() + } finally { + strategiesLoading.value = false + } } async function loadPresets() { @@ -265,6 +271,7 @@ onUnmounted(() => { :status="status" :busy="busy" :presets="presets" + :loading="strategiesLoading" @start="handleStart" @stop="handleStop" @kill="handleKill" @@ -272,8 +279,8 @@ onUnmounted(() => { @delete-preset="handleDeletePreset" /> - - + +
diff --git a/frontend/src/components/BacktestPanel.vue b/frontend/src/components/BacktestPanel.vue index 4ab0a9d..9562ea7 100644 --- a/frontend/src/components/BacktestPanel.vue +++ b/frontend/src/components/BacktestPanel.vue @@ -9,6 +9,7 @@ import StrategyForm from './StrategyForm.vue' defineProps({ strategies: { type: Array, required: true }, status: { type: Object, required: true }, + loading: { type: Boolean, default: false }, }) const running = ref(false) @@ -44,6 +45,7 @@ async function run(payload) { :busy="running" :symbol="status.symbol" :timeframe="status.timeframe" + :loading="loading" submit-label="Run backtest" @submit="run" /> diff --git a/frontend/src/components/StrategyForm.vue b/frontend/src/components/StrategyForm.vue index a3ef9dc..e6c63f8 100644 --- a/frontend/src/components/StrategyForm.vue +++ b/frontend/src/components/StrategyForm.vue @@ -10,6 +10,9 @@ const props = defineProps({ symbol: { type: String, default: 'BTC/USDT' }, timeframe: { type: String, default: '5m' }, presets: { type: Array, default: () => [] }, + // True while the strategy list is still being fetched, so the dropdown can + // show a loading state instead of appearing empty. + loading: { type: Boolean, default: false }, }) const emit = defineEmits(['submit', 'save-preset', 'delete-preset', 'load-preset']) @@ -99,7 +102,14 @@ function submit() {
diff --git a/frontend/src/components/StrategyPanel.vue b/frontend/src/components/StrategyPanel.vue index 2b1c885..b93862f 100644 --- a/frontend/src/components/StrategyPanel.vue +++ b/frontend/src/components/StrategyPanel.vue @@ -8,6 +8,7 @@ const props = defineProps({ status: { type: Object, required: true }, busy: { type: Boolean, default: false }, presets: { type: Array, default: () => [] }, + loading: { type: Boolean, default: false }, }) const emit = defineEmits(['start', 'stop', 'kill', 'save-preset', 'delete-preset']) @@ -34,6 +35,7 @@ function confirmKill() { :symbol="status.symbol" :timeframe="status.timeframe" :presets="presets" + :loading="loading" @submit="emit('start', $event)" @save-preset="emit('save-preset', $event)" @delete-preset="emit('delete-preset', $event)" diff --git a/frontend/src/components/SweepPanel.vue b/frontend/src/components/SweepPanel.vue index fb8f295..e77692c 100644 --- a/frontend/src/components/SweepPanel.vue +++ b/frontend/src/components/SweepPanel.vue @@ -7,6 +7,7 @@ import CollapsiblePanel from './CollapsiblePanel.vue' const props = defineProps({ strategies: { type: Array, required: true }, status: { type: Object, required: true }, + loading: { type: Boolean, default: false }, }) const running = ref(false) @@ -97,7 +98,10 @@ async function run() {
diff --git a/frontend/src/strategy-form.test.js b/frontend/src/strategy-form.test.js new file mode 100644 index 0000000..359f301 --- /dev/null +++ b/frontend/src/strategy-form.test.js @@ -0,0 +1,42 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import StrategyForm from './components/StrategyForm.vue' + +function mountForm(props) { + return mount(StrategyForm, { props: { strategies: [], ...props } }) +} + +describe('StrategyForm strategy dropdown states', () => { + it('shows a loading option while the list is being fetched', () => { + const wrapper = mountForm({ loading: true }) + const select = wrapper.find('select') + expect(select.text()).toContain('Loading strategies…') + expect(select.attributes('disabled')).toBeDefined() + expect(wrapper.find('option[value=""]').attributes('disabled')).toBeDefined() + }) + + it('shows a no-strategies option when loaded but empty', () => { + const wrapper = mountForm({ loading: false }) + expect(wrapper.find('select').text()).toContain('No strategies available') + }) + + it('lists strategies and enables the select once loaded', () => { + const wrapper = mountForm({ + loading: false, + strategies: [ + { + key: 'sma_cross', + name: 'SMA Crossover', + description: '', + params: [{ name: 'fast', type: 'int', default: 10, description: '' }], + }, + { key: 'ema_cross', name: 'EMA Crossover', description: '', params: [] }, + ], + }) + const select = wrapper.find('select') + expect(select.attributes('disabled')).toBeUndefined() + expect(select.findAll('option')).toHaveLength(2) + expect(select.text()).toContain('SMA Crossover') + expect(select.element.value).toBe('sma_cross') + }) +})