From 9f182ca267395836663fc253f4f85042037ca2a8 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 13 Apr 2026 11:29:09 -0400 Subject: [PATCH 01/16] add filewatcher tab with base functionality --- .../Controllers/OpenXDA/OpenXDAControllers.cs | 27 +- .../TSX/SystemCenter/AppHost/FileWatcher.tsx | 234 ++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 6a61a4ab5..3e90d4267 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs @@ -103,7 +103,32 @@ public class EventTypeAssetTypeController : ModelController [RoutePrefix("api/OpenXDA/DataOperation")] public class DataOperationController : ModelController { } [RoutePrefix("api/OpenXDA/DataOperationFailure")] - public class DataOperationFailureController : ModelController { } + public class DataOperationFailureController : ModelController { + + [Route("RecentFailures"), HttpPost] + public IHttpActionResult RecentFailures([FromBody] PostData postData) + { + using DataTable value = GetSearchResults(postData, 0); + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + foreach (DataRow row in value.Rows) + { + TableOperations dataFileTbl = new TableOperations(connection); + openXDA.Model.DataFile dataFile = dataFileTbl.QueryRecordWhere("FileGroupID = {0}", row.Field("FileGroupID")); + } + } + int num = CountSearchResults(postData); + int valueOrDefault = Take.GetValueOrDefault(50); + return Ok(new PagedResults + { + Data = JsonConvert.SerializeObject(value), + RecordsPerPage = valueOrDefault, + TotalRecords = num, + NumberOfPages = (num + valueOrDefault - 1) / valueOrDefault + }); + } + + } [RoutePrefix("api/OpenXDA/DataReader")] public class DataReaderController : ModelController { } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx new file mode 100644 index 000000000..359c29fbb --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx @@ -0,0 +1,234 @@ +//****************************************************************************************************** +// FileWatcher.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 04/09/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react' +import { OpenXDA, Application , SystemCenter as SC} from '@gpa-gemstone/application-typings'; +import { Table, Paging, Column } from '@gpa-gemstone/react-table' +import { Plot, Bar } from '@gpa-gemstone/react-graph' +import StatusGroup from './StatusGroup' +import { LoadingScreen } from '@gpa-gemstone/react-interactive'; +import moment from 'moment' + + +const FileWatcher = (props: {}) => { + + const [sortField, setSortField] = React.useState('ID') + const [ascending, setAscending] = React.useState(false) + const [plotWidth, setPlotWidth] = React.useState(100); + const [plotHeight, setPlotHeight] = React.useState(400); + const [hoveredItem, setHoveredItem] = React.useState(null) + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState() + const [totalFailurePages, setTotalFailurePages] = React.useState() + const [dataFile, setDataFile] = React.useState([]) + const [dataOperationFailure, setDataOperationFailure] = React.useState([]) + const [status, setStatus] = React.useState('uninitiated') + const [timeframe, setTimeframe] = React.useState<[number, number]>([null, null]) + + const rowRef = React.useRef(null); + + + const bars = React.useMemo(() => { + const timestamps = [] + dataFile.forEach((df) => { + if (!(timestamps.includes(df.DataStartTime))) { + timestamps.push(df.DataStartTime) + } + if (!(timestamps.includes(df.ProcessingEndTime))) { + timestamps.push(df.ProcessingEndTime) + } + }) + timestamps.sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}) + const bars = {} + timestamps.forEach((ts) => { bars[ts] = 0 }) + dataFile.forEach((df) => { + // add 1 to every timestamp from start to end, start inclusive, end exclusive + timestamps.forEach((ts) => { moment(ts).valueOf() >= moment(df.DataStartTime).valueOf() && moment(ts).valueOf() < moment(df.ProcessingEndTime).valueOf() ? bars[ts]++ : null }) + }) + console.log(bars) + return bars + }, [dataFile]) + + // set plot dimensions + React.useLayoutEffect(() => { + setPlotHeight(rowRef?.current?.offsetHeight ?? 400) + setPlotWidth((rowRef?.current?.offsetWidth ?? 130) - 30) + }); + + + React.useEffect(() => { + if (dataFile.length == 0) + return; + const startTime = Math.min(...dataFile.map((i) => moment(i.DataStartTime).valueOf())) + const endTime = moment.now().valueOf() + setTimeframe([startTime, endTime]) + }, [dataFile]) + + React.useEffect(() => { + setStatus('loading') + getFileGroups() + getDataOperationFailure() + setStatus('idle') + }, [sortField, ascending, page]) + + function getFileGroups() { + const h = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ Searches: [], OrderBy: sortField, Ascending: ascending }), + }); + + h.done((d) => { + setDataFile(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages) + setStatus('idle') + }).fail(() => { + setStatus('error') + }) + + return function cleanup() { + if (h.abort != null) + h.abort(); + } + } + + function getDataOperationFailure() { + const h = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ Searches: [], OrderBy: 'TimeOfFailure', Ascending: false }), + }); + + h.done((d) => { + setDataOperationFailure(JSON.parse(d.Data)) + setTotalFailurePages(d.NumberOfPages) + setStatus('idle') + }).fail(() => { + setStatus('error') + }) + + return function cleanup() { + if (h.abort != null) + h.abort(); + } + } + + + return ( +
+ + {status === 'idle' && dataFile !== null ? <> +
+
+ + {Object.keys(bars).sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}).map((bar, i, array) => ( + + ))} + +
+
+ + Data={dataFile} + SortKey={sortField} + Ascending={ascending} + KeySelector={(item) => item.ID} + OnSort={(d) => { + if (d.colField == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + > + + Key={'FilePath'} + AllowSort={true} + Field={'FilePath'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > + FilePath + + + Key={'DataStartTime'} + AllowSort={true} + Field={'DataStartTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > + Data Start Time + + + Key={'ProcessingEndTime'} + AllowSort={true} + Field={'ProcessingEndTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > + Processing End Time + + + + setPage(p - 1)} /> +
+
+
+ { return { Name: 'Failure:', Status: 'Error', Details: [{Status: 'Error', Description: d.Log}] }})} + HoveredItem={hoveredItem} + SetHoveredItem={setHoveredItem} + Name={'events'} + /> +
+ + : null} +
) +} + + +export default FileWatcher \ No newline at end of file From 3ce173cbc24af268cb82a76207c5679bbe371a51 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 14 Apr 2026 10:21:06 -0400 Subject: [PATCH 02/16] improve styling and layout --- .../Controllers/OpenXDA/OpenXDAControllers.cs | 3 +++ .../TSX/SystemCenter/AppHost/FileWatcher.tsx | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 3e90d4267..44bc65e93 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs @@ -36,6 +36,7 @@ using openXDA.Model.SystemCenter; using PQView.Model; using System; +using System.IO; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -109,12 +110,14 @@ public class DataOperationFailureController : ModelController dataFileTbl = new TableOperations(connection); openXDA.Model.DataFile dataFile = dataFileTbl.QueryRecordWhere("FileGroupID = {0}", row.Field("FileGroupID")); + row["dataFileName"] = Path.GetFileName(dataFile.FilePath); } } int num = CountSearchResults(postData); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx index 359c29fbb..e669787a6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx @@ -29,6 +29,9 @@ import StatusGroup from './StatusGroup' import { LoadingScreen } from '@gpa-gemstone/react-interactive'; import moment from 'moment' +interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { + dataFileName: string +} const FileWatcher = (props: {}) => { @@ -41,10 +44,10 @@ const FileWatcher = (props: {}) => { const [totalPages, setTotalPages] = React.useState() const [totalFailurePages, setTotalFailurePages] = React.useState() const [dataFile, setDataFile] = React.useState([]) - const [dataOperationFailure, setDataOperationFailure] = React.useState([]) + const [dataOperationFailure, setDataOperationFailure] = React.useState([]) const [status, setStatus] = React.useState('uninitiated') const [timeframe, setTimeframe] = React.useState<[number, number]>([null, null]) - + const [yRange, setYRange] = React.useState<[number, number]>([0, 50]) const rowRef = React.useRef(null); @@ -65,7 +68,7 @@ const FileWatcher = (props: {}) => { // add 1 to every timestamp from start to end, start inclusive, end exclusive timestamps.forEach((ts) => { moment(ts).valueOf() >= moment(df.DataStartTime).valueOf() && moment(ts).valueOf() < moment(df.ProcessingEndTime).valueOf() ? bars[ts]++ : null }) }) - console.log(bars) + setYRange([Math.min(...Object.values(bars) as number[]), Math.max(...Object.values(bars) as number[])]) return bars }, [dataFile]) @@ -143,19 +146,21 @@ const FileWatcher = (props: {}) => { return ( -
+
{status === 'idle' && dataFile !== null ? <> -
-
+
+
{Object.keys(bars).sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}).map((bar, i, array) => ( @@ -168,7 +173,7 @@ const FileWatcher = (props: {}) => { ))}
-
+
Data={dataFile} SortKey={sortField} @@ -219,7 +224,7 @@ const FileWatcher = (props: {}) => {
{ return { Name: 'Failure:', Status: 'Error', Details: [{Status: 'Error', Description: d.Log}] }})} + StatusItems={dataOperationFailure.map((d) => { return { Name: `${d.TimeOfFailure}: ${d.dataFileName}`, Status: 'Error', Details: [{Status: 'Error', Description: d.Log}] }})} HoveredItem={hoveredItem} SetHoveredItem={setHoveredItem} Name={'events'} From 9eb10f2b070a60a80670518dd240e931acca9dbc Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 14 Apr 2026 10:45:28 -0400 Subject: [PATCH 03/16] add Y Axis label --- .../wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx index e669787a6..e4f031aeb 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx @@ -161,6 +161,7 @@ const FileWatcher = (props: {}) => { yZoom={true} Ymin={0} Ymax={100} + Ylabel={'Files Queued' } showDateOnTimeAxis={true} > {Object.keys(bars).sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}).map((bar, i, array) => ( From 0007d2d4102807f11ccb7b22bd974a57b0ec4826 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 27 Apr 2026 12:55:45 -0400 Subject: [PATCH 04/16] change file watcher to files processed, with changes to data --- .../SystemCenter/Model/DataFile.cs | 23 +++- .../{FileWatcher.tsx => FilesProcessed.tsx} | 118 ++++++++++-------- 2 files changed, 87 insertions(+), 54 deletions(-) rename Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/{FileWatcher.tsx => FilesProcessed.tsx} (71%) diff --git a/Source/Applications/SystemCenter/Model/DataFile.cs b/Source/Applications/SystemCenter/Model/DataFile.cs index 59da4a789..ad8ef9c79 100644 --- a/Source/Applications/SystemCenter/Model/DataFile.cs +++ b/Source/Applications/SystemCenter/Model/DataFile.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Net; using System.Net.Http; @@ -47,6 +48,7 @@ namespace SystemCenter.Model SELECT DataFile.*, FileGroup.DataStartTime, + FileGroup.ProcessingStartTime, FileGroup.ProcessingEndTime, FileGroup.MeterID, FileGroup.ProcessingStatus AS ProcessingState @@ -59,6 +61,7 @@ public class DataFile : openXDA.Model.DataFile { [ParentKey(typeof(Meter))] public int MeterID { get; set; } + public DateTime ProcessingStartTime { get; set; } [DefaultSortOrder(false)] public DateTime ProcessingEndTime { get; set; } public DateTime DataStartTime { get; set; } @@ -206,7 +209,25 @@ public IHttpActionResult Download(int id) } } - + [Route("AggregateRecentlyProcessedFiles"), HttpGet] + public IHttpActionResult AggregateRecentlyProcessedFiles() + { + String sqlQuery = @" + SELECT + FORMAT (FileGroup.ProcessingStartTime, 'yyyy-MM-dd HH') AS Hour, + COUNT(DataFile.ID) as Count + FROM + openXDA.dbo.DataFile JOIN openXDA.dbo.FileGroup ON DataFile.FileGroupID = FileGroup.ID + WHERE + FileGroup.ProcessingStartTime > DATEADD(HOUR, DATEDIFF(HOUR, 0, GETDATE()) - 48, 0) + GROUP BY FORMAT (FileGroup.ProcessingStartTime, 'yyyy-MM-dd HH');"; + DataTable result; + using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + { + result = connection.RetrieveData(sqlQuery); + } + return Ok(result); + } } } \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx similarity index 71% rename from Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx rename to Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index e4f031aeb..5066da140 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FileWatcher.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -33,7 +33,12 @@ interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure dataFileName: string } -const FileWatcher = (props: {}) => { +interface IAggregateProcessedFile { + Hour: string, + Count: number +} + +const FilesProcessed = (props: {}) => { const [sortField, setSortField] = React.useState('ID') const [ascending, setAscending] = React.useState(false) @@ -46,31 +51,10 @@ const FileWatcher = (props: {}) => { const [dataFile, setDataFile] = React.useState([]) const [dataOperationFailure, setDataOperationFailure] = React.useState([]) const [status, setStatus] = React.useState('uninitiated') - const [timeframe, setTimeframe] = React.useState<[number, number]>([null, null]) - const [yRange, setYRange] = React.useState<[number, number]>([0, 50]) + const [timeframe, setTimeframe] = React.useState<[number, number]>([moment().subtract(48, 'hour').startOf('hour').valueOf(), moment().add(1, 'hour').startOf('hour').valueOf()]) + const [yMax, setYMax] = React.useState(0) const rowRef = React.useRef(null); - - - const bars = React.useMemo(() => { - const timestamps = [] - dataFile.forEach((df) => { - if (!(timestamps.includes(df.DataStartTime))) { - timestamps.push(df.DataStartTime) - } - if (!(timestamps.includes(df.ProcessingEndTime))) { - timestamps.push(df.ProcessingEndTime) - } - }) - timestamps.sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}) - const bars = {} - timestamps.forEach((ts) => { bars[ts] = 0 }) - dataFile.forEach((df) => { - // add 1 to every timestamp from start to end, start inclusive, end exclusive - timestamps.forEach((ts) => { moment(ts).valueOf() >= moment(df.DataStartTime).valueOf() && moment(ts).valueOf() < moment(df.ProcessingEndTime).valueOf() ? bars[ts]++ : null }) - }) - setYRange([Math.min(...Object.values(bars) as number[]), Math.max(...Object.values(bars) as number[])]) - return bars - }, [dataFile]) + const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) // set plot dimensions React.useLayoutEffect(() => { @@ -78,22 +62,37 @@ const FileWatcher = (props: {}) => { setPlotWidth((rowRef?.current?.offsetWidth ?? 130) - 30) }); - - React.useEffect(() => { - if (dataFile.length == 0) - return; - const startTime = Math.min(...dataFile.map((i) => moment(i.DataStartTime).valueOf())) - const endTime = moment.now().valueOf() - setTimeframe([startTime, endTime]) - }, [dataFile]) - React.useEffect(() => { setStatus('loading') getFileGroups() getDataOperationFailure() + getAggregateRecentlyProcessedFiles() setStatus('idle') }, [sortField, ascending, page]) + function getAggregateRecentlyProcessedFiles() { + const h = $.ajax({ + type: "GET", + url: `${homePath}api/OpenXDA/DataFile/AggregateRecentlyProcessedFiles`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + }) + + h.done((d) => { + setAggregateProcessedFiles(d) + setYMax(Math.max(...d?.map((c) => {return c.Count}))) + }).fail(() => { + setStatus('error') + }) + return function cleanup() { + if (h.abort != null) + h.abort(); + + } + } + function getFileGroups() { const h = $.ajax({ type: "POST", @@ -102,7 +101,7 @@ const FileWatcher = (props: {}) => { dataType: 'json', cache: false, async: true, - data: JSON.stringify({ Searches: [], OrderBy: sortField, Ascending: ascending }), + data: JSON.stringify({ Searches: [{ FieldName: 'ProcessingStartTime', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS'), Operator: '>', Type: 'datetime' }], OrderBy: sortField, Ascending: ascending }), }); h.done((d) => { @@ -127,7 +126,7 @@ const FileWatcher = (props: {}) => { dataType: 'json', cache: false, async: true, - data: JSON.stringify({ Searches: [], OrderBy: 'TimeOfFailure', Ascending: false }), + data: JSON.stringify({ Searches: [{ FieldName: 'TimeOfFailure', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS'), Operator: '>', Type: 'datetime' }], OrderBy: 'TimeOfFailure', Ascending: false }), }); h.done((d) => { @@ -155,23 +154,27 @@ const FileWatcher = (props: {}) => { height={plotHeight} width={plotWidth} defaultTdomain={timeframe} - defaultYdomain={yRange} + defaultYdomain={[0, yMax]} onTDomainChange={setTimeframe} - zoom={true} - yZoom={true} + zoom={false} + yZoom={false} + xZoom={false} + Tmin={timeframe[0]} + Tmax={timeframe[1]} Ymin={0} - Ymax={100} - Ylabel={'Files Queued' } - showDateOnTimeAxis={true} + Ymax={yMax} // should be dynamic + Ylabel={'Files Queued'} > - {Object.keys(bars).sort((a, b) => { return moment(a).valueOf() - moment(b).valueOf()}).map((bar, i, array) => ( - - ))} + {aggregateProcessedFiles.length == 0 ? null : + aggregateProcessedFiles.map((a, i) => { + return + + })}
@@ -208,6 +211,15 @@ const FileWatcher = (props: {}) => { > Data Start Time + + Key={'ProcessingStartTime'} + AllowSort={true} + Field={'ProcessingStartTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > + Processing Start Time + Key={'ProcessingEndTime'} AllowSort={true} @@ -223,12 +235,12 @@ const FileWatcher = (props: {}) => {
- { return { Name: `${d.TimeOfFailure}: ${d.dataFileName}`, Status: 'Error', Details: [{Status: 'Error', Description: d.Log}] }})} HoveredItem={hoveredItem} SetHoveredItem={setHoveredItem} - Name={'events'} + Name={'Data Operation Failures'} />
@@ -237,4 +249,4 @@ const FileWatcher = (props: {}) => { } -export default FileWatcher \ No newline at end of file +export default FilesProcessed \ No newline at end of file From 91e10a9dc77b416b2ef77111f3096da8a05c884f Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 27 Apr 2026 15:40:33 -0400 Subject: [PATCH 05/16] add file name to system center data file --- .../Controllers/OpenXDA/OpenXDAControllers.cs | 4 +- .../SystemCenter/Model/DataFile.cs | 40 ++++++++++++++ .../SystemCenter/AppHost/FilesProcessed.tsx | 52 +++++++++++-------- .../Scripts/TSX/SystemCenter/global.d.ts | 1 + 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 44bc65e93..05d53157f 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs @@ -110,14 +110,14 @@ public class DataOperationFailureController : ModelController dataFileTbl = new TableOperations(connection); openXDA.Model.DataFile dataFile = dataFileTbl.QueryRecordWhere("FileGroupID = {0}", row.Field("FileGroupID")); - row["dataFileName"] = Path.GetFileName(dataFile.FilePath); + row["DataFileName"] = Path.GetFileName(dataFile.FilePath); } } int num = CountSearchResults(postData); diff --git a/Source/Applications/SystemCenter/Model/DataFile.cs b/Source/Applications/SystemCenter/Model/DataFile.cs index ad8ef9c79..cf68c39fe 100644 --- a/Source/Applications/SystemCenter/Model/DataFile.cs +++ b/Source/Applications/SystemCenter/Model/DataFile.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.Data; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -66,6 +67,8 @@ public class DataFile : openXDA.Model.DataFile public DateTime ProcessingEndTime { get; set; } public DateTime DataStartTime { get; set; } public int ProcessingState { get; set; } + [NonRecordField] + public string FileName => Path.GetFileName(FilePath); } [RoutePrefix("api/OpenXDA/DataFile")] @@ -228,6 +231,43 @@ public IHttpActionResult AggregateRecentlyProcessedFiles() } return Ok(result); } + + [Route("PagedResults"), HttpPost] + public override IHttpActionResult GetPagedList([FromBody] PostData postData, int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + using DataTable table = GetSearchResults(postData, page); + DataFile[] results = table + .AsEnumerable() + .Select(row => new DataFile() + { + CreationTime = row.Field("CreationTime"), + ID = row.Field("ID"), + FileGroupID = row.Field("FileGroupID"), + FilePath = row.Field("FilePath"), + FilePathHash = row.Field("FilePathHash"), + FileSize = row.Field("FileSize"), + LastWriteTime = row.Field("LastWriteTime"), + LastAccessTime = row.Field("LastAccessTime"), + MeterID = row.Field("MeterID"), + DataStartTime = row.Field("DataStartTime"), + ProcessingEndTime = row.Field("ProcessingEndTime"), + ProcessingState = row.Field("ProcessingState"), + ProcessingStartTime = row.Field("ProcessingStartTime") + }).ToArray(); + int recordCount = CountSearchResults(postData); + int recordPerPage = Take ?? 50; + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(results), + RecordsPerPage = recordPerPage, + TotalRecords = recordCount, + NumberOfPages = (recordCount + recordPerPage - 1) / recordPerPage + }); + } + } } \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 5066da140..3b990ef74 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -22,15 +22,15 @@ //****************************************************************************************************** import * as React from 'react' -import { OpenXDA, Application , SystemCenter as SC} from '@gpa-gemstone/application-typings'; +import { OpenXDA, Application} from '@gpa-gemstone/application-typings'; import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' -import StatusGroup from './StatusGroup' import { LoadingScreen } from '@gpa-gemstone/react-interactive'; +import { SystemCenter as SC } from '../global'; import moment from 'moment' interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { - dataFileName: string + DataFileName: string } interface IAggregateProcessedFile { @@ -40,15 +40,14 @@ interface IAggregateProcessedFile { const FilesProcessed = (props: {}) => { - const [sortField, setSortField] = React.useState('ID') + const [sortField, setSortField] = React.useState('ID') const [ascending, setAscending] = React.useState(false) const [plotWidth, setPlotWidth] = React.useState(100); const [plotHeight, setPlotHeight] = React.useState(400); - const [hoveredItem, setHoveredItem] = React.useState(null) const [page, setPage] = React.useState(0); const [totalPages, setTotalPages] = React.useState() const [totalFailurePages, setTotalFailurePages] = React.useState() - const [dataFile, setDataFile] = React.useState([]) + const [dataFile, setDataFile] = React.useState([]) const [dataOperationFailure, setDataOperationFailure] = React.useState([]) const [status, setStatus] = React.useState('uninitiated') const [timeframe, setTimeframe] = React.useState<[number, number]>([moment().subtract(48, 'hour').startOf('hour').valueOf(), moment().add(1, 'hour').startOf('hour').valueOf()]) @@ -178,7 +177,7 @@ const FilesProcessed = (props: {}) => {
- + Data={dataFile} SortKey={sortField} Ascending={ascending} @@ -193,39 +192,48 @@ const FilesProcessed = (props: {}) => { } }} > - - Key={'FilePath'} + + Key={'FileName'} AllowSort={true} - Field={'FilePath'} + Field={'FileName'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} > - FilePath + File Name - + Key={'DataStartTime'} AllowSort={true} Field={'DataStartTime'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} > Data Start Time - + Key={'ProcessingStartTime'} AllowSort={true} Field={'ProcessingStartTime'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} > Processing Start Time - + Key={'ProcessingEndTime'} AllowSort={true} Field={'ProcessingEndTime'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} > Processing End Time @@ -235,13 +243,15 @@ const FilesProcessed = (props: {}) => {
- { return { Name: `${d.TimeOfFailure}: ${d.dataFileName}`, Status: 'Error', Details: [{Status: 'Error', Description: d.Log}] }})} - HoveredItem={hoveredItem} - SetHoveredItem={setHoveredItem} - Name={'Data Operation Failures'} - /> + {dataOperationFailure.map((e, i) => { + return
+

{e.DataFileName}

+

{e.TimeOfFailure}

+

{e.Log}

+

{e.StackTrace}

+
+ }) + }
: null} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts index 14afb7e59..0e83755df 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts @@ -48,6 +48,7 @@ export namespace SystemCenter { MICStatus: 'Error' | 'Warning' | '', MiMDStatus: 'Error' | 'Warning' | '', LastConfigChange: string, DQStatus: 'Error' | 'Warning' | '' } + interface DataFile extends GemstoneXDA.Types.DataFile { ProcessingStartTime: string, FileName: string } interface OpenMICDailyStatistic { ID: number, Date: string, Meter: string, LastSuccessfulConnection: string, LastUnsuccessfulConnection: string, LastUnsuccessfulConnectionExplanation: string, TotalConnections: number, TotalUnsuccessfulConnections: number, TotalSuccessfulConnections: number } interface MiMDDailyStatistic { ID: number, Date: string, Meter: string, LastSuccessfulFileProcessed: string, LastUnsuccessfulFileProcessed: string, LastUnsuccessfulFileProcessedExplanation: string, TotalFilesProcessed: number, TotalUnsuccessfulFilesProcessed: number, TotalSuccessfulFilesProcessed: number, ConfigChanges: number, DiagnosticAlarms: number, ComplianceIssues: number, LastConfigFileChange: string } interface OpenXDADailyStatistic { ID: number, Date: string, Meter: string, LastSuccessfulFileProcessed: string, LastUnsuccessfulFileProcessed: string, LastUnsuccessfulFileProcessedExplanation: string, TotalFilesProcessed: number, TotalUnsuccessfulFilesProcessed: number, TotalSuccessfulFilesProcessed: number, TotalEmailsSent: number, AverageDownloadLatency: number, AverageProcessingStartLatency: number, AverageProcessingEndLatency: number, AverageEmailLatency: number, AverageTotalProcessingLatency: number, AverageTotalEmailLatency: number } From 2d519439054cdf45b491dee7bbf640e1beedd430 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 28 Apr 2026 10:38:35 -0400 Subject: [PATCH 06/16] move Data operation failures away from using StatusGroup --- .../SystemCenter/AppHost/FilesProcessed.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 3b990ef74..bf5bc2e8a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -27,6 +27,7 @@ import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' import { LoadingScreen } from '@gpa-gemstone/react-interactive'; import { SystemCenter as SC } from '../global'; +import Reason from '../CommonComponents/Reason'; import moment from 'moment' interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { @@ -243,12 +244,28 @@ const FilesProcessed = (props: {}) => {
+
+ Data Operation Failures : {dataOperationFailure.map((e, i) => { - return
-

{e.DataFileName}

-

{e.TimeOfFailure}

-

{e.Log}

-

{e.StackTrace}

+ return
+
{e.DataFileName}
+
+ {moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')} +
+
+ Log: + +
+
+ Stack Trace: + +
}) } From 9f86779107e0d23a9c41d2d6831b4eafc317090e Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 30 Apr 2026 10:02:06 -0400 Subject: [PATCH 07/16] add stack trace and log to data operation failures --- .../SystemCenter/AppHost/FilesProcessed.tsx | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index bf5bc2e8a..0ae565a7f 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -22,12 +22,12 @@ //****************************************************************************************************** import * as React from 'react' -import { OpenXDA, Application} from '@gpa-gemstone/application-typings'; +import { OpenXDA, Application } from '@gpa-gemstone/application-typings'; import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' -import { LoadingScreen } from '@gpa-gemstone/react-interactive'; +import { LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'; +import { ToolTip } from '@gpa-gemstone/react-forms'; import { SystemCenter as SC } from '../global'; -import Reason from '../CommonComponents/Reason'; import moment from 'moment' interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { @@ -48,6 +48,7 @@ const FilesProcessed = (props: {}) => { const [page, setPage] = React.useState(0); const [totalPages, setTotalPages] = React.useState() const [totalFailurePages, setTotalFailurePages] = React.useState() + const [hovered, setHovered] = React.useState('') const [dataFile, setDataFile] = React.useState([]) const [dataOperationFailure, setDataOperationFailure] = React.useState([]) const [status, setStatus] = React.useState('uninitiated') @@ -55,6 +56,8 @@ const FilesProcessed = (props: {}) => { const [yMax, setYMax] = React.useState(0) const rowRef = React.useRef(null); const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) + const [detailModalContent, setDetailModalContent] = React.useState('') + const [showDetailModal, setShowDetailModal] = React.useState(false) // set plot dimensions React.useLayoutEffect(() => { @@ -82,7 +85,7 @@ const FilesProcessed = (props: {}) => { h.done((d) => { setAggregateProcessedFiles(d) - setYMax(Math.max(...d?.map((c) => {return c.Count}))) + setYMax(Math.max(...d?.map((c) => { return c.Count }))) }).fail(() => { setStatus('error') }) @@ -143,6 +146,10 @@ const FilesProcessed = (props: {}) => { } } + function handleViewMoreClick(event: React.MouseEvent, message: string) { + setDetailModalContent(message) + setShowDetailModal(true) + } return (
@@ -177,7 +184,7 @@ const FilesProcessed = (props: {}) => { })}
-
+
Data={dataFile} SortKey={sortField} @@ -248,23 +255,52 @@ const FilesProcessed = (props: {}) => { Data Operation Failures : {dataOperationFailure.map((e, i) => { return
-
{e.DataFileName}
-
+
{moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')}
+
+
{e.DataOperationTypeName.split('.')[e.DataOperationTypeName.split('.').length - 1]}
+
+
{e.DataFileName}
- Log: - +
setHovered(`failurelog${e.ID.toString()}`)} + onMouseLeave={() => setHovered('')} + data-tooltip={`failurelog${e.ID.toString()}`} + > + View Log
+ + {e.Log.length > 100 + ? <> +

{`${e.Log.slice(0, 100)}...`}

+ { handleViewMoreClick(ev, e.Log) }}>View more + + :

{e.Log}

} +
+
- Stack Trace: - +
setHovered(`failurestacktrace${e.ID.toString()}`)} + onMouseLeave={() => setHovered('')} + data-tooltip={`failurestacktrace${e.ID.toString()}`} + > + View Stack Trace +
+ + {e.StackTrace.length > 100 + ? <> +

{`${e.StackTrace.slice(0, 100)}...`}

+ {handleViewMoreClick(ev,e.StackTrace)}}>View more + + :

{e.StackTrace}

} +
}) From 7cb9244e481ede9a1b98ac8ab19e8bc81d85d93a Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 30 Apr 2026 13:25:32 -0400 Subject: [PATCH 08/16] add plot selection to filter processed files table and data operations --- .../SystemCenter/AppHost/FilesProcessed.tsx | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 0ae565a7f..36be0193c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -58,6 +58,7 @@ const FilesProcessed = (props: {}) => { const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) const [detailModalContent, setDetailModalContent] = React.useState('') const [showDetailModal, setShowDetailModal] = React.useState(false) + const [filteredHour, setFilteredHour] = React.useState(null) // set plot dimensions React.useLayoutEffect(() => { @@ -69,9 +70,12 @@ const FilesProcessed = (props: {}) => { setStatus('loading') getFileGroups() getDataOperationFailure() - getAggregateRecentlyProcessedFiles() setStatus('idle') - }, [sortField, ascending, page]) + }, [sortField, ascending, page, filteredHour]) + + React.useEffect(() => { + getAggregateRecentlyProcessedFiles() + }, []) function getAggregateRecentlyProcessedFiles() { const h = $.ajax({ @@ -97,6 +101,26 @@ const FilesProcessed = (props: {}) => { } function getFileGroups() { + const filters = filteredHour === null ? [{ + FieldName: 'ProcessingStartTime', + Operator: '>', + Type: 'datetime', + SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') + }] + : [ + { + FieldName: 'ProcessingStartTime', + Operator: '>=', + Type: 'datetime', + SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') +}, + { + FieldName: 'ProcessingStartTime', + Operator: '<', + Type: 'datetime', + SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') +} + ] const h = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, @@ -104,7 +128,7 @@ const FilesProcessed = (props: {}) => { dataType: 'json', cache: false, async: true, - data: JSON.stringify({ Searches: [{ FieldName: 'ProcessingStartTime', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS'), Operator: '>', Type: 'datetime' }], OrderBy: sortField, Ascending: ascending }), + data: JSON.stringify({ Searches: filters, OrderBy: sortField, Ascending: ascending }), }); h.done((d) => { @@ -122,6 +146,27 @@ const FilesProcessed = (props: {}) => { } function getDataOperationFailure() { + const filters = filteredHour === null ? [{ + FieldName: 'TimeOfFailure', + Operator: '>', + Type: 'datetime', + SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') + }] + : [ + { + FieldName: 'TimeOfFailure', + Operator: '>=', + Type: 'datetime', + SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') +}, + { + FieldName: 'TimeOfFailure', + Operator: '<', + Type: 'datetime', + SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') +} + ] + const h = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures`, @@ -129,7 +174,7 @@ const FilesProcessed = (props: {}) => { dataType: 'json', cache: false, async: true, - data: JSON.stringify({ Searches: [{ FieldName: 'TimeOfFailure', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS'), Operator: '>', Type: 'datetime' }], OrderBy: 'TimeOfFailure', Ascending: false }), + data: JSON.stringify({ Searches: filters, OrderBy: 'TimeOfFailure', Ascending: false }), }); h.done((d) => { @@ -151,6 +196,10 @@ const FilesProcessed = (props: {}) => { setShowDetailModal(true) } + const handleOnPlotSelect = React.useCallback((x: number, y: number[]) => { + const selectedHour = aggregateProcessedFiles.find(a => moment(a.Hour).valueOf() < x && (moment(a.Hour).valueOf() + 3600000) > x && y[0] < a.Count) + setFilteredHour(selectedHour?.Hour ?? null) + }, [aggregateProcessedFiles]) return (
@@ -171,6 +220,7 @@ const FilesProcessed = (props: {}) => { Ymin={0} Ymax={yMax} // should be dynamic Ylabel={'Files Queued'} + onSelect={handleOnPlotSelect} > {aggregateProcessedFiles.length == 0 ? null : aggregateProcessedFiles.map((a, i) => { @@ -179,6 +229,7 @@ const FilesProcessed = (props: {}) => { BarOrigin={moment(a.Hour).valueOf()} BarWidth={3600000} Color={'black'} + key={a.Hour} > })} @@ -253,8 +304,9 @@ const FilesProcessed = (props: {}) => {
Data Operation Failures : - {dataOperationFailure.map((e, i) => { - return
+ {dataOperationFailure.map((e) => { + return
{moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')}
@@ -305,9 +357,20 @@ const FilesProcessed = (props: {}) => {
}) } +
: null} + { setShowDetailModal(false) }} + Show={showDetailModal} + ShowCancel={false} + ShowX={true} + ShowConfirm={false } + > + {detailModalContent} +
) } From f03a54d4ddddba422068a1886b0c1d85c8305605 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 30 Apr 2026 14:08:59 -0400 Subject: [PATCH 09/16] add selection for data operation failures and files --- .../SystemCenter/AppHost/FilesProcessed.tsx | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 36be0193c..d48a7ce7e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -58,7 +58,9 @@ const FilesProcessed = (props: {}) => { const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) const [detailModalContent, setDetailModalContent] = React.useState('') const [showDetailModal, setShowDetailModal] = React.useState(false) + const [selectedFile, setSelectedFile] = React.useState(null) const [filteredHour, setFilteredHour] = React.useState(null) + const [selectedTime, setSelectedTime] = React.useState(null) // set plot dimensions React.useLayoutEffect(() => { @@ -67,6 +69,8 @@ const FilesProcessed = (props: {}) => { }); React.useEffect(() => { + setSelectedTime(null) + setSelectedFile(null) setStatus('loading') getFileGroups() getDataOperationFailure() @@ -195,11 +199,22 @@ const FilesProcessed = (props: {}) => { setDetailModalContent(message) setShowDetailModal(true) } - + const handleOnPlotSelect = React.useCallback((x: number, y: number[]) => { const selectedHour = aggregateProcessedFiles.find(a => moment(a.Hour).valueOf() < x && (moment(a.Hour).valueOf() + 3600000) > x && y[0] < a.Count) setFilteredHour(selectedHour?.Hour ?? null) }, [aggregateProcessedFiles]) + + function handleOnTableClick(data, event: React.MouseEvent) { + setSelectedFile(data.row.FileName) + setSelectedTime(data.row.ProcessingStartTime) + } + + function handleDataOperationFailureClick(data: INamedDataOperationFailure, event: React.MouseEvent) { + setSelectedFile(data.DataFileName) + setSelectedTime(data.TimeOfFailure) + } + return (
@@ -221,6 +236,8 @@ const FilesProcessed = (props: {}) => { Ymax={yMax} // should be dynamic Ylabel={'Files Queued'} onSelect={handleOnPlotSelect} + pan={false} + defaultMouseMode={'select'} > {aggregateProcessedFiles.length == 0 ? null : aggregateProcessedFiles.map((a, i) => { @@ -228,9 +245,9 @@ const FilesProcessed = (props: {}) => { Data={[a.Count]} BarOrigin={moment(a.Hour).valueOf()} BarWidth={3600000} - Color={'black'} + Color={a.Hour === filteredHour ? 'yellow' : moment(selectedTime).hour() === moment(a.Hour).hour() && selectedTime !== null ? 'green' : 'black'} key={a.Hour} - > + > })} @@ -250,6 +267,8 @@ const FilesProcessed = (props: {}) => { setSortField(d.colField); } }} + OnClick={handleOnTableClick} + Selected={(item) => item.FileName == selectedFile} > Key={'FileName'} @@ -298,30 +317,31 @@ const FilesProcessed = (props: {}) => { - setPage(p - 1)} /> + setPage(p - 1)} />
Data Operation Failures : {dataOperationFailure.map((e) => { - return
{ handleDataOperationFailureClick(e, evt) }}>
- {moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')} -
+ {moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')} +
{e.DataOperationTypeName.split('.')[e.DataOperationTypeName.split('.').length - 1]}
{e.DataFileName}
-
+
setHovered(`failurelog${e.ID.toString()}`)} onMouseLeave={() => setHovered('')} data-tooltip={`failurelog${e.ID.toString()}`} > View Log -
+
{ :

{e.Log}

}
-
+
setHovered(`failurestacktrace${e.ID.toString()}`)} onMouseLeave={() => setHovered('')} @@ -353,10 +373,10 @@ const FilesProcessed = (props: {}) => { :

{e.StackTrace}

} +
-
- }) - } + }) + }
From e3254bcf7f93e33c4232ff62d9c730658060929d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 1 May 2026 14:22:21 -0400 Subject: [PATCH 10/16] fix plot bugs and include processing state in table --- .../SystemCenter/AppHost/FilesProcessed.tsx | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index d48a7ce7e..a11c4f1a7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -27,6 +27,7 @@ import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' import { LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { SystemCenter as SC } from '../global'; import moment from 'moment' @@ -40,14 +41,13 @@ interface IAggregateProcessedFile { } const FilesProcessed = (props: {}) => { - const [sortField, setSortField] = React.useState('ID') const [ascending, setAscending] = React.useState(false) const [plotWidth, setPlotWidth] = React.useState(100); const [plotHeight, setPlotHeight] = React.useState(400); const [page, setPage] = React.useState(0); - const [totalPages, setTotalPages] = React.useState() - const [totalFailurePages, setTotalFailurePages] = React.useState() + const [totalPages, setTotalPages] = React.useState(0) + const [totalFailurePages, setTotalFailurePages] = React.useState(0) const [hovered, setHovered] = React.useState('') const [dataFile, setDataFile] = React.useState([]) const [dataOperationFailure, setDataOperationFailure] = React.useState([]) @@ -138,6 +138,8 @@ const FilesProcessed = (props: {}) => { h.done((d) => { setDataFile(JSON.parse(d.Data)) setTotalPages(d.NumberOfPages) + if (page - 1 >= d.NumberOfPages) + setPage(d.NumberOfPages - 1) setStatus('idle') }).fail(() => { setStatus('error') @@ -245,7 +247,7 @@ const FilesProcessed = (props: {}) => { Data={[a.Count]} BarOrigin={moment(a.Hour).valueOf()} BarWidth={3600000} - Color={a.Hour === filteredHour ? 'yellow' : moment(selectedTime).hour() === moment(a.Hour).hour() && selectedTime !== null ? 'green' : 'black'} + Color={a.Hour === filteredHour ? 'yellow' : moment(a.Hour).startOf('hour').valueOf() === moment(selectedTime).startOf('hour').valueOf() && selectedTime !== null ? 'green' : 'black'} key={a.Hour} > @@ -272,7 +274,7 @@ const FilesProcessed = (props: {}) => { > Key={'FileName'} - AllowSort={true} + AllowSort={false} Field={'FileName'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} @@ -286,6 +288,8 @@ const FilesProcessed = (props: {}) => { HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} Content={({ item, field }) => { + if (item[field] == "0001-01-01T00:00:00") + return 'N/A' return {moment(item[field]).format('MM/DD/YYYY hh:mm')} }} > @@ -298,6 +302,8 @@ const FilesProcessed = (props: {}) => { HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} Content={({ item, field }) => { + if (item[field] == null || item[field] == undefined) + return 'N/A' return {moment(item[field]).format('MM/DD/YYYY hh:mm')} }} > @@ -310,12 +316,24 @@ const FilesProcessed = (props: {}) => { HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} Content={({ item, field }) => { + if (item[field] == "0001-01-01T00:00:00") + return 'N/A' return {moment(item[field]).format('MM/DD/YYYY hh:mm')} }} > Processing End Time - + + Key={'ProcessingState'} + AllowSort={true} + Field={'ProcessingState'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + return processingStateToSymbol(item[field] as number) + }} + > + setPage(p - 1)} />
@@ -395,4 +413,18 @@ const FilesProcessed = (props: {}) => { } -export default FilesProcessed \ No newline at end of file +const processingStateToSymbol = (processingState: number) => { + if (processingState == 0) //Added - Unknown + return ; + if (processingState == 1) //Queued + return ; + if (processingState == 2) // Processing + return ; + if (processingState == 3) // Processed + return + if (processingState == 4) // Error + return ; + if (processingState == 5) // Partial Success + return ; + return +} \ No newline at end of file From 87a38b6dacc0f286b0893e7bb84d709a47880ada Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 4 May 2026 10:47:15 -0400 Subject: [PATCH 11/16] make data operation failure separate component --- .../SystemCenter/SystemCenter.csproj | 38 ++++---- .../AppHost/DataOperationFailure.tsx | 95 +++++++++++++++++++ .../SystemCenter/AppHost/FilesProcessed.tsx | 74 +++------------ 3 files changed, 130 insertions(+), 77 deletions(-) create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx diff --git a/Source/Applications/SystemCenter/SystemCenter.csproj b/Source/Applications/SystemCenter/SystemCenter.csproj index e5ab704c8..a20655153 100644 --- a/Source/Applications/SystemCenter/SystemCenter.csproj +++ b/Source/Applications/SystemCenter/SystemCenter.csproj @@ -346,6 +346,8 @@ + + @@ -493,30 +495,30 @@ PreserveNewest - - PreserveNewest - - + + PreserveNewest + + PreserveNewest PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -827,7 +829,7 @@ False - + diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx new file mode 100644 index 000000000..009939a57 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx @@ -0,0 +1,95 @@ +//****************************************************************************************************** +// FileWatcher.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 05/04/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react' +import moment from 'moment' +import { ToolTip } from '@gpa-gemstone/react-forms' +import { OpenXDA } from '@gpa-gemstone/application-typings'; + + +export interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { + DataFileName: string +} +interface IProps { + NamedDataOperationFailure: INamedDataOperationFailure + SelectedFile: string + Hovered: string + HandleViewMoreClick: (info: string, evt: React.MouseEvent) => void + HandleDataOperationFailureClick: (dataOperationFailure: INamedDataOperationFailure, evt: React.MouseEvent) => void + SetHovered: React.Dispatch> +} + +const DataOperationFailure = (props: IProps) => { + return
{ props.HandleDataOperationFailureClick(props.NamedDataOperationFailure, evt) }}> +
+ {moment(props.NamedDataOperationFailure.TimeOfFailure).format('MM/DD/YYYY hh:mm')} +
+
+
{props.NamedDataOperationFailure.DataOperationTypeName.split('.')[props.NamedDataOperationFailure.DataOperationTypeName.split('.').length - 1]}
+
+
{props.NamedDataOperationFailure.DataFileName}
+
+
props.SetHovered(`failurelog${props.NamedDataOperationFailure.ID.toString()}`)} + onMouseLeave={() => props.SetHovered('')} + data-tooltip={`failurelog${props.NamedDataOperationFailure.ID.toString()}`} + > + View Log +
+ + {props.NamedDataOperationFailure.Log.length > 100 + ? <> +

{`${props.NamedDataOperationFailure.Log.slice(0, 100)}...`}

+ { props.HandleViewMoreClick(props.NamedDataOperationFailure.Log, evt) }}>View more + + :

{props.NamedDataOperationFailure.Log}

} +
+
+
+
props.SetHovered(`failurestacktrace${props.NamedDataOperationFailure.ID.toString()}`)} + onMouseLeave={() => props.SetHovered('')} + data-tooltip={`failurestacktrace${props.NamedDataOperationFailure.ID.toString()}`} + > + View Stack Trace +
+ + {props.NamedDataOperationFailure.StackTrace.length > 100 + ? <> +

{`${props.NamedDataOperationFailure.StackTrace.slice(0, 100)}...`}

+ { props.HandleViewMoreClick(props.NamedDataOperationFailure.StackTrace, evt) }}>View more + + :

{props.NamedDataOperationFailure.StackTrace}

} +
+
+
+} + +export default DataOperationFailure \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index a11c4f1a7..27cc6118c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -22,7 +22,7 @@ //****************************************************************************************************** import * as React from 'react' -import { OpenXDA, Application } from '@gpa-gemstone/application-typings'; +import { Application } from '@gpa-gemstone/application-typings'; import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' import { LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'; @@ -30,10 +30,7 @@ import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { SystemCenter as SC } from '../global'; import moment from 'moment' - -interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { - DataFileName: string -} +import DataOperationFailure, { INamedDataOperationFailure} from './DataOperationFailure' interface IAggregateProcessedFile { Hour: string, @@ -197,8 +194,8 @@ const FilesProcessed = (props: {}) => { } } - function handleViewMoreClick(event: React.MouseEvent, message: string) { - setDetailModalContent(message) + function handleViewMoreClick(info: string, event: React.MouseEvent) { + setDetailModalContent(info) setShowDetailModal(true) } @@ -338,61 +335,19 @@ const FilesProcessed = (props: {}) => { setPage(p - 1)} />
-
+
Data Operation Failures : {dataOperationFailure.map((e) => { - return
{ handleDataOperationFailureClick(e, evt) }}> -
- {moment(e.TimeOfFailure).format('MM/DD/YYYY hh:mm')} -
-
-
{e.DataOperationTypeName.split('.')[e.DataOperationTypeName.split('.').length - 1]}
-
-
{e.DataFileName}
-
-
setHovered(`failurelog${e.ID.toString()}`)} - onMouseLeave={() => setHovered('')} - data-tooltip={`failurelog${e.ID.toString()}`} - > - View Log -
- - {e.Log.length > 100 - ? <> -

{`${e.Log.slice(0, 100)}...`}

- { handleViewMoreClick(ev, e.Log) }}>View more - - :

{e.Log}

} -
-
-
-
setHovered(`failurestacktrace${e.ID.toString()}`)} - onMouseLeave={() => setHovered('')} - data-tooltip={`failurestacktrace${e.ID.toString()}`} - > - View Stack Trace -
- - {e.StackTrace.length > 100 - ? <> -

{`${e.StackTrace.slice(0, 100)}...`}

- {handleViewMoreClick(ev,e.StackTrace)}}>View more - - :

{e.StackTrace}

} -
-
-
+ return }) }
@@ -412,6 +367,7 @@ const FilesProcessed = (props: {}) => {
) } +export default FilesProcessed const processingStateToSymbol = (processingState: number) => { if (processingState == 0) //Added - Unknown From e5e8b8fedf46f9ddd23f3ae71638e068ba7ac293 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 4 May 2026 12:08:10 -0400 Subject: [PATCH 12/16] page and scroll data operation failures --- .../Controllers/OpenXDA/OpenXDAControllers.cs | 6 +- .../SystemCenter/AppHost/FilesProcessed.tsx | 58 +++++++++++-------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 05d53157f..83b86cc8d 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs @@ -106,10 +106,10 @@ public class DataOperationController : ModelController { } [RoutePrefix("api/OpenXDA/DataOperationFailure")] public class DataOperationFailureController : ModelController { - [Route("RecentFailures"), HttpPost] - public IHttpActionResult RecentFailures([FromBody] PostData postData) + [Route("RecentFailures/{page}"), HttpPost] + public IHttpActionResult RecentFailures([FromBody] PostData postData, [FromUri] int page) { - using DataTable value = GetSearchResults(postData, 0); + using DataTable value = GetSearchResults(postData, page); value.Columns.Add("DataFileName", typeof(String)); using (AdoDataConnection connection = new AdoDataConnection(Connection)) { diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 27cc6118c..6fbe7159b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -26,23 +26,23 @@ import { Application } from '@gpa-gemstone/application-typings'; import { Table, Paging, Column } from '@gpa-gemstone/react-table' import { Plot, Bar } from '@gpa-gemstone/react-graph' import { LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { SystemCenter as SC } from '../global'; import moment from 'moment' -import DataOperationFailure, { INamedDataOperationFailure} from './DataOperationFailure' +import DataOperationFailure, { INamedDataOperationFailure } from './DataOperationFailure' interface IAggregateProcessedFile { Hour: string, Count: number } -const FilesProcessed = (props: {}) => { +const FilesProcessed = () => { const [sortField, setSortField] = React.useState('ID') const [ascending, setAscending] = React.useState(false) const [plotWidth, setPlotWidth] = React.useState(100); const [plotHeight, setPlotHeight] = React.useState(400); const [page, setPage] = React.useState(0); + const [failurePage, setFailurePage] = React.useState(0) const [totalPages, setTotalPages] = React.useState(0) const [totalFailurePages, setTotalFailurePages] = React.useState(0) const [hovered, setHovered] = React.useState('') @@ -66,13 +66,11 @@ const FilesProcessed = (props: {}) => { }); React.useEffect(() => { - setSelectedTime(null) - setSelectedFile(null) setStatus('loading') getFileGroups() getDataOperationFailure() setStatus('idle') - }, [sortField, ascending, page, filteredHour]) + }, [sortField, ascending, page, filteredHour, failurePage]) React.useEffect(() => { getAggregateRecentlyProcessedFiles() @@ -114,14 +112,14 @@ const FilesProcessed = (props: {}) => { Operator: '>=', Type: 'datetime', SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') -}, + }, { FieldName: 'ProcessingStartTime', Operator: '<', Type: 'datetime', SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') -} - ] + } + ] const h = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, @@ -137,6 +135,7 @@ const FilesProcessed = (props: {}) => { setTotalPages(d.NumberOfPages) if (page - 1 >= d.NumberOfPages) setPage(d.NumberOfPages - 1) + // if datafile not in there, set it to null setStatus('idle') }).fail(() => { setStatus('error') @@ -161,18 +160,18 @@ const FilesProcessed = (props: {}) => { Operator: '>=', Type: 'datetime', SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') -}, + }, { FieldName: 'TimeOfFailure', Operator: '<', Type: 'datetime', SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') -} + } ] const h = $.ajax({ type: "POST", - url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures`, + url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures/${failurePage}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, @@ -183,6 +182,8 @@ const FilesProcessed = (props: {}) => { h.done((d) => { setDataOperationFailure(JSON.parse(d.Data)) setTotalFailurePages(d.NumberOfPages) + if (failurePage - 1 >= d.NumberOfPages) + setPage(d.NumberOfPages - 1) setStatus('idle') }).fail(() => { setStatus('error') @@ -338,18 +339,27 @@ const FilesProcessed = (props: {}) => {
Data Operation Failures : - {dataOperationFailure.map((e) => { - return +
+ {dataOperationFailure.map((e) => { + return - }) - } + /> + }) + } +
+
+
+
+ setFailurePage(p - 1)} /> +
+
@@ -360,7 +370,7 @@ const FilesProcessed = (props: {}) => { Show={showDetailModal} ShowCancel={false} ShowX={true} - ShowConfirm={false } + ShowConfirm={false} > {detailModalContent} From 43d1bd9cd4724231a1d6d1d6ec61eb4755b5289c Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 4 May 2026 12:56:06 -0400 Subject: [PATCH 13/16] refactor selection-based searches --- .../SystemCenter/AppHost/FilesProcessed.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index 6fbe7159b..a031645ee 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -100,14 +100,17 @@ const FilesProcessed = () => { } function getFileGroups() { - const filters = filteredHour === null ? [{ + let filters = [] + if (filteredHour === null) { + filters.concat({ FieldName: 'ProcessingStartTime', Operator: '>', Type: 'datetime', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') - }] - : [ - { + }) + } + else { + filters.concat([{ FieldName: 'ProcessingStartTime', Operator: '>=', Type: 'datetime', @@ -119,7 +122,9 @@ const FilesProcessed = () => { Type: 'datetime', SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') } - ] + ]) + } + const h = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, @@ -148,13 +153,17 @@ const FilesProcessed = () => { } function getDataOperationFailure() { - const filters = filteredHour === null ? [{ + let filters = [] + if (filteredHour === null) { + filters.concat({ FieldName: 'TimeOfFailure', Operator: '>', Type: 'datetime', SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') - }] - : [ + }) + } + else { + filters.concat([ { FieldName: 'TimeOfFailure', Operator: '>=', @@ -167,7 +176,8 @@ const FilesProcessed = () => { Type: 'datetime', SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') } - ] + ]) + } const h = $.ajax({ type: "POST", From 63f12944976e176b4e56b941c2da67b2251a1312 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 6 May 2026 14:07:42 -0400 Subject: [PATCH 14/16] add files processed tab --- .../Scripts/TSX/SystemCenter/AppHost/NodeDetails.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/NodeDetails.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/NodeDetails.tsx index 7fbf35f8e..0697500da 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/NodeDetails.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/NodeDetails.tsx @@ -30,8 +30,9 @@ import NodeHealth from './NodeHealth'; import ConsoleWindow from './ConsoleWindow' import { IHost } from './ApplicationCard'; import { SystemCenter as SC } from '../global' +import FilesProcessed from './FilesProcessed' -type tab = 'connections' | 'health' | 'console' +type tab = 'connections' | 'health' | 'console' | 'filesprocessed' interface ITab { Label: string, Id: string } export interface IMessage { Message: string, Type: number } @@ -48,7 +49,7 @@ export interface IProps { const ApplicationTabs = { 'SystemCenter': [{ Label: 'Connections', Id: 'connections' }, { Label: 'Console', Id: 'console' }], - 'XDA': [{ Label: 'Connections', Id: 'connections' }, { Label: 'Health', Id: 'health' }, { Label: 'Console', Id: 'console' }], + 'XDA': [{ Label: 'Connections', Id: 'connections' }, { Label: 'Health', Id: 'health' }, { Label: 'Console', Id: 'console' }, { Label: 'Files Processed', Id: 'filesprocessed' }], 'MiMD': [{ Label: 'Console', Id: 'console' }], 'openMIC': [{ Label: 'Health', Id: 'health' }] } @@ -105,6 +106,9 @@ const NodeDetails = (props: IProps) => { Close={() => props.SetConsole(null)} />
+ : tab === "filesprocessed" ?
+ +
: null}
From 0fe00c2fc2f86b0bd8ad220f112cafe9979c9812 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 8 May 2026 10:50:26 -0400 Subject: [PATCH 15/16] move processing status to common component --- .../CommonComponents/ProcessingStatus.tsx | 129 ++++++++++++++++++ .../TSX/SystemCenter/ProcessedFile/ByFile.tsx | 101 +------------- 2 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ProcessingStatus.tsx diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ProcessingStatus.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ProcessingStatus.tsx new file mode 100644 index 000000000..d55fbb1bd --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ProcessingStatus.tsx @@ -0,0 +1,129 @@ +//****************************************************************************************************** +// ProcessingStatus.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Natalie Beatty +// Moved from ProcessedFile/ByFile.tsx +// +//****************************************************************************************************** + +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { CreateGuid } from '@gpa-gemstone/helper-functions'; +import * as React from 'react'; +import { ToolTip } from '@gpa-gemstone/react-forms'; + +interface IStatusProps { + Status: number; + FileGroupID: number; + Interactive: boolean; +} + +const ProcessingStatus = (props: IStatusProps) => { + const [message, setMessage] = React.useState(undefined); + const [hover, setHover] = React.useState(false); + const [guid, _setGuid] = React.useState(CreateGuid()); + + React.useEffect(() => { + if (!props.Interactive) + return + switch (props.Status) { + case 5: + setMessage("Click to see issues."); + break; + default: + setMessage(undefined); + } + }, [props.Status, props.Interactive]); + + const onClick = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + if (!props.Interactive) + return + if (props.Status === 5) + window.location.href = `${homePath}index.cshtml?name=DataOperationsFailures&FileGroupID=${props.FileGroupID}`; + }, [props.Status, props.FileGroupID, props.Interactive]); + + const visual = React.useMemo(() => { + if (props.Status == 0) //Added - Unknown + return "badge-light"; + if (props.Status == 1) //Queued + return "badge-info"; + if (props.Status == 2) // Processing + return "badge-primary"; + if (props.Status == 3) // Processed + return "badge-success"; + if (props.Status == 4) // Error + return "badge-danger"; + if (props.Status == 5) // Partial Success + return "badge-warning"; + return "badge-warning"; + }, [props.Status]); + + const text = React.useMemo(() => { + if (props.Status == 0) //Added - Unknown + return "Unknown"; + if (props.Status == 1) //Queued + return "Queued"; + if (props.Status == 2) // Processing + return "Processing"; + if (props.Status == 3) // Processed + return "Processed"; + if (props.Status == 4) // Error + return "Failure"; + if (props.Status == 5) // Partial Success + return "Warning"; + return "Unknwown"; + }, [props.Status]); + + const Symbol = React.useMemo(() => { + if (props.Status == 0) //Added - Unknown + return ; + if (props.Status == 1) //Queued + return ; + if (props.Status == 2) // Processing + return ; + if (props.Status == 3) // Processed + return + if (props.Status == 4) // Error + return ; + if (props.Status == 5) // Partial Success + return ; + return ; + }, [props.Status]); + + return ( + <> + setHover(true)} + onMouseLeave={() => setHover(false)} + onClick={onClick} + > + {Symbol} {text} + + { + message === undefined ? null : + + {message} + + } + + ); +} + +export default ProcessingStatus \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ProcessedFile/ByFile.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ProcessedFile/ByFile.tsx index d3e477180..a51f0c920 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ProcessedFile/ByFile.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ProcessedFile/ByFile.tsx @@ -22,10 +22,7 @@ //****************************************************************************************************** import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { CreateGuid } from '@gpa-gemstone/helper-functions'; import { LoadingIcon, LoadingScreen, Modal, Search, SearchBar, Warning } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; import { Column, Paging, Table } from '@gpa-gemstone/react-table'; import moment from 'moment'; import * as React from 'react'; @@ -35,6 +32,7 @@ import { OpenXDA as GlobalXDA } from '../global'; import { useAppDispatch, useAppSelector } from '../hooks'; import { DataFileSlice } from '../Store/Store'; import EditionLockModal from '../CommonComponents/Restrictions/EditionLockModal'; +import ProcessingStatus from '../CommonComponents/ProcessingStatus' const filterableList: Search.IField[] = [ { isPivotField: false, key: 'FilePath', label: 'File Path', type: 'string' }, @@ -356,7 +354,7 @@ const ByFile: Application.Types.iByComponent = (props) => { Field={'ProcessingState'} HeaderStyle={{ width: '10%' }} RowStyle={{ width: '10%' }} - Content={({ item }) => } + Content={({ item }) => } > Status @@ -450,100 +448,5 @@ const ByFile: Application.Types.iByComponent = (props) => { ) } -interface IStatusProps { - Status: number; - FileGroupID: number; -} - -const ProcessingStatus = (props: IStatusProps) => { - const [message, setMessage] = React.useState(undefined); - const [hover, setHover] = React.useState(false); - const [guid, _setGuid] = React.useState(CreateGuid()); - - React.useEffect(() => { - switch (props.Status) { - case 5: - setMessage("Click to see issues."); - break; - default: - setMessage(undefined); - } - }, [props.Status]); - - const onClick = React.useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - if (props.Status === 5) - window.location.href = `${homePath}index.cshtml?name=DataOperationsFailures&FileGroupID=${props.FileGroupID}`; - }, [props.Status, props.FileGroupID]); - - const visual = React.useMemo(() => { - if (props.Status == 0) //Added - Unknown - return "badge-light"; - if (props.Status == 1) //Queued - return "badge-info"; - if (props.Status == 2) // Processing - return "badge-primary"; - if (props.Status == 3) // Processed - return "badge-success"; - if (props.Status == 4) // Error - return "badge-danger"; - if (props.Status == 5) // Partial Success - return "badge-warning"; - return "badge-warning"; - }, [props.Status]); - - const text = React.useMemo(() => { - if (props.Status == 0) //Added - Unknown - return "Unknown"; - if (props.Status == 1) //Queued - return "Queued"; - if (props.Status == 2) // Processing - return "Processing"; - if (props.Status == 3) // Processed - return "Processed"; - if (props.Status == 4) // Error - return "Failure"; - if (props.Status == 5) // Partial Success - return "Warning"; - return "Unknwown"; - }, [props.Status]); - - const Symbol = React.useMemo(() => { - if (props.Status == 0) //Added - Unknown - return ; - if (props.Status == 1) //Queued - return ; - if (props.Status == 2) // Processing - return ; - if (props.Status == 3) // Processed - return - if (props.Status == 4) // Error - return ; - if (props.Status == 5) // Partial Success - return ; - return ; - }, [props.Status]); - - return ( - <> - setHover(true)} - onMouseLeave={() => setHover(false)} - onClick={onClick} - > - {Symbol} {text} - - { - message === undefined ? null : - - {message} - - } - - ); -} - export default ByFile; From 54c9c34d45f08b23a39b6c5aff74b4f2b6e771d3 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 8 May 2026 10:53:52 -0400 Subject: [PATCH 16/16] split into separate components --- .../AppHost/DataOperationFailure.tsx | 10 +- .../AppHost/DataOperationFailures.tsx | 134 ++++++ .../SystemCenter/AppHost/FilesProcessed.tsx | 385 ++---------------- .../AppHost/FilesProcessedGraph.tsx | 112 +++++ .../AppHost/FilesProcessedTable.tsx | 187 +++++++++ 5 files changed, 475 insertions(+), 353 deletions(-) create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailures.tsx create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedGraph.tsx create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedTable.tsx diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx index 009939a57..d56538c19 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx @@ -1,5 +1,5 @@ //****************************************************************************************************** -// FileWatcher.tsx - Gbtc +// DataOperationFailure.tsx - Gbtc // // Copyright © 2026, Grid Protection Alliance. All Rights Reserved. // @@ -25,23 +25,21 @@ import moment from 'moment' import { ToolTip } from '@gpa-gemstone/react-forms' import { OpenXDA } from '@gpa-gemstone/application-typings'; - export interface INamedDataOperationFailure extends OpenXDA.Types.DataOperationFailure { DataFileName: string } interface IProps { NamedDataOperationFailure: INamedDataOperationFailure - SelectedFile: string + SelectedFile: number Hovered: string HandleViewMoreClick: (info: string, evt: React.MouseEvent) => void - HandleDataOperationFailureClick: (dataOperationFailure: INamedDataOperationFailure, evt: React.MouseEvent) => void SetHovered: React.Dispatch> } const DataOperationFailure = (props: IProps) => { - return
{ props.HandleDataOperationFailureClick(props.NamedDataOperationFailure, evt) }}> + >
{moment(props.NamedDataOperationFailure.TimeOfFailure).format('MM/DD/YYYY hh:mm')}
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailures.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailures.tsx new file mode 100644 index 000000000..f06208ede --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailures.tsx @@ -0,0 +1,134 @@ +//****************************************************************************************************** +// DataOperationFailures.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 05/07/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react' +import { Application } from '@gpa-gemstone/application-typings'; +import moment from 'moment' +import DataOperationFailure, { INamedDataOperationFailure } from './DataOperationFailure' +import { Paging } from '@gpa-gemstone/react-table' + +interface IProps { + FilteredHour: string + SelectedFile: number + HandleViewMoreClick: (info: string, event: React.MouseEvent) => void +} + +const DataOperationFailures = (props: IProps) => { + const [status, setStatus] = React.useState('uninitiated') + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0) + const [dataOperationFailures, setDataOperationFailures] = React.useState([]) + const [hovered, setHovered] = React.useState('') + + + React.useEffect(() => { + setStatus('loading') + getDataOperationFailure() + setStatus('idle') + }, [page, props.FilteredHour, props.SelectedFile]) + + function getDataOperationFailure() { + let filters = [] + if (props.SelectedFile !== null) { + filters = [{ + FieldName: 'FileGroupID', + Operator: '=', + Type: 'number', + SearchText: props.SelectedFile + }] + } + else if (props.FilteredHour !== null) { + filters = [ + { + FieldName: 'TimeOfFailure', + Operator: '>=', + Type: 'datetime', + SearchText: moment(props.FilteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') + }, + { + FieldName: 'TimeOfFailure', + Operator: '<', + Type: 'datetime', + SearchText: moment(props.FilteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') + } + ] + } + else { + filters = [{ + FieldName: 'TimeOfFailure', + Operator: '>', + Type: 'datetime', + SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') + }] + } + + const h = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ Searches: filters, OrderBy: 'TimeOfFailure', Ascending: false }), + }); + + h.done((d) => { + setDataOperationFailures(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages) + if (page >= d.NumberOfPages && d.NumberOfPages > 0) + setPage(d.NumberOfPages - 1) + setStatus('idle') + }).fail(() => { + setStatus('error') + }) + + return function cleanup() { + if (h.abort != null) + h.abort(); + } + } + return
+ Data Operation Failures : +
+
+ {dataOperationFailures.map((e) => { + return + }) + } +
+
+
+
+ setPage(p - 1)} /> +
+
+
+} + +export default DataOperationFailures \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx index a031645ee..2eef27ca2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -1,5 +1,5 @@ //****************************************************************************************************** -// FileWatcher.tsx - Gbtc +// FilesProcessed.tsx - Gbtc // // Copyright © 2026, Grid Protection Alliance. All Rights Reserved. // @@ -22,385 +22,76 @@ //****************************************************************************************************** import * as React from 'react' -import { Application } from '@gpa-gemstone/application-typings'; -import { Table, Paging, Column } from '@gpa-gemstone/react-table' -import { Plot, Bar } from '@gpa-gemstone/react-graph' -import { LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'; -import { ReactIcons } from '@gpa-gemstone/gpa-symbols' -import { SystemCenter as SC } from '../global'; -import moment from 'moment' -import DataOperationFailure, { INamedDataOperationFailure } from './DataOperationFailure' - -interface IAggregateProcessedFile { - Hour: string, - Count: number -} +import { Modal } from '@gpa-gemstone/react-interactive'; +import { useGetContainerPosition } from '@gpa-gemstone/helper-functions' +import FilesProcessedGraph from './FilesProcessedGraph' +import FilesProcessedTable from './FilesProcessedTable' +import DataOperationFailures from './DataOperationFailures' const FilesProcessed = () => { - const [sortField, setSortField] = React.useState('ID') - const [ascending, setAscending] = React.useState(false) - const [plotWidth, setPlotWidth] = React.useState(100); - const [plotHeight, setPlotHeight] = React.useState(400); - const [page, setPage] = React.useState(0); - const [failurePage, setFailurePage] = React.useState(0) - const [totalPages, setTotalPages] = React.useState(0) - const [totalFailurePages, setTotalFailurePages] = React.useState(0) - const [hovered, setHovered] = React.useState('') - const [dataFile, setDataFile] = React.useState([]) - const [dataOperationFailure, setDataOperationFailure] = React.useState([]) - const [status, setStatus] = React.useState('uninitiated') - const [timeframe, setTimeframe] = React.useState<[number, number]>([moment().subtract(48, 'hour').startOf('hour').valueOf(), moment().add(1, 'hour').startOf('hour').valueOf()]) - const [yMax, setYMax] = React.useState(0) const rowRef = React.useRef(null); - const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) const [detailModalContent, setDetailModalContent] = React.useState('') const [showDetailModal, setShowDetailModal] = React.useState(false) - const [selectedFile, setSelectedFile] = React.useState(null) - const [filteredHour, setFilteredHour] = React.useState(null) - const [selectedTime, setSelectedTime] = React.useState(null) - - // set plot dimensions - React.useLayoutEffect(() => { - setPlotHeight(rowRef?.current?.offsetHeight ?? 400) - setPlotWidth((rowRef?.current?.offsetWidth ?? 130) - 30) - }); - - React.useEffect(() => { - setStatus('loading') - getFileGroups() - getDataOperationFailure() - setStatus('idle') - }, [sortField, ascending, page, filteredHour, failurePage]) - - React.useEffect(() => { - getAggregateRecentlyProcessedFiles() - }, []) - - function getAggregateRecentlyProcessedFiles() { - const h = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/DataFile/AggregateRecentlyProcessedFiles`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: false, - async: true, - }) - - h.done((d) => { - setAggregateProcessedFiles(d) - setYMax(Math.max(...d?.map((c) => { return c.Count }))) - }).fail(() => { - setStatus('error') - }) - return function cleanup() { - if (h.abort != null) - h.abort(); - - } - } - - function getFileGroups() { - let filters = [] - if (filteredHour === null) { - filters.concat({ - FieldName: 'ProcessingStartTime', - Operator: '>', - Type: 'datetime', - SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') - }) - } - else { - filters.concat([{ - FieldName: 'ProcessingStartTime', - Operator: '>=', - Type: 'datetime', - SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') - }, - { - FieldName: 'ProcessingStartTime', - Operator: '<', - Type: 'datetime', - SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') - } - ]) - } - - const h = $.ajax({ - type: "POST", - url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: false, - async: true, - data: JSON.stringify({ Searches: filters, OrderBy: sortField, Ascending: ascending }), - }); - - h.done((d) => { - setDataFile(JSON.parse(d.Data)) - setTotalPages(d.NumberOfPages) - if (page - 1 >= d.NumberOfPages) - setPage(d.NumberOfPages - 1) - // if datafile not in there, set it to null - setStatus('idle') - }).fail(() => { - setStatus('error') - }) - - return function cleanup() { - if (h.abort != null) - h.abort(); - } - } - - function getDataOperationFailure() { - let filters = [] - if (filteredHour === null) { - filters.concat({ - FieldName: 'TimeOfFailure', - Operator: '>', - Type: 'datetime', - SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') - }) - } - else { - filters.concat([ - { - FieldName: 'TimeOfFailure', - Operator: '>=', - Type: 'datetime', - SearchText: moment(filteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') - }, - { - FieldName: 'TimeOfFailure', - Operator: '<', - Type: 'datetime', - SearchText: moment(filteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') - } - ]) - } - - const h = $.ajax({ - type: "POST", - url: `${homePath}api/OpenXDA/DataOperationFailure/RecentFailures/${failurePage}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: false, - async: true, - data: JSON.stringify({ Searches: filters, OrderBy: 'TimeOfFailure', Ascending: false }), - }); - - h.done((d) => { - setDataOperationFailure(JSON.parse(d.Data)) - setTotalFailurePages(d.NumberOfPages) - if (failurePage - 1 >= d.NumberOfPages) - setPage(d.NumberOfPages - 1) - setStatus('idle') - }).fail(() => { - setStatus('error') - }) - - return function cleanup() { - if (h.abort != null) - h.abort(); - } - } + const [selectedFile, setSelectedFile] = React.useState(null) + const [filteredHour, setFilteredHour] = React.useState(null) + const [selectedTime, setSelectedTime] = React.useState(null) + const {offsetWidth, offsetHeight} = useGetContainerPosition(rowRef) function handleViewMoreClick(info: string, event: React.MouseEvent) { setDetailModalContent(info) setShowDetailModal(true) } - const handleOnPlotSelect = React.useCallback((x: number, y: number[]) => { - const selectedHour = aggregateProcessedFiles.find(a => moment(a.Hour).valueOf() < x && (moment(a.Hour).valueOf() + 3600000) > x && y[0] < a.Count) - setFilteredHour(selectedHour?.Hour ?? null) - }, [aggregateProcessedFiles]) - function handleOnTableClick(data, event: React.MouseEvent) { - setSelectedFile(data.row.FileName) + if (data.row.FileGroupID === selectedFile) { + setSelectedFile(null) + setSelectedTime(null) + return + } + setSelectedFile(data.row.FileGroupID) setSelectedTime(data.row.ProcessingStartTime) } - function handleDataOperationFailureClick(data: INamedDataOperationFailure, event: React.MouseEvent) { - setSelectedFile(data.DataFileName) - setSelectedTime(data.TimeOfFailure) - } - return (
- - {status === 'idle' && dataFile !== null ? <>
- - {aggregateProcessedFiles.length == 0 ? null : - aggregateProcessedFiles.map((a, i) => { - return - - })} - +
- - Data={dataFile} - SortKey={sortField} - Ascending={ascending} - KeySelector={(item) => item.ID} - OnSort={(d) => { - if (d.colField == sortField) { - setAscending(!ascending); - } - else { - setAscending(true); - setSortField(d.colField); - } - }} - OnClick={handleOnTableClick} - Selected={(item) => item.FileName == selectedFile} - > - - Key={'FileName'} - AllowSort={false} - Field={'FileName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > - File Name - - - Key={'DataStartTime'} - AllowSort={true} - Field={'DataStartTime'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item, field }) => { - if (item[field] == "0001-01-01T00:00:00") - return 'N/A' - return {moment(item[field]).format('MM/DD/YYYY hh:mm')} - }} - > - Data Start Time - - - Key={'ProcessingStartTime'} - AllowSort={true} - Field={'ProcessingStartTime'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item, field }) => { - if (item[field] == null || item[field] == undefined) - return 'N/A' - return {moment(item[field]).format('MM/DD/YYYY hh:mm')} - }} - > - Processing Start Time - - - Key={'ProcessingEndTime'} - AllowSort={true} - Field={'ProcessingEndTime'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item, field }) => { - if (item[field] == "0001-01-01T00:00:00") - return 'N/A' - return {moment(item[field]).format('MM/DD/YYYY hh:mm')} - }} - > - Processing End Time - - - Key={'ProcessingState'} - AllowSort={true} - Field={'ProcessingState'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item, field }) => { - return processingStateToSymbol(item[field] as number) - }} - > - - - setPage(p - 1)} /> +
-
- Data Operation Failures : -
-
- {dataOperationFailure.map((e) => { - return - }) - } -
-
-
-
- setFailurePage(p - 1)} /> -
-
-
+
- - : null} { setShowDetailModal(false) }} Show={showDetailModal} ShowCancel={false} ShowX={true} ShowConfirm={false} + Size={'lg'} > {detailModalContent} -
) +
+ ) } -export default FilesProcessed - -const processingStateToSymbol = (processingState: number) => { - if (processingState == 0) //Added - Unknown - return ; - if (processingState == 1) //Queued - return ; - if (processingState == 2) // Processing - return ; - if (processingState == 3) // Processed - return - if (processingState == 4) // Error - return ; - if (processingState == 5) // Partial Success - return ; - return -} \ No newline at end of file +export default FilesProcessed \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedGraph.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedGraph.tsx new file mode 100644 index 000000000..4de6b637a --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedGraph.tsx @@ -0,0 +1,112 @@ +//****************************************************************************************************** +// FilesProcessedGraph.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 05/07/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react' +import moment from 'moment' +import { Application } from '@gpa-gemstone/application-typings'; +import { Plot, Bar } from '@gpa-gemstone/react-graph' + +export interface IAggregateProcessedFile { + Hour: string, + Count: number +} + +interface IProps { + OffsetHeight: number + OffsetWidth: number + FilteredHour: string + SelectedTime: string + SetFilteredHour: React.Dispatch> +} + +const FilesProcessedGraph = (props: IProps) => { + const [yMax, setYMax] = React.useState(0) + const [timeframe, setTimeframe] = React.useState<[number, number]>([moment().subtract(48, 'hour').startOf('hour').valueOf(), moment().add(1, 'hour').startOf('hour').valueOf()]) + const [status, setStatus] = React.useState('uninitiated') + const [aggregateProcessedFiles, setAggregateProcessedFiles] = React.useState([]) + + React.useEffect(() => { + getAggregateRecentlyProcessedFiles() + }, []) + + function getAggregateRecentlyProcessedFiles() { + const h = $.ajax({ + type: "GET", + url: `${homePath}api/OpenXDA/DataFile/AggregateRecentlyProcessedFiles`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + }) + + h.done((d) => { + setAggregateProcessedFiles(d) + setYMax(Math.max(...d?.map((c) => { return c.Count }))) + }).fail(() => { + setStatus('error') + }) + return function cleanup() { + if (h.abort != null) + h.abort(); + + } + } + + const handleOnPlotSelect = React.useCallback((x: number, y: number[]) => { + const selectedHour = aggregateProcessedFiles.find(a => moment(a.Hour).valueOf() < x && (moment(a.Hour).valueOf() + 3600000) > x && y[0] < a.Count) + props.SetFilteredHour(selectedHour?.Hour ?? null) + }, [aggregateProcessedFiles]) + + return + {aggregateProcessedFiles.length == 0 ? null : + aggregateProcessedFiles.map((a) => { + return + + })} + +} + +export default FilesProcessedGraph \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedTable.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedTable.tsx new file mode 100644 index 000000000..f3453b0ae --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessedTable.tsx @@ -0,0 +1,187 @@ +//****************************************************************************************************** +// FilesProcessedTable.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 05/07/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react' +import { Table, Paging, Column } from '@gpa-gemstone/react-table' +import { SystemCenter as SC } from '../global'; +import moment from 'moment' +import { Application } from '@gpa-gemstone/application-typings'; +import ProcessingStatus from '../CommonComponents/ProcessingStatus' + +interface IProps { + FilteredHour: string + SelectedFile: number + HandleOnTableClick: (data: any, evt: React.MouseEvent) => void +} + +const FilesProcessedTable = (props: IProps) => { + const [sortField, setSortField] = React.useState('ID') + const [ascending, setAscending] = React.useState(false) + const [dataFile, setDataFile] = React.useState([]) + const [totalPages, setTotalPages] = React.useState(0) + const [page, setPage] = React.useState(0); + const [status, setStatus] = React.useState('uninitiated') + + React.useEffect(() => { + setStatus('loading') + getFileGroups() + setStatus('idle') + }, [sortField, ascending, page, props.FilteredHour]) + + function getFileGroups() { + let filters = [] + if (props.FilteredHour === null) { + filters = [{ + FieldName: 'ProcessingStartTime', + Operator: '>', + Type: 'datetime', + SearchText: moment().subtract(48, 'hour').startOf('hour').format('YYYY-MM-DD HH:mm:ss.SSS') + }] + } + else { + filters = [{ + FieldName: 'ProcessingStartTime', + Operator: '>=', + Type: 'datetime', + SearchText: moment(props.FilteredHour).format('YYYY-MM-DD HH:mm:ss.SSS') + }, + { + FieldName: 'ProcessingStartTime', + Operator: '<', + Type: 'datetime', + SearchText: moment(props.FilteredHour).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss.SSS') + } + ] + } + const h = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/DataFile/PagedList/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ Searches: filters, OrderBy: sortField, Ascending: ascending }), + }); + + h.done((d) => { + setDataFile(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages) + if (page >= d.NumberOfPages && d.NumberOfPages > 0) + setPage(d.NumberOfPages - 1) + setStatus('idle') + }).fail(() => { + setStatus('error') + }) + + return function cleanup() { + if (h.abort != null) + h.abort(); + } + } + + return <> + + Data={dataFile} + SortKey={sortField} + Ascending={ascending} + KeySelector={(item) => item.ID} + OnSort={(d) => { + if (d.colField == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + OnClick={props.HandleOnTableClick} + Selected={(item) => item.FileGroupID === props.SelectedFile} + > + + Key={'FileName'} + AllowSort={false} + Field={'FileName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > + File Name + + + Key={'DataStartTime'} + AllowSort={true} + Field={'DataStartTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + if (item[field] == "0001-01-01T00:00:00") + return 'N/A' + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} + > + Data Start Time + + + Key={'ProcessingStartTime'} + AllowSort={true} + Field={'ProcessingStartTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + if (item[field] == null || item[field] == undefined) + return 'N/A' + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} + > + Processing Start Time + + + Key={'ProcessingEndTime'} + AllowSort={true} + Field={'ProcessingEndTime'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + if (item[field] == "0001-01-01T00:00:00") + return 'N/A' + return {moment(item[field]).format('MM/DD/YYYY hh:mm')} + }} + > + Processing End Time + + + Key={'ProcessingState'} + AllowSort={true} + Field={'ProcessingState'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item, field }) => { + return + }} + > + + + setPage(p - 1)} /> + +} + +export default FilesProcessedTable \ No newline at end of file