diff --git a/src/ScadaLink.CentralUI/Components/Forms/OpcUaEndpointEditor.razor b/src/ScadaLink.CentralUI/Components/Forms/OpcUaEndpointEditor.razor
new file mode 100644
index 0000000..c2d2baa
--- /dev/null
+++ b/src/ScadaLink.CentralUI/Components/Forms/OpcUaEndpointEditor.razor
@@ -0,0 +1,150 @@
+@namespace ScadaLink.CentralUI.Components.Forms
+@using ScadaLink.Commons.Types.DataConnections
+@using ScadaLink.Commons.Types.Flattening
+
+
+
@Title
+
+ @if (IsLegacy)
+ {
+
+ This connection was migrated from a legacy format.
+ Review the settings and Save to update.
+
+ }
+
+
+
+
+
+ @RenderFieldError("EndpointUrl")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Timing
+
+
+
Subscription
+
+
+
Heartbeat
+ @if (Config.Heartbeat is null)
+ {
+
+ }
+ else
+ {
+
+
+
+
+ @RenderFieldError("Heartbeat.TagPath")
+
+
+
+
+ @RenderFieldError("Heartbeat.MaxSilenceSeconds")
+
+
+
+
+
+ }
+
+
+@code {
+ [Parameter, EditorRequired] public OpcUaEndpointConfig Config { get; set; } = default!;
+ [Parameter] public string Title { get; set; } = "Endpoint";
+ [Parameter] public string IdPrefix { get; set; } = "endpoint";
+ [Parameter] public bool IsLegacy { get; set; }
+ [Parameter] public ValidationResult? Errors { get; set; }
+
+ private void EnableHeartbeat() =>
+ Config.Heartbeat = new OpcUaHeartbeatConfig();
+
+ private RenderFragment? RenderFieldError(string field)
+ {
+ var match = Errors?.Errors.FirstOrDefault(e =>
+ e.EntityName != null
+ && (e.EntityName == field || e.EntityName.EndsWith("." + field)));
+ return match is null
+ ? null
+ : @