fix(central-ui): keep the instance picker disabled until the site's instances have loaded (typing raced the load and was wiped)
Both Flow Debugger and Debug View enable the shared InstancePicker as soon as _selectedSiteId lands, but Blazor renders at the site-change handler's first await — at which point the picker is still holding the PREVIOUS site's Areas / Instances lists. Typing into the field in that window (an operator, or Playwright's FillAsync, which waits only for "enabled") was silently lost: the picker compares both lists by reference, so the freshly-assigned lists read as "the site changed" and it clears the field text and closes the panel. Both pages now hold an _instancesLoading flag across the whole load — set before the first await, cleared in a finally so a fault or a site-scope refusal cannot leave the picker disabled forever — and fold it into the picker's Disabled expression. InstancePicker itself is unchanged. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
<InstancePicker Areas="_siteAreas" Instances="_siteInstances"
|
||||
ItemLabel="InstanceLabel"
|
||||
SelectedInstanceId="_selectedInstanceId" SelectedInstanceIdChanged="OnInstancePicked"
|
||||
Disabled="@_connected" DataTestPrefix="debug-instance" />
|
||||
Disabled="@(_connected || _instancesLoading)" DataTestPrefix="debug-instance" />
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end gap-2">
|
||||
@if (!_connected)
|
||||
@@ -263,6 +263,12 @@
|
||||
private bool _loading = true;
|
||||
private bool _connected;
|
||||
private bool _connecting;
|
||||
|
||||
// True for the whole of LoadInstancesForSite. Blazor renders as soon as that load hits its
|
||||
// first await, and the page is not _connected then — so without this the picker is enabled
|
||||
// while it is still showing the PREVIOUS site's tree, and anything typed into it is wiped
|
||||
// when the new lists land (the picker reads a new list object as "the site changed").
|
||||
private bool _instancesLoading;
|
||||
private bool _connectedFromStorage;
|
||||
|
||||
// Which tab pane is visible — "attributes" (default) or "alarms".
|
||||
@@ -428,30 +434,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the selected site's instances and areas into the picker's two lists, holding
|
||||
/// <c>_instancesLoading</c> for the whole round trip so the field cannot be typed into
|
||||
/// while it is still showing the previous site's tree. The flag is cleared in a
|
||||
/// <c>finally</c>: a scope refusal or a repository fault must not leave the picker
|
||||
/// disabled forever.
|
||||
/// </summary>
|
||||
private async Task LoadInstancesForSite()
|
||||
{
|
||||
_siteInstances = new();
|
||||
_siteAreas = new();
|
||||
_selectedInstanceId = 0;
|
||||
if (_selectedSiteId == 0) return;
|
||||
// Site scoping (CentralUI-002): re-check the claim server-side — a query
|
||||
// string or stale localStorage value could name a site outside the grant.
|
||||
if (!await SiteScope.IsSiteAllowedAsync(_selectedSiteId))
|
||||
{
|
||||
_selectedSiteId = 0;
|
||||
_toast.ShowError("You are not permitted to debug instances on that site.");
|
||||
return;
|
||||
}
|
||||
_instancesLoading = true;
|
||||
try
|
||||
{
|
||||
_siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
|
||||
.Where(i => i.State == InstanceState.Enabled)
|
||||
.ToList();
|
||||
_siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
|
||||
_siteInstances = new();
|
||||
_siteAreas = new();
|
||||
_selectedInstanceId = 0;
|
||||
if (_selectedSiteId == 0) return;
|
||||
// Site scoping (CentralUI-002): re-check the claim server-side — a query
|
||||
// string or stale localStorage value could name a site outside the grant.
|
||||
if (!await SiteScope.IsSiteAllowedAsync(_selectedSiteId))
|
||||
{
|
||||
_selectedSiteId = 0;
|
||||
_toast.ShowError("You are not permitted to debug instances on that site.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
|
||||
.Where(i => i.State == InstanceState.Enabled)
|
||||
.ToList();
|
||||
_siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_toast.ShowError($"Failed to load instances: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
finally
|
||||
{
|
||||
_toast.ShowError($"Failed to load instances: {ex.Message}");
|
||||
_instancesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
loaded once per site — the picker treats a new list as a site change. *@
|
||||
<InstancePicker Areas="_areas" Instances="_instances"
|
||||
SelectedInstanceId="_selectedInstanceId" SelectedInstanceIdChanged="OnInstancePickedAsync"
|
||||
Disabled="@(_running || _selectedSiteId == 0)"
|
||||
Disabled="@(_running || _selectedSiteId == 0 || _instancesLoading)"
|
||||
Placeholder="Search instances…" DataTestPrefix="flow-instance" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
|
||||
+33
-9
@@ -122,6 +122,16 @@ public partial class FlowDebugger : ComponentBase, IDisposable
|
||||
|
||||
/// <summary>The selected site's areas; the picker nests them into its tree. See <see cref="_instances"/>.</summary>
|
||||
private List<Area> _areas = [];
|
||||
|
||||
/// <summary>
|
||||
/// True while <see cref="LoadInstancesAsync"/> is fetching a site's instances and areas, and
|
||||
/// the sole reason the picker is disabled for that window. Blazor renders the moment the load
|
||||
/// hits its first await, and by then <see cref="_selectedSiteId"/> is already the NEW site
|
||||
/// while the two lists are still the OLD ones — so without this the field is enabled over
|
||||
/// stale data, and anything typed into it is wiped when the lists land (the picker reads a new
|
||||
/// list object as "the site changed" and clears its text).
|
||||
/// </summary>
|
||||
private bool _instancesLoading;
|
||||
private List<FlowScriptOption> _scripts = [];
|
||||
|
||||
private int _selectedMethodId;
|
||||
@@ -419,19 +429,33 @@ public partial class FlowDebugger : ComponentBase, IDisposable
|
||||
await PredictAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the selected site's instances and areas into the picker's two lists, holding
|
||||
/// <see cref="_instancesLoading"/> for the whole round trip so the field cannot be typed
|
||||
/// into while it is still showing the previous site's tree. The flag is cleared in a
|
||||
/// <c>finally</c>: a repository fault must not leave the picker disabled forever.
|
||||
/// </summary>
|
||||
private async Task LoadInstancesAsync()
|
||||
{
|
||||
if (_selectedSiteId <= 0)
|
||||
_instancesLoading = true;
|
||||
try
|
||||
{
|
||||
_instances = [];
|
||||
_areas = [];
|
||||
return;
|
||||
}
|
||||
if (_selectedSiteId <= 0)
|
||||
{
|
||||
_instances = [];
|
||||
_areas = [];
|
||||
return;
|
||||
}
|
||||
|
||||
_instances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
|
||||
.OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
// The picker nests these itself (InstanceTreeBuilder), so order does not matter here.
|
||||
_areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
|
||||
_instances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
|
||||
.OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
// The picker nests these itself (InstanceTreeBuilder), so order does not matter here.
|
||||
_areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_instancesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user