diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 6a61a4ab5..83b86cc8d 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; @@ -103,7 +104,34 @@ 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/{page}"), HttpPost] + public IHttpActionResult RecentFailures([FromBody] PostData postData, [FromUri] int page) + { + using DataTable value = GetSearchResults(postData, page); + value.Columns.Add("DataFileName", typeof(String)); + 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")); + row["DataFileName"] = Path.GetFileName(dataFile.FilePath); + } + } + 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/Model/DataFile.cs b/Source/Applications/SystemCenter/Model/DataFile.cs index 59da4a789..cf68c39fe 100644 --- a/Source/Applications/SystemCenter/Model/DataFile.cs +++ b/Source/Applications/SystemCenter/Model/DataFile.cs @@ -25,7 +25,9 @@ using System; 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; @@ -47,6 +49,7 @@ namespace SystemCenter.Model SELECT DataFile.*, FileGroup.DataStartTime, + FileGroup.ProcessingStartTime, FileGroup.ProcessingEndTime, FileGroup.MeterID, FileGroup.ProcessingStatus AS ProcessingState @@ -59,10 +62,13 @@ 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; } public int ProcessingState { get; set; } + [NonRecordField] + public string FileName => Path.GetFileName(FilePath); } [RoutePrefix("api/OpenXDA/DataFile")] @@ -206,7 +212,62 @@ 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); + } + + [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/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..d56538c19 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/DataOperationFailure.tsx @@ -0,0 +1,93 @@ +//****************************************************************************************************** +// DataOperationFailure.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: number + Hovered: string + HandleViewMoreClick: (info: string, evt: React.MouseEvent) => void + SetHovered: React.Dispatch> +} + +const DataOperationFailure = (props: IProps) => { + return
+
+ {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/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 new file mode 100644 index 000000000..2eef27ca2 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AppHost/FilesProcessed.tsx @@ -0,0 +1,97 @@ +//****************************************************************************************************** +// FilesProcessed.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 { 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 rowRef = React.useRef(null); + 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) + const {offsetWidth, offsetHeight} = useGetContainerPosition(rowRef) + + function handleViewMoreClick(info: string, event: React.MouseEvent) { + setDetailModalContent(info) + setShowDetailModal(true) + } + + function handleOnTableClick(data, event: React.MouseEvent) { + if (data.row.FileGroupID === selectedFile) { + setSelectedFile(null) + setSelectedTime(null) + return + } + setSelectedFile(data.row.FileGroupID) + setSelectedTime(data.row.ProcessingStartTime) + } + + return ( +
+
+
+ +
+
+ +
+
+
+ +
+ { setShowDetailModal(false) }} + Show={showDetailModal} + ShowCancel={false} + ShowX={true} + ShowConfirm={false} + Size={'lg'} + > + {detailModalContent} + +
+ ) +} + +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 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} 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; 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 }