From b1571f0d440326f4c0213c607bf3bcbc37504474 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 19 Aug 2026 15:44:33 -0400 Subject: [PATCH 1/5] fix bugs and unclear messages and use Database Progress modal for external database fields update --- .../SystemCenter/SystemCenterController.cs | 128 ++++++++++++-- .../ScheduledProcesses/ScheduledExtDBTask.cs | 159 ++++++++++++------ .../CommonComponents/ExtDBTaskStatusModal.tsx | 6 +- .../SystemCenter/ExternalDB/ExternalDB.tsx | 42 +---- 4 files changed, 236 insertions(+), 99 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs b/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs index 9f8e3b3b2..bd6d5fa77 100644 --- a/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs +++ b/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs @@ -1836,20 +1836,72 @@ public static AppStatus CreateErrorStatus(InvalidOperationException e) } [HttpGet, Route("UnscheduledUpdate/{recordID:int}")] - public IHttpActionResult UnscheduledUpdate(int recordID) + public HttpResponseMessage UnscheduledUpdate(int recordID) { + HttpResponseMessage response = Request.CreateResponse(); + if (!PostAuthCheck()) - return Unauthorized(); + { + response.StatusCode = HttpStatusCode.Unauthorized; + return response; + } + ExternalDatabases? extDB; using (AdoDataConnection connection = ConnectionFactory()) { - ExternalDatabases? extDB = new TableOperations(connection).QueryRecordWhere("ID = {0}", recordID); + extDB = new TableOperations(connection).QueryRecordWhere("ID = {0}", recordID); if (extDB == null) - return NotFound(); - - return Ok(ScheduledExtDBTask.Run(extDB)); + { + response.StatusCode = HttpStatusCode.NotFound; + return response; + } } + + ConcurrentQueue logQueue = new ConcurrentQueue(); + + Task runTask = Task.Run(() => ScheduledExtDBTask.Run(extDB, logQueue)); + + response.Content = new PushStreamContent(async (stream, content, context) => + { + try + { + while (!runTask.IsCompleted || !logQueue.IsEmpty) + { + if (!logQueue.TryDequeue(out ExtDBTaskStatus? msg)) + { + await Task.Delay(250); + continue; + } + + byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(msg) + "\n"); + await stream.WriteAsync(bytes, 0, bytes.Length); + await stream.FlushAsync(); + } + + if (runTask.IsFaulted) + { + ExtDBTaskStatus errorMessage = new ExtDBTaskStatus() + { + PercentFinished = 100, + Status = "Error", + Message = runTask.Exception.GetBaseException().Message + }; + + byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(errorMessage) + "\n"); + await stream.WriteAsync(bytes, 0, bytes.Length); + await stream.FlushAsync(); + } + } + finally + { + stream.Close(); + } + }, "application/x-ndjson"); + + response.Headers.CacheControl = new CacheControlHeaderValue() { NoCache = true }; + response.Headers.TransferEncodingChunked = true; + return response; } [HttpGet, Route("UnscheduledUpdate/{recordID:int}/{parentTable}")] @@ -1922,20 +1974,72 @@ public HttpResponseMessage UnscheduledUpdate(int recordID, string parentTable) } [HttpGet, Route("UnscheduledUpdate/{recordID:int}/{parentTable}/{parentID:int}")] - public IHttpActionResult UnscheduledUpdate(int recordID, string parentTable, int parentID) + public HttpResponseMessage UnscheduledUpdate(int recordID, string parentTable, int parentID) { + HttpResponseMessage response = Request.CreateResponse(); + if (!PostAuthCheck()) - return Unauthorized(); + { + response.StatusCode = HttpStatusCode.Unauthorized; + return response; + } + ExternalDatabases? extDB; using (AdoDataConnection connection = ConnectionFactory()) { - ExternalDatabases? extDB = new TableOperations(connection).QueryRecordWhere("ID = {0}", recordID); + extDB = new TableOperations(connection).QueryRecordWhere("ID = {0}", recordID); if (extDB == null) - return NotFound(); - - return Ok(ScheduledExtDBTask.Run(extDB, parentTable, parentID)); + { + response.StatusCode = HttpStatusCode.NotFound; + return response; + } } + + ConcurrentQueue logQueue = new ConcurrentQueue(); + + Task runTask = Task.Run(() => ScheduledExtDBTask.Run(extDB, parentTable, parentID, logQueue)); + + response.Content = new PushStreamContent(async (stream, content, context) => + { + try + { + while (!runTask.IsCompleted || !logQueue.IsEmpty) + { + if (!logQueue.TryDequeue(out ExtDBTaskStatus? msg)) + { + await Task.Delay(250); + continue; + } + + byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(msg) + "\n"); + await stream.WriteAsync(bytes, 0, bytes.Length); + await stream.FlushAsync(); + } + + if (runTask.IsFaulted) + { + ExtDBTaskStatus errorMessage = new ExtDBTaskStatus() + { + PercentFinished = 100, + Status = "Error", + Message = runTask.Exception.GetBaseException().Message + }; + + byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(errorMessage) + "\n"); + await stream.WriteAsync(bytes, 0, bytes.Length); + await stream.FlushAsync(); + } + } + finally + { + stream.Close(); + } + }, "application/x-ndjson"); + + response.Headers.CacheControl = new CacheControlHeaderValue() { NoCache = true }; + response.Headers.TransferEncodingChunked = true; + return response; } } diff --git a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs index 099bb32a0..c3e581afe 100644 --- a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs +++ b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs @@ -49,18 +49,32 @@ public class ScheduledExtDBTask #region [ Member ] public ExternalDatabases ExternalDB { get; set; } public static readonly Func ConnectionFactory = () => new AdoDataConnection("systemSettings"); + // Line segment excluded, special case - public static readonly Type[] CheckedTypes = new Type[] { typeof(Meter), typeof(Location), typeof(Model.Customer), - typeof(Line), typeof(Breaker), typeof(Bus), typeof(CapBank), typeof(Transformer), typeof(CapBankRelay), typeof(DER), typeof(Asset), typeof(Generation), typeof(StationAux), typeof(StationBattery) }; - public static readonly string[] TableNames = { "Meter", "Location", "Customer", "Line", "Breaker", "Bus", "CapBank", "Transformer", "CapBankRelay", "DER", "Asset", "Generation", "StationAux", "StationBattery" }; // temporary + public static readonly Dictionary TableNames = new Dictionary { + {typeof(Meter),"Meter" }, + {typeof(Location), "Location"}, + {typeof(Model.Customer), "Customer"}, + {typeof(Line), "Line"}, + {typeof(Breaker), "Breaker"}, + {typeof(Bus), "Bus"}, + {typeof(CapBank), "CapBank"}, + {typeof(Transformer), "Transformer"}, + {typeof(CapBankRelay), "CapBankRelay"}, + {typeof(DER), "DER"}, + {typeof(Asset), "Asset"}, + {typeof(Generation), "Generation"}, + {typeof(StationAux), "StationAux"}, + {typeof(StationBattery), "StationBattery"} + }; public const string RegexPattern = "[{][^{}]*[}]"; public const string RegexIllegal = @"[\s]"; - private static IDictionary TypeTableNameDict; - private static string FieldCountSQL(string tableName) { + + private static string FieldCountSQL(Type type) { return $@" - SELECT SUM(RecordCount*(AF + XF)) AS CT FROM (SELECT (SELECT COUNT(ID) FROM {tableName}) AS RecordCount, - (SELECT COUNT(ID) FROM AdditionalField WHERE AdditionalField.ParentTable LIKE '{tableName}' AND ExternalDBTableID = extDBTables.ID AND IsKey = 0) AS AF, - (SELECT COUNT(ID) FROM ExternalOpenXDAField WHERE ExternalOpenXDAField.ParentTable LIKE '{tableName}' AND ExternalOpenXDAField.ExternalDBTableID = extDBTables.ID ) AS XF + SELECT SUM(RecordCount*(AF + XF)) AS CT FROM (SELECT (SELECT COUNT(ID) FROM {TableNames[type]}) AS RecordCount, + (SELECT COUNT(ID) FROM AdditionalField WHERE AdditionalField.ParentTable LIKE '{TableNames[type]}' AND ExternalDBTableID = extDBTables.ID AND IsKey = 0) AS AF, + (SELECT COUNT(ID) FROM ExternalOpenXDAField WHERE ExternalOpenXDAField.ParentTable LIKE '{TableNames[type]}' AND ExternalOpenXDAField.ExternalDBTableID = extDBTables.ID ) AS XF FROM extDBTables WHERE ExtDBID = {{0}}) T"; } public class ExtDBTaskStatus @@ -79,16 +93,6 @@ public ScheduledExtDBTask(ExternalDatabases task) ExternalDB = task; } - static ScheduledExtDBTask() - { - TypeTableNameDict = new Dictionary(); - foreach (Type type in CheckedTypes) - { - Type tableOp = typeof(TableOperations<>).MakeGenericType(type); - MethodInfo getTableName = tableOp.GetMethod("GetTableName", BindingFlags.Static | BindingFlags.Public); - TypeTableNameDict.Add(type, (string)getTableName.Invoke(null, new object[] { })); - } - } #endregion #region [ Methods ] @@ -120,6 +124,7 @@ public static int Run(ExternalDatabases extDB, ConcurrentQueue logQueue?.Enqueue(new ExtDBTaskStatus() { RowsAffected = 0, PercentFinished = 100, + RecordsAffected = 0, Status = "Error", Message = $"No tables found connected to external database {extDB.Name}." }); @@ -134,14 +139,13 @@ public static int Run(ExternalDatabases extDB, ConcurrentQueue using (AdoDataConnection externalConnection = GetExternalConnection(extDB)) { // total count += in a foreach Type t - foreach (Type t in CheckedTypes) + foreach (Type t in TableNames.Keys) { - int typeIndex = CheckedTypes.ToList().FindIndex(checkedType => checkedType == t); - string tableName = TableNames[typeIndex]; - totalFields += xdaConnection.ExecuteScalar(FieldCountSQL(tableName), extDB.ID); + string tableName = TableNames[t]; + totalFields += xdaConnection.ExecuteScalar(FieldCountSQL(t), extDB.ID); } - foreach (Type t in CheckedTypes) + foreach (Type t in TableNames.Keys) { foreach (extDBTables extTable in extTables) { @@ -155,7 +159,8 @@ public static int Run(ExternalDatabases extDB, ConcurrentQueue { RowsAffected = rowsAffected, PercentFinished = 100, - Status = "Info", + RecordsAffected = 0, + Status = "Success", Message = $"Finished updating external database {extDB.Name}: {rowsAffected} rows affected." }); return rowsAffected; @@ -163,26 +168,29 @@ public static int Run(ExternalDatabases extDB, ConcurrentQueue } public static int Run(ExternalDatabases extDB, string parentTable, int? parentID = null, ConcurrentQueue logQueue = null) { - using (AdoDataConnection xdaConnection = ConnectionFactory()) + Type tableType; + try { - Type tableType; - try - { - tableType = TypeTableNameDict.First(x => x.Value == parentTable).Key; - } - catch (InvalidOperationException ex) + tableType = TableNames.First(item => item.Value == parentTable).Key; + } + catch (InvalidOperationException ex) + { + Log.Error($"Type {parentTable} is not a recognized xda parent table."); + + logQueue?.Enqueue(new ExtDBTaskStatus() { - Log.Error($"Type {parentTable} is not a recognized xda parent table."); + RowsAffected = 0, + PercentFinished = 100, + RecordsAffected = 0, + Status = "Error", + Message = $"Type {parentTable} is not a recognized xda parent table." + }); + return 0; + } - logQueue?.Enqueue(new ExtDBTaskStatus() - { - RowsAffected = 0, - PercentFinished = 100, - Status = "Error", - Message = $"Type {parentTable} is not a recognized xda parent table." - }); - return 0; - } + using (AdoDataConnection xdaConnection = ConnectionFactory()) + { + TableOperations tblTable = new TableOperations(xdaConnection); IEnumerable extTables = tblTable.QueryRecordsWhere("ExtDBID = {0}", extDB.ID); if (extTables.Count() == 0) @@ -192,6 +200,7 @@ public static int Run(ExternalDatabases extDB, string parentTable, int? parentID { RowsAffected = 0, PercentFinished = 100, + RecordsAffected = 0, Status = "Error", Message = $"No tables found connected to external database {extDB.Name}." }); @@ -204,7 +213,7 @@ public static int Run(ExternalDatabases extDB, string parentTable, int? parentID int rowsAffected = 0; using (AdoDataConnection externalConnection = GetExternalConnection(extDB)) { - string sql = FieldCountSQL(parentTable); + string sql = FieldCountSQL(tableType); int totalFields = xdaConnection.ExecuteScalar(sql, extDB.ID); foreach (extDBTables extTable in extTables) { @@ -217,7 +226,8 @@ public static int Run(ExternalDatabases extDB, string parentTable, int? parentID { RowsAffected = rowsAffected, PercentFinished = 100, - Status = "Info", + RecordsAffected = 0, + Status = "Success", Message = $"Finished updating external database {extDB.Name}: {rowsAffected} rows affected." }); return rowsAffected; @@ -232,9 +242,36 @@ private static int RunOnType(Type type, extDBTables extTable, int totalFields, int tableFieldOffset, ConcurrentQueue logQueue, int? parentID = null) { - var updateMethods = typeof(ScheduledExtDBTask).GetMethod("UpdateData", BindingFlags.Static | BindingFlags.Public); - var typedUpdateMethod = updateMethods.MakeGenericMethod(new[] { type }); - return (int)typedUpdateMethod.Invoke(null, new object[] { extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID }); + if (type == typeof(Meter)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Location)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Model.Customer)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Line)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Breaker)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Bus)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(CapBank)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Transformer)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(CapBankRelay)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(DER)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Asset)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(Generation)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(StationAux)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + else if (type == typeof(StationBattery)) + return UpdateData(extTable, addlFieldsTable, addlValuesTable, xdaFieldTable, context, xdaConnection, extConnection, totalFields, tableFieldOffset, logQueue, parentID); + // shouldn't be possible + throw new ArgumentOutOfRangeException($"{type} is not recognized as an XDA parent table."); } public static int UpdateData(extDBTables extTable, @@ -249,15 +286,18 @@ public static int UpdateData(extDBTables extTable, // Ignore key fields, since those don't make sense to not be auto updated IEnumerable addlFields = addlFieldsTable.QueryRecordsWhere("ParentTable = {0} AND ExternalDBTableID = {1} AND IsKey = 0", table.TableName, extTable.ID); IEnumerable xdaFields = xdaFieldTable.QueryRecordsWhere("ParentTable = {0} AND ExternalDBTableID = {1}", table.TableName, extTable.ID); - // Todo: add external xda table to this, is put off for now + if (!addlFields.Any() && !xdaFields.Any()) return 0; IEnumerable allRecords; if (parentID is not null) allRecords = table.QueryRecordsWhere("ID = {0}", parentID); else allRecords = table.QueryRecords(); int rows = 0; + int updatedRecords = 0; + string userTableName = (table.TableName == "Location" ? "Substation" : table.TableName); foreach (T record in allRecords) { int recordID = GetID(record); + int rowsBeforeRecord = rows; if (recordID == -1) continue; // Should be impossible to trigger without huge overhauling of openXDA DataRowCollection data = RetrieveDataRecord(record, extTable, table, addlFieldsTable, addlValuesTable, context, extConnection); // null means no specific record was found @@ -275,6 +315,7 @@ public static int UpdateData(extDBTables extTable, logQueue?.Enqueue(new ExtDBTaskStatus() { RowsAffected = tableFieldOffset + rows, + RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Warning", Message = $"Additional field with no field in external database found: ID {field.ID}, Name {field.FieldName}, External Table {extTable.TableName}" @@ -313,6 +354,7 @@ public static int UpdateData(extDBTables extTable, logQueue.Enqueue(new ExtDBTaskStatus() { RowsAffected = tableFieldOffset + rows, + RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Warning", Message = $"External OpenXDA field with no field in external database found: ID {field.ID}, Name {field.FieldName}, External Table {extTable.TableName}" @@ -326,6 +368,7 @@ public static int UpdateData(extDBTables extTable, logQueue.Enqueue(new ExtDBTaskStatus() { RowsAffected = tableFieldOffset + rows, + RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Warning", Message = $"External OpenXDA field defined that does not exist on the xda model {field.FieldName} on table {field.ParentTable}" @@ -341,17 +384,20 @@ public static int UpdateData(extDBTables extTable, logQueue.Enqueue(new ExtDBTaskStatus() { RowsAffected = tableFieldOffset + rows, + RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Info", - Message = $"Record updated successfully." + Message = $"{(userTableName)} \"{GetName(record)}\" updated successfully for {rows - rowsBeforeRecord} fields." }); + updatedRecords += 1; } logQueue.Enqueue(new ExtDBTaskStatus() { RowsAffected = tableFieldOffset + rows, + RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Info", - Message = $"Table {table.TableName} updated successfully." + Message = $"Table {userTableName} updated successfully for {updatedRecords} records and {rows} fields." }); return rows; } @@ -436,10 +482,10 @@ private static void DefineAllowedVariables(ExpressionContext context) context.Variables.DefineVariable("key", typeof(string)); context.Variables["key"] = null; // Define all vars that could be pulled from openXDA - foreach(Type type in CheckedTypes) + foreach(Type type in TableNames.Keys) { string tableName; - if (!TypeTableNameDict.TryGetValue(type, out tableName)) + if (!TableNames.TryGetValue(type, out tableName)) { Log.Warn($"Type {type.Name} checked in Scheduled DB Task could not find associated table name."); continue; @@ -560,6 +606,17 @@ public static AdoDataConnection GetExternalConnection(ExternalDatabases extDB) return (int) idObj.GetValue(record); } + private static string GetName(T record) where T: class, new() + { + HashSet nonAssetTypes = new HashSet() { typeof(Meter), typeof(Model.Customer), typeof(Location) }; + string propertyName = "AssetName"; + if (nonAssetTypes.Contains(typeof(T))) + propertyName = "Name"; + PropertyInfo nameObj = record.GetType().GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + if (nameObj is null) return ""; + return (string)nameObj.GetValue(record); + } + private static string RegexReplaceFunction(Match match, ExpressionContext context, List parameters) { try diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ExtDBTaskStatusModal.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ExtDBTaskStatusModal.tsx index f58254700..5cbd2d138 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ExtDBTaskStatusModal.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ExtDBTaskStatusModal.tsx @@ -29,7 +29,7 @@ import { SystemCenter as SC } from '../global'; interface IProps { CallBack: () => void, Record: SystemCenter.Types.DetailedExternalDatabases | undefined, - RecordType: 'Asset' | 'Meter' | 'Location' | 'Customer' | OpenXDA.Types.AssetTypeName, + RecordType?: 'Asset' | 'Meter' | 'Location' | 'Customer' | OpenXDA.Types.AssetTypeName, ParentID?: number, SetStatus: (s: Application.Types.Status) => void } @@ -53,7 +53,7 @@ const ExtDBTaskStatusModal = (props: IProps) => { props.SetStatus('loading'); - const path = `${homePath}api/SystemCenter/ExternalDatabases/UnscheduledUpdate/${props.Record.ID}/${(props.RecordType == 'CapacitorBank' ? 'CapBank' : (props.RecordType == 'CapacitorBankRelay' ? 'CapBankRelay' : props.RecordType))}${props.ParentID === undefined ? '' : "/" + props.ParentID}`; + const path = `${homePath}api/SystemCenter/ExternalDatabases/UnscheduledUpdate/${props.Record.ID}/${(props.RecordType == 'CapacitorBank' ? 'CapBank' : (props.RecordType == 'CapacitorBankRelay' ? 'CapBankRelay' : props.RecordType)) ?? ''}${props.ParentID === undefined ? '' : "/" + props.ParentID}`; const abortController = new AbortController(); @@ -151,7 +151,7 @@ const ExtDBTaskStatusModal = (props: IProps) => {
-
{`${props.RecordType}s updated: ${recordsUpdated}`}
+
{`${props.RecordType ?? 'Record'}s updated: ${recordsUpdated}`}
{`Fields updated: ${fieldsUpdated}`}
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDB.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDB.tsx index ffc812d49..017e968b2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDB.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDB.tsx @@ -28,8 +28,9 @@ import ExternalDBTables from './ExternalDBTables'; import { useAppSelector, useAppDispatch } from '../hooks'; import { ExternalDatabasesSlice } from '../Store/Store'; import { LoadingScreen, Modal, TabSelector, Warning } from '@gpa-gemstone/react-interactive'; -import { Application } from '@gpa-gemstone/application-typings'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { SystemCenter as SC } from '../global'; +import ExtDBTaskStatusModal from '../CommonComponents/ExtDBTaskStatusModal'; declare var homePath: string; declare type Tab = 'info' | 'tables'; @@ -44,7 +45,7 @@ export default function ExternalDB(props: { ID: number, Tab: Tab }) { const [showRemove, setShowRemove] = React.useState(false); const [requestStatus, setRequestStatus] = React.useState('uninitiated'); - const [extDBTaskStatuses, setExtDBTaskStatuses] = React.useState([]); + const [activeUpdate, setActiveUpdate] = React.useState(undefined); const Tabs = [ { Id: "info", Label: "Info" }, @@ -74,32 +75,6 @@ export default function ExternalDB(props: { ID: number, Tab: Tab }) { window.location.href = homePath + 'index.cshtml?name=ByExternalDB'; } - const RequestUpdate = React.useCallback(() => { - setRequestStatus('loading'); - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/ExternalDatabases/UnscheduledUpdate/${record.ID}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: false, - async: true - }); - handle.done((newExtDBTaskStatuses: SC.ExtDBTaskStatus[]) => { - setExtDBTaskStatuses(newExtDBTaskStatuses); - setRequestStatus('idle'); - }); - handle.fail(() => { - setRequestStatus('error'); - }); - return () => { - if (handle != null && handle.abort != null) handle.abort(); - }; - }, [record]); - - const ClosePopup = React.useCallback(() => { - setRequestStatus('uninitiated'); - }, [setRequestStatus]); - if (record == null) return null; return (
@@ -112,7 +87,7 @@ export default function ExternalDB(props: { ID: number, Tab: Tab }) { + onClick={() => setActiveUpdate(record)}>Update Fields
@@ -133,10 +108,11 @@ export default function ExternalDB(props: { ID: number, Tab: Tab }) { Message={'This will permanently delete this External Database and cannot be undone.'} Show={showRemove} Title={'Delete ' + (record?.Name ?? 'External Database')} CallBack={(conf) => { if (conf) Delete(); setShowRemove(false); }} /> - -

{requestStatus === 'idle' ? "Unscheduled update successful." : "Unscheduled Update Failure."}

-
+ { setActiveUpdate(undefined) }} + SetStatus={setRequestStatus} + Record={activeUpdate} + />
) } From 5f6b3b4dca16bc7b42c060bd65ced04c4529b24f Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 20 Aug 2026 15:07:38 -0400 Subject: [PATCH 2/5] update status messages and add warning if multiple rows returned for a record --- .../ScheduledProcesses/ScheduledExtDBTask.cs | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs index c3e581afe..aca66c923 100644 --- a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs +++ b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs @@ -81,9 +81,9 @@ public class ExtDBTaskStatus { public string Message { get; set; } public string Status { get; set; } - public int PercentFinished { get; set; } - public int RowsAffected { get; set; } - public int RecordsAffected { get; set; } + public int PercentFinished { get; set; } + public int RowsAffected { get; set; } + public int RecordsAffected { get; set; } } #endregion @@ -161,7 +161,7 @@ public static int Run(ExternalDatabases extDB, ConcurrentQueue PercentFinished = 100, RecordsAffected = 0, Status = "Success", - Message = $"Finished updating external database {extDB.Name}: {rowsAffected} rows affected." + Message = $"Finished retrieving data from External Database {extDB.Name}: {rowsAffected} Field(s) updated." }); return rowsAffected; } @@ -228,7 +228,7 @@ public static int Run(ExternalDatabases extDB, string parentTable, int? parentID PercentFinished = 100, RecordsAffected = 0, Status = "Success", - Message = $"Finished updating external database {extDB.Name}: {rowsAffected} rows affected." + Message = $"Finished retrieving data from External Database {extDB.Name}: {rowsAffected} Field(s) updated." }); return rowsAffected; } @@ -299,7 +299,8 @@ public static int UpdateData(extDBTables extTable, int recordID = GetID(record); int rowsBeforeRecord = rows; if (recordID == -1) continue; // Should be impossible to trigger without huge overhauling of openXDA - DataRowCollection data = RetrieveDataRecord(record, extTable, table, addlFieldsTable, addlValuesTable, context, extConnection); + int percentComplete = ((tableFieldOffset + rows) / totalFields) * 100; + DataRowCollection data = RetrieveDataRecord(record, extTable, table, addlFieldsTable, addlValuesTable, context, extConnection, logQueue, percentComplete, rows, updatedRecords); // null means no specific record was found if (data is null) continue; foreach (AdditionalField field in addlFields) @@ -337,7 +338,7 @@ public static int UpdateData(extDBTables extTable, { if (fieldValue == addlValue.Value) continue; addlValue.Value = fieldValue; - rows += addlValuesTable.UpdateRecord(addlValue); // message for record update? field update? + rows += addlValuesTable.UpdateRecord(addlValue); } } bool hasXdaChanges = false; @@ -387,7 +388,7 @@ public static int UpdateData(extDBTables extTable, RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Info", - Message = $"{(userTableName)} \"{GetName(record)}\" updated successfully for {rows - rowsBeforeRecord} fields." + Message = $"Successfully updated {userTableName} \"{GetName(record)}\" with data from {extTable.TableName}: {rows - rowsBeforeRecord} Field(s) updated." }); updatedRecords += 1; } @@ -397,7 +398,7 @@ public static int UpdateData(extDBTables extTable, RecordsAffected = updatedRecords, PercentFinished = ((tableFieldOffset + rows) / totalFields) * 100, Status = "Info", - Message = $"Table {userTableName} updated successfully for {updatedRecords} records and {rows} fields." + Message = $"Finished retrieving {userTableName} data from Table {extTable.TableName}: {updatedRecords} {userTableName}(s) and {rows} Field(s) updated." }); return rows; } @@ -442,10 +443,22 @@ public static DataTable RetrieveDataRecordTable(T record, extDBTables extTabl public static DataRowCollection RetrieveDataRecord(T record, extDBTables extTable, TableOperations table, TableOperations addlTable, TableOperations addlValuesTable, - ExpressionContext context, AdoDataConnection externalConnection) where T : class, new() + ExpressionContext context, AdoDataConnection externalConnection, ConcurrentQueue logQueue, int percentComplete, int rowsAffected, int recordsAffected) where T : class, new() { DataRowCollection data = RetrieveDataRecordTable(record, extTable, table, addlTable, addlValuesTable, context, externalConnection)?.Rows; - if (data is null || data.Count != 1) return null; + if (data is null) return null; + if (data.Count != 1) + { + logQueue.Enqueue(new ExtDBTaskStatus() + { + PercentFinished = percentComplete, + RowsAffected = rowsAffected, + RecordsAffected = recordsAffected, + Status = "Warning", + Message = $"Table {extTable.TableName} contains {data.Count} results matching {GetUserRecordType(table.TableName)} {GetName(record)}" + }); + return null; + } return data; } @@ -650,6 +663,9 @@ private static string RegexReplaceFunction(Match match, ExpressionContext contex #region [ Static ] private static readonly ILog Log = LogManager.GetLogger(typeof(ExternalDatabases)); + + private static string GetUserRecordType(string recordType) => (recordType == "Location" ? "Substation" : recordType); + #endregion } From 8a751d431e5cc7ced1bb97619911f4eb42b49d8c Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 20 Aug 2026 15:10:55 -0400 Subject: [PATCH 3/5] only list a record as updated if a field of it has been updated --- .../SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs index aca66c923..808d62f9c 100644 --- a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs +++ b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs @@ -390,7 +390,8 @@ public static int UpdateData(extDBTables extTable, Status = "Info", Message = $"Successfully updated {userTableName} \"{GetName(record)}\" with data from {extTable.TableName}: {rows - rowsBeforeRecord} Field(s) updated." }); - updatedRecords += 1; + if (rows != rowsBeforeRecord) + updatedRecords += 1; } logQueue.Enqueue(new ExtDBTaskStatus() { From 71762b2894521b8d4c87ec41665cb3c8a81cea7e Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 20 Aug 2026 16:46:57 -0400 Subject: [PATCH 4/5] prevent errors from comparing Integer to Null in flee expression --- .../SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs index 808d62f9c..3c752b915 100644 --- a/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs +++ b/Source/Applications/SystemCenter/ScheduledProcesses/ScheduledExtDBTask.cs @@ -34,12 +34,6 @@ using System.Reflection; using System.Data; using GSF.Collections; -using System.Web.Http.Filters; -using Microsoft.Graph.ExternalConnectors; -using System.Data.Common; -using System.Runtime.Remoting.Contexts; -using System.Web.Services.Description; -using System.Web.UI.WebControls; using System.Collections.Concurrent; namespace SystemCenter.ScheduledProcesses @@ -640,7 +634,7 @@ private static string RegexReplaceFunction(Match match, ExpressionContext contex if (context.Variables[variableComponents[0]] is null) return "null"; if (variableComponents.Length > 2 && string.Equals(variableComponents[1], "Field", StringComparison.OrdinalIgnoreCase)) variable = RemoveIllegalCharacters(variableComponents[2]); - string stringExpression = $"if({variable} <> null, {variable}.toString(), null)"; + string stringExpression = $"if({variable}.toString() <> null, {variable}.toString(), null)"; IGenericExpression expression = context.CompileGeneric(stringExpression); string eval = expression.Evaluate(); if (eval is null) return "null"; From adf5ae288c4a7aa81e559e1b4fddc11869b784da Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 09:53:42 -0400 Subject: [PATCH 5/5] find Capacitor Bank and Capacitor Bank Relay additional fields in asset info tab --- .../CommonComponents/AdditionalFieldsProperties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsProperties.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsProperties.tsx index ce934950d..1a699d10e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsProperties.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsProperties.tsx @@ -55,7 +55,7 @@ function AdditionalFieldsProperties(props: IProps): JSX.Element { const filt: Search.IFilter[] = [{ FieldName: 'ParentTable', Operator: '=', - SearchText: props.ParentTable, + SearchText: (props.ParentTable == "CapacitorBank" ? "CapBank" : (props.ParentTable == "CapacitorBankRelay" ? "CapBankRelay" : props.ParentTable)), Type: "string", IsPivotColumn: false }, {