Sources: scope the connector picker to the source type and require it
The Sources dialog listed every connector regardless of kind, so an HA source could be bound to an MQTT broker and saved happily — then never ingest. The picker was also clearable, and null meant opposite things per path: an HA source with no connector is skipped outright, an MQTT one was subscribed on every broker. Filter candidates by the connector kind the source type needs (HA -> HomeAssistant, MQTT/Tasmota -> MqttBroker; Tasmota has no endpoint kind of its own), clear a selection invalidated by a type change, and preselect the sole candidate so the single-broker case is one click. Offer a link to /admin/connectors when none exists rather than an empty dropdown. Save now rejects a missing or mismatched connector: such a source has no connection details and would silently never ingest, so it should not be possible to save it looking configured. Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
This commit is contained in:
@@ -214,20 +214,30 @@ else
|
|||||||
<MudText Typo="Typo.h6">@(_sourceEdit.Id == 0 ? "New source" : "Edit source")</MudText>
|
<MudText Typo="Typo.h6">@(_sourceEdit.Id == 0 ? "New source" : "Edit source")</MudText>
|
||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudSelect T="SourceType" @bind-Value="_sourceEdit.SourceType" Label="Source type" Class="mb-2">
|
<MudSelect T="SourceType" Value="_sourceEdit.SourceType" ValueChanged="OnSourceTypeChanged" Label="Source type" Class="mb-2">
|
||||||
@foreach (var type in Enum.GetValues<SourceType>())
|
@foreach (var type in Enum.GetValues<SourceType>())
|
||||||
{
|
{
|
||||||
<MudSelectItem T="SourceType" Value="type">@type</MudSelectItem>
|
<MudSelectItem T="SourceType" Value="type">@type</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
@if (_sourceEdit.SourceType is SourceType.HomeAssistant or SourceType.Mqtt or SourceType.Tasmota)
|
@if (RequiredEndpointType(_sourceEdit.SourceType) is { } needed)
|
||||||
{
|
{
|
||||||
<MudSelect T="int?" @bind-Value="_sourceEdit.EndpointId" Label="Connector" Clearable="true" Class="mb-2">
|
if (ConnectorsFor(needed).Count == 0)
|
||||||
@foreach (var e in _endpoints)
|
{
|
||||||
{
|
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-2">
|
||||||
<MudSelectItem T="int?" Value="@((int?)e.Id)">@e.Name (@e.Type)</MudSelectItem>
|
No @needed connector yet — <MudLink Href="/admin/connectors">create one</MudLink>
|
||||||
}
|
(set it up once; every source then just picks it).
|
||||||
</MudSelect>
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudSelect T="int?" @bind-Value="_sourceEdit.EndpointId" Label="Connector" Required="true" Class="mb-2">
|
||||||
|
@foreach (var e in ConnectorsFor(needed))
|
||||||
|
{
|
||||||
|
<MudSelectItem T="int?" Value="@((int?)e.Id)">@e.Name</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if (_sourceEdit.SourceType == SourceType.HomeAssistant)
|
@if (_sourceEdit.SourceType == SourceType.HomeAssistant)
|
||||||
{
|
{
|
||||||
@@ -305,6 +315,7 @@ else
|
|||||||
if (source is null)
|
if (source is null)
|
||||||
{
|
{
|
||||||
_sourceEdit = new SourceEdit();
|
_sourceEdit = new SourceEdit();
|
||||||
|
OnSourceTypeChanged(_sourceEdit.SourceType);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -332,6 +343,28 @@ else
|
|||||||
|
|
||||||
private async Task SaveSourceAsync()
|
private async Task SaveSourceAsync()
|
||||||
{
|
{
|
||||||
|
// A live source without a matching connector has no connection details and would silently
|
||||||
|
// never ingest, so refuse it here rather than letting it look configured.
|
||||||
|
if (RequiredEndpointType(_sourceEdit.SourceType) is { } needed)
|
||||||
|
{
|
||||||
|
var selected = _endpoints.FirstOrDefault(e => e.Id == _sourceEdit.EndpointId);
|
||||||
|
if (selected is null)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Pick a {needed} connector for this {_sourceEdit.SourceType} source.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected.Type != needed)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"'{selected.Name}' is a {selected.Type} connector; a {_sourceEdit.SourceType} source needs {needed}.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sourceEdit.EndpointId = null;
|
||||||
|
}
|
||||||
|
|
||||||
var config = new SourceConfig
|
var config = new SourceConfig
|
||||||
{
|
{
|
||||||
EntityId = Trim(_sourceEdit.EntityId),
|
EntityId = Trim(_sourceEdit.EntityId),
|
||||||
@@ -394,6 +427,44 @@ else
|
|||||||
|
|
||||||
private static string? Trim(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
private static string? Trim(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which connector kind a source type needs, or null if it needs none (manual/import/virtual).
|
||||||
|
/// Tasmota has no endpoint kind of its own — it is served by an MQTT broker connector.
|
||||||
|
/// </summary>
|
||||||
|
private static EndpointType? RequiredEndpointType(SourceType sourceType) => sourceType switch
|
||||||
|
{
|
||||||
|
SourceType.HomeAssistant => EndpointType.HomeAssistant,
|
||||||
|
SourceType.Mqtt or SourceType.Tasmota => EndpointType.MqttBroker,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<IngestionEndpoint> ConnectorsFor(EndpointType type) =>
|
||||||
|
_endpoints.Where(e => e.Type == type).ToList();
|
||||||
|
|
||||||
|
// Changing the source type can invalidate the chosen connector (an HA connector cannot serve an
|
||||||
|
// MQTT source), so drop a selection that no longer fits rather than saving a mismatched pair.
|
||||||
|
private void OnSourceTypeChanged(SourceType sourceType)
|
||||||
|
{
|
||||||
|
_sourceEdit.SourceType = sourceType;
|
||||||
|
|
||||||
|
var needed = RequiredEndpointType(sourceType);
|
||||||
|
var selected = _endpoints.FirstOrDefault(e => e.Id == _sourceEdit.EndpointId);
|
||||||
|
if (needed is null || (selected is not null && selected.Type != needed))
|
||||||
|
{
|
||||||
|
_sourceEdit.EndpointId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sole candidate: preselect it, so the common single-broker / single-HA setup is one click.
|
||||||
|
if (needed is not null && _sourceEdit.EndpointId is null)
|
||||||
|
{
|
||||||
|
var candidates = _endpoints.Where(e => e.Type == needed).ToList();
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
{
|
||||||
|
_sourceEdit.EndpointId = candidates[0].Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SourceEdit
|
private sealed class SourceEdit
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user