Show a loading state in the strategy dropdown
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.
This commit is contained in:
1 parent
def86d7f56
commit
5a8c7ecc58
6 files changed
+71
-4
No files matched your search
@@ -29,6 +29,7 @@ const status = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const strategies = ref([])
|
const strategies = ref([])
|
||||||
|
const strategiesLoading = ref(true)
|
||||||
const presets = ref([])
|
const presets = ref([])
|
||||||
const positions = ref([])
|
const positions = ref([])
|
||||||
const orders = ref([])
|
const orders = ref([])
|
||||||
@@ -71,7 +72,12 @@ async function refreshAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadStrategies() {
|
async function loadStrategies() {
|
||||||
|
strategiesLoading.value = true
|
||||||
|
try {
|
||||||
strategies.value = await api.getStrategies()
|
strategies.value = await api.getStrategies()
|
||||||
|
} finally {
|
||||||
|
strategiesLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPresets() {
|
async function loadPresets() {
|
||||||
@@ -265,6 +271,7 @@ onUnmounted(() => {
|
|||||||
:status="status"
|
:status="status"
|
||||||
:busy="busy"
|
:busy="busy"
|
||||||
:presets="presets"
|
:presets="presets"
|
||||||
|
:loading="strategiesLoading"
|
||||||
@start="handleStart"
|
@start="handleStart"
|
||||||
@stop="handleStop"
|
@stop="handleStop"
|
||||||
@kill="handleKill"
|
@kill="handleKill"
|
||||||
@@ -272,8 +279,8 @@ onUnmounted(() => {
|
|||||||
@delete-preset="handleDeletePreset"
|
@delete-preset="handleDeletePreset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BacktestPanel :strategies="strategies" :status="status" />
|
<BacktestPanel :strategies="strategies" :status="status" :loading="strategiesLoading" />
|
||||||
<SweepPanel :strategies="strategies" :status="status" />
|
<SweepPanel :strategies="strategies" :status="status" :loading="strategiesLoading" />
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<EquityChart :points="equityPoints" />
|
<EquityChart :points="equityPoints" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import StrategyForm from './StrategyForm.vue'
|
|||||||
defineProps({
|
defineProps({
|
||||||
strategies: { type: Array, required: true },
|
strategies: { type: Array, required: true },
|
||||||
status: { type: Object, required: true },
|
status: { type: Object, required: true },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const running = ref(false)
|
const running = ref(false)
|
||||||
@@ -44,6 +45,7 @@ async function run(payload) {
|
|||||||
:busy="running"
|
:busy="running"
|
||||||
:symbol="status.symbol"
|
:symbol="status.symbol"
|
||||||
:timeframe="status.timeframe"
|
:timeframe="status.timeframe"
|
||||||
|
:loading="loading"
|
||||||
submit-label="Run backtest"
|
submit-label="Run backtest"
|
||||||
@submit="run"
|
@submit="run"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const props = defineProps({
|
|||||||
symbol: { type: String, default: 'BTC/USDT' },
|
symbol: { type: String, default: 'BTC/USDT' },
|
||||||
timeframe: { type: String, default: '5m' },
|
timeframe: { type: String, default: '5m' },
|
||||||
presets: { type: Array, default: () => [] },
|
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'])
|
const emit = defineEmits(['submit', 'save-preset', 'delete-preset', 'load-preset'])
|
||||||
|
|
||||||
@@ -99,7 +102,14 @@ function submit() {
|
|||||||
<div class="grid gap-2 sm:grid-cols-3">
|
<div class="grid gap-2 sm:grid-cols-3">
|
||||||
<label class="space-y-1">
|
<label class="space-y-1">
|
||||||
<span class="label">Strategy</span>
|
<span class="label">Strategy</span>
|
||||||
<select v-model="selectedKey" class="input" :disabled="disabled">
|
<select
|
||||||
|
v-model="selectedKey"
|
||||||
|
class="input"
|
||||||
|
:disabled="disabled || !strategies.length"
|
||||||
|
>
|
||||||
|
<option v-if="!strategies.length" value="" disabled>
|
||||||
|
{{ loading ? 'Loading strategies…' : 'No strategies available' }}
|
||||||
|
</option>
|
||||||
<option v-for="s in strategies" :key="s.key" :value="s.key">{{ s.name }}</option>
|
<option v-for="s in strategies" :key="s.key" :value="s.key">{{ s.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const props = defineProps({
|
|||||||
status: { type: Object, required: true },
|
status: { type: Object, required: true },
|
||||||
busy: { type: Boolean, default: false },
|
busy: { type: Boolean, default: false },
|
||||||
presets: { type: Array, default: () => [] },
|
presets: { type: Array, default: () => [] },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['start', 'stop', 'kill', 'save-preset', 'delete-preset'])
|
const emit = defineEmits(['start', 'stop', 'kill', 'save-preset', 'delete-preset'])
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ function confirmKill() {
|
|||||||
:symbol="status.symbol"
|
:symbol="status.symbol"
|
||||||
:timeframe="status.timeframe"
|
:timeframe="status.timeframe"
|
||||||
:presets="presets"
|
:presets="presets"
|
||||||
|
:loading="loading"
|
||||||
@submit="emit('start', $event)"
|
@submit="emit('start', $event)"
|
||||||
@save-preset="emit('save-preset', $event)"
|
@save-preset="emit('save-preset', $event)"
|
||||||
@delete-preset="emit('delete-preset', $event)"
|
@delete-preset="emit('delete-preset', $event)"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import CollapsiblePanel from './CollapsiblePanel.vue'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
strategies: { type: Array, required: true },
|
strategies: { type: Array, required: true },
|
||||||
status: { type: Object, required: true },
|
status: { type: Object, required: true },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const running = ref(false)
|
const running = ref(false)
|
||||||
@@ -97,7 +98,10 @@ async function run() {
|
|||||||
<div class="grid gap-2 sm:grid-cols-4">
|
<div class="grid gap-2 sm:grid-cols-4">
|
||||||
<label class="space-y-1">
|
<label class="space-y-1">
|
||||||
<span class="label">Strategy</span>
|
<span class="label">Strategy</span>
|
||||||
<select v-model="selectedKey" class="input" :disabled="running">
|
<select v-model="selectedKey" class="input" :disabled="running || !strategies.length">
|
||||||
|
<option v-if="!strategies.length" value="" disabled>
|
||||||
|
{{ loading ? 'Loading strategies…' : 'No strategies available' }}
|
||||||
|
</option>
|
||||||
<option v-for="s in strategies" :key="s.key" :value="s.key">{{ s.name }}</option>
|
<option v-for="s in strategies" :key="s.key" :value="s.key">{{ s.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user