diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 97c92e7147..2202c7ddb9 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -39,6 +39,7 @@ using GSF.Data; using GSF.Data.Model; using GSF.Web.Model; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using openXDA.Model; using SystemCenter.Model; @@ -57,16 +58,24 @@ private class extendedAssetGroupView: AssetGroupView } - [HttpGet, Route("{assetGroupID:int}/Assets")] - public IHttpActionResult GetAssets(int assetGroupID) + [HttpPost, Route("{assetGroupID:int}/Assets/{page:int}")] + public IHttpActionResult GetAssets([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) { if (GetRoles == string.Empty || User.IsInRole(GetRoles)) { + + HashSet validSortFields = new HashSet { "assetname", "assetkey", "assettype" }; + + if (!validSortFields.Contains(postData.OrderBy.ToLower())) + return BadRequest($"{postData.OrderBy} is not a valid sort field."); + using (AdoDataConnection connection = new AdoDataConnection(Connection)) { try { - string sql = @"SELECT + int recordsPerPage = PageSize ?? 50; + + string sql = @$"SELECT DISTINCT Asset.ID, AssetAssetGroup.AssetGroupID, @@ -91,9 +100,30 @@ GROUP BY Asset.VoltageKV, AssetType.Name, AssetAssetGroup.AssetGroupID - HAVING AssetAssetGroup.AssetGroupID = {0}"; + HAVING AssetAssetGroup.AssetGroupID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {recordsPerPage * page} ROWS + FETCH NEXT {recordsPerPage} ROWS ONLY + "; - return Ok(connection.RetrieveData(sql,assetGroupID)); + string countSql = @"SELECT + COUNT(DISTINCT AssetID) + FROM + AssetAssetGroup + WHERE + AssetGroupID = {0}"; + + int count = connection.ExecuteScalar(countSql, assetGroupID); + + DataTable results = connection.RetrieveData(sql, assetGroupID); + + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(results), + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage, + TotalRecords = count, + RecordsPerPage = recordsPerPage + }); } catch (Exception ex) { @@ -159,16 +189,23 @@ public IHttpActionResult RemoveAsset(int assetGroupID, int assetID) } } - [HttpGet, Route("{assetGroupID:int}/Meters")] - public IHttpActionResult GetMeters(int assetGroupID) + [HttpPost, Route("{assetGroupID:int}/Meters/{page:int}")] + public IHttpActionResult GetMeters([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) { if (GetRoles == string.Empty || User.IsInRole(GetRoles)) { + string[] validSortFields = { "name", "location" }; + + if (!validSortFields.Any(field => String.Equals(field, postData.OrderBy.ToLower()))) + return BadRequest($"{postData.OrderBy} is not a valid sort field."); + using (AdoDataConnection connection = new AdoDataConnection(Connection)) { try { - string sql = @"SELECT DISTINCT + int recordsPerPage = PageSize ?? 50; + + string sql = @$"SELECT DISTINCT Meter.ID, MeterAssetGroup.AssetGroupID, Meter.AssetKey, @@ -177,6 +214,28 @@ public IHttpActionResult GetMeters(int assetGroupID) Meter.Model, Location.Name as Location, COUNT(DISTINCT MeterAsset.AssetID) as MappedAssets + FROM + Meter LEFT JOIN + Location ON Meter.LocationID = Location.ID LEFT JOIN + MeterAsset ON Meter.ID = MeterAsset.MeterID LEFT JOIN + Asset ON MeterAsset.AssetID = Asset.ID LEFT JOIN + MeterAssetGroup ON Meter.ID = MeterAssetGroup.MeterID + GROUP BY + Meter.ID, + Meter.AssetKey, + Meter.Name, + Meter.Make, + Meter.Model, + Location.Name, + MeterAssetGroup.AssetGroupID + HAVING MeterAssetGroup.AssetGroupID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {recordsPerPage * page} ROWS + FETCH NEXT {recordsPerPage} ROWS ONLY" + ; + + string countSql = @"SELECT + COUNT(DISTINCT Meter.ID) FROM Meter LEFT JOIN Location ON Meter.LocationID = Location.ID LEFT JOIN @@ -193,7 +252,17 @@ GROUP BY MeterAssetGroup.AssetGroupID HAVING MeterAssetGroup.AssetGroupID = {0}"; - return Ok(connection.RetrieveData(sql,assetGroupID)); + int count = connection.ExecuteScalar(countSql, assetGroupID); + + DataTable results = connection.RetrieveData(sql, assetGroupID); + + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(results), + TotalRecords = count, + RecordsPerPage = recordsPerPage, + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage + }); } catch (Exception ex) { @@ -283,8 +352,8 @@ public IHttpActionResult GetUserAccounts(int assetGroupID) return Unauthorized(); } - [HttpGet, Route("{assetGroupID:int}/AssetGroups")] - public IHttpActionResult GetSubGroups(int assetGroupID) + [HttpPost, Route("{assetGroupID:int}/AssetGroups/{page:int}")] + public IHttpActionResult GetSubGroupsPaged([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) { if (GetRoles == string.Empty || User.IsInRole(GetRoles)) { @@ -292,9 +361,23 @@ public IHttpActionResult GetSubGroups(int assetGroupID) { try { - IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID); + int recordsPerPage = PageSize ?? 50; - return Ok(records); + TableOperations table = new TableOperations(connection); + + RecordRestriction recordRestriction = new RecordRestriction("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID); + + int count = table.QueryRecordCount(recordRestriction); + + IEnumerable records = new TableOperations(connection).QueryRecords(postData.OrderBy, postData.Ascending, page + 1, recordsPerPage, recordRestriction); + + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(records), + TotalRecords = count, + RecordsPerPage = recordsPerPage, + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage + }); } catch (Exception ex) { diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index 3779cb0198..2c02bfc048 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -71,7 +71,7 @@ public IHttpActionResult GetAssetLocationsPaged([FromBody] PostData postData, [F if (!GetAuthCheck()) return Unauthorized(); - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; PagedResults results = new PagedResults(); @@ -164,7 +164,7 @@ public IHttpActionResult GetAssetMetersPaged([FromBody] PostData postData, [From { try { - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; string[] sortFields = { "AssetKey", "Name", "Make", "Model" }; @@ -200,11 +200,16 @@ public IHttpActionResult GetAssetAssetConnections([FromBody] PostData postData, { if (GetRoles == string.Empty || User.IsInRole(GetRoles)) { + HashSet validSortFields = new HashSet { "assetname", "assetkey", "name" }; + + if (!validSortFields.Contains(postData.OrderBy.ToLower())) + return BadRequest($"{postData.OrderBy} is not a valid sort field."); + using (AdoDataConnection connection = new AdoDataConnection(Connection)) { try { - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; string[] sortFields = { "AssetName", "AssetKey", "Name" }; @@ -686,8 +691,8 @@ public IHttpActionResult PostExistingMeterForAsset(int assetID, int meterID) } } - [HttpGet, Route("{assetID:int}/ConnectedChannels")] - public IHttpActionResult GetAssetChannels(int assetID) + [HttpPost, Route("{assetID:int}/ConnectedChannels/{page:int}")] + public IHttpActionResult GetAssetChannels([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) { if (GetRoles == string.Empty || User.IsInRole(GetRoles)) { @@ -695,6 +700,8 @@ public IHttpActionResult GetAssetChannels(int assetID) { using (AdoDataConnection connection = new AdoDataConnection(Connection)) { + int recordsPerPage = PageSize ?? 50; + Asset asset = new TableOperations(connection).QueryRecordWhere("ID={0}", assetID); if (asset is null) throw (new Exception($"Asset ID={assetID} not found in OpenXDA database")); @@ -706,17 +713,43 @@ public IHttpActionResult GetAssetChannels(int assetID) if (connectedChannels.Count > 0) { - TableOperations tableOp = new TableOperations(connection); - // Channels get triplicated from Series Type ID in ChannelDetail View - IEnumerable uniqueChannels = new TableOperations(connection) - .QueryRecordsWhere($"ID in ({string.Join(", ", connectedChannels.Select(channels => channels.ID))})") - .DistinctBy(c => c.ID); - return Ok(uniqueChannels); + string countSql = $@" + SELECT COUNT(*) + FROM ChannelDetail + WHERE ID in ({string.Join(", ", connectedChannels.Select(channels => channels.ID))}) + "; + + string sql = @$" + SELECT * + FROM ChannelDetail + WHERE ID in ({string.Join(", ", connectedChannels.Select(channels => channels.ID))}) + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {page * recordsPerPage} ROWS + FETCH NEXT {recordsPerPage} ROWS ONLY + "; + + int count = connection.ExecuteScalar(countSql); + + DataTable results = connection.RetrieveData(sql); + + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(results), + RecordsPerPage = recordsPerPage, + TotalRecords = count, + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage + }); } else { - return Ok(new List()); + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(new List()), // just return an empty list + RecordsPerPage = recordsPerPage, + TotalRecords = 0, + NumberOfPages = 0 + }); } } } catch (Exception ex) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDALineController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDALineController.cs index e5afaa32cb..48628cab45 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDALineController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDALineController.cs @@ -31,6 +31,7 @@ using GSF.Data; using GSF.Data.Model; using GSF.Web.Model; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using openXDA.Model; using SystemCenter.Model; @@ -94,6 +95,38 @@ record = record.Concat(new TableOperations(connection).QueryRecords } + [HttpPost, Route("{lineID:int}/LineSegments/{page:int}")] + public IHttpActionResult GetLineSegmentsForLinePaged([FromBody] PostData postData, [FromUri] int lineID, [FromUri] int page) + { + if (GetRoles == string.Empty || User.IsInRole(GetRoles)) + { + int recordsPerPage = PageSize ?? 50; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + RecordRestriction restriction = new RecordRestriction(@"ID in (select ChildID from AssetRelationship where AssetRelationshipTypeID = (SELECT ID FROM AssetRelationshipType WHERE Name = 'Line-LineSegment') AND ParentID = {0}) + OR ID in (select ParentID from AssetRelationship where AssetRelationshipTypeID = (SELECT ID FROM AssetRelationshipType WHERE Name = 'Line-LineSegment') AND ChildID = {0})", lineID); + + TableOperations tbl = new TableOperations(connection); + + int count = tbl.QueryRecordCount(restriction); + + IEnumerable records = tbl.QueryRecords(postData.OrderBy, postData.Ascending, page + 1, recordsPerPage, restriction); + + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(records), + RecordsPerPage = recordsPerPage, + TotalRecords = count, + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage + }); + } + } + else + return Unauthorized(); + + } + public override IHttpActionResult Post([FromBody] JObject record) { if (PostRoles == string.Empty || User.IsInRole(PostRoles)) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Meters/OpenXDAMeterConfigurationController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Meters/OpenXDAMeterConfigurationController.cs index 2e07048530..bbfe79f3fd 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Meters/OpenXDAMeterConfigurationController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Meters/OpenXDAMeterConfigurationController.cs @@ -45,7 +45,7 @@ public class OpenXDAMeterConfigurationController : ModelController(countQuery, meterID); + + DataTable channelTable = connection.RetrieveData(ChannelQuery, meterID); + string channelJSON = JsonConvert.SerializeObject(channelTable); + JArray channelArray = JArray.Parse(channelJSON); DataTable seriesTable = connection.RetrieveData(SeriesQuery, meterID); string seriesJSON = JsonConvert.SerializeObject(seriesTable); @@ -395,7 +402,13 @@ IEnumerable FilterChannels() } } - return Ok(FilterChannels()); + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(FilterChannels()), + TotalRecords = channelCount, + NumberOfPages = (channelCount + recordsPerPage - 1) / recordsPerPage, + RecordsPerPage = recordsPerPage + }); } diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs index 3bf4f15680..d33619a1c8 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDAControllers.cs @@ -111,7 +111,7 @@ public IHttpActionResult RecentFailures([FromBody] PostData postData, [FromUri] if (!GetAuthCheck()) return Unauthorized(); - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; List param = new(); diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs index 9038b85caf..bc943e78cc 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs @@ -217,7 +217,7 @@ public IHttpActionResult GetMetersForLocation(int locationID, int page, int asc, if (!string.IsNullOrEmpty(GetRoles) && User.IsInRole(GetRoles)) return Unauthorized(); - int recordsPerPage = 50; + int recordsPerPage = PageSize ?? 50; using (AdoDataConnection connection = new AdoDataConnection(Connection)) { int totalRecords = connection.ExecuteScalar($@" @@ -256,7 +256,7 @@ FROM Meter [HttpGet, Route("{locationID:int}/Assets/{page:int}/{asc:int}/{orderBy}")] public IHttpActionResult GetAssetsForLocation(int locationID, int page, int asc, string orderBy) { - int recordsPerPage = 50; + int recordsPerPage = PageSize ?? 50; if (!string.IsNullOrEmpty(GetRoles) && User.IsInRole(GetRoles)) return Unauthorized(); @@ -323,7 +323,7 @@ public IHttpActionResult GetImagesForLocation(int locationID, int page) if (Directory.Exists(Path.Combine(path, key))) { IEnumerable imagePaths = Directory.GetFiles(Path.Combine(path, key)).Select(fp => new FileInfo(fp).Name); - return Ok(PageImagePaths(imagePaths, page, Take ?? 50)); + return Ok(PageImagePaths(imagePaths, page, PageSize ?? 50)); } else return Ok(new PagedResults() @@ -331,7 +331,7 @@ public IHttpActionResult GetImagesForLocation(int locationID, int page) Data = JsonConvert.SerializeObject(new string[0]), TotalRecords = 0, NumberOfPages = 0, - RecordsPerPage = Take ?? 50 + RecordsPerPage = PageSize ?? 50 }); } else diff --git a/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs b/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs index 563d97416a..3b5ff0f9be 100644 --- a/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs +++ b/Source/Applications/SystemCenter/Controllers/SystemCenter/SystemCenterController.cs @@ -633,7 +633,7 @@ public IHttpActionResult GetAdditionalFieldsForTable(string openXDAParentTable, { string orderByExpression = DefaultSort; - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; if (sort != null && sort != string.Empty) orderByExpression = $"{sort} {(ascending == 1 ? "ASC" : "DESC")}"; diff --git a/Source/Applications/SystemCenter/Model/DataFile.cs b/Source/Applications/SystemCenter/Model/DataFile.cs index cc3dd6d1c6..5aa8a88573 100644 --- a/Source/Applications/SystemCenter/Model/DataFile.cs +++ b/Source/Applications/SystemCenter/Model/DataFile.cs @@ -321,7 +321,7 @@ public override IHttpActionResult GetPagedList([FromBody] PostData postData, int }).ToArray(); int recordCount = CountSearchResults(postData); - int recordPerPage = Take ?? 50; + int recordPerPage = PageSize ?? 50; return Ok(new PagedResults() { Data = JsonConvert.SerializeObject(results), @@ -360,7 +360,7 @@ public override IHttpActionResult GetPagedList([FromBody] PostData postData, int }).ToArray(); int recordCount = CountSearchResults(postData); - int recordPerPage = Take ?? 50; + int recordPerPage = PageSize ?? 50; return Ok(new PagedResults() { Data = JsonConvert.SerializeObject(results), @@ -398,7 +398,7 @@ public override IHttpActionResult GetPagedList([FromBody] PostData postData, int }).ToArray(); int recordCount = CountSearchResults(postData); - int recordPerPage = Take ?? 50; + int recordPerPage = PageSize ?? 50; return Ok(new PagedResults() { Data = JsonConvert.SerializeObject(results), diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index b74dc2becf..a5a7bebc89 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -111,7 +111,6 @@ public class DeviceHealthReport [RoutePrefix("api/DeviceHealthReport")] public class DeviceHealthReportController : ModelController { - public int PagingAmount { get; set; } = 50; public class DailyStatisticsRecord { [PrimaryKey(true)] @@ -144,7 +143,7 @@ public override IHttpActionResult GetPagedList([FromBody] PostData postData, int { PagedResults pagedReports = new() { - RecordsPerPage = 50 + RecordsPerPage = PageSize ?? 50 }; PostData openMicRequestBody = new() diff --git a/Source/Applications/SystemCenter/Model/Node.cs b/Source/Applications/SystemCenter/Model/Node.cs index 2ecac66e82..4b94d86db6 100644 --- a/Source/Applications/SystemCenter/Model/Node.cs +++ b/Source/Applications/SystemCenter/Model/Node.cs @@ -30,7 +30,7 @@ namespace SystemCenter.Model { - [TableName("Node"), ReturnLimit(50), + [TableName("Node"), CustomView(@" SELECT Node.ID, diff --git a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs index 2906643a64..cf97222c1f 100644 --- a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs +++ b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs @@ -100,6 +100,40 @@ public IHttpActionResult GetUsers(string groupID) "(SELECT COUNT(ID) FROM SecurityGroupUserAccount WHERE SecurityGroupID = {0} AND UserAccountID = UserAccount.ID) > 0", groupID))); } + [HttpPost] + [Route("Users/PagedList/{groupID}/{page:int}")] + public IHttpActionResult GetPagedUsers([FromBody] PostData postData, [FromUri] String groupID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + HashSet sortFields = new HashSet { "phone", "email", "firstname", "lastname", "accountname" }; + if (!sortFields.Contains(postData.OrderBy.ToLower())) + return BadRequest($"{postData.OrderBy} is not a valid sort field."); + + int recordsPerPage = PageSize ?? 50; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $@"SELECT UserAccount.*, UserAccount.Name as AccountName + FROM SecurityGroupUserAccount JOIN UserAccount ON UserAccountID = UserAccount.ID WHERE SecurityGroupID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {page * recordsPerPage} ROWS FETCH NEXT {recordsPerPage} ROWS ONLY"; + string countSql = "SELECT COUNT(*) FROM SecurityGroupUserAccount JOIN UserAccount ON UserAccountID = UserAccount.ID WHERE SecurityGroupID = {0}"; + + DataTable results = connection.RetrieveData(sql, groupID.ToString()); + int count = connection.ExecuteScalar(countSql, groupID); + + return Ok(new PagedResults() + { + Data= JsonConvert.SerializeObject(results), + TotalRecords = count, + NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage, + RecordsPerPage = recordsPerPage + }); + } + } + [HttpPost] [Route("{groupID}/PostRoles")] public IHttpActionResult PostGroupRoles([FromBody] IEnumerable record, string groupID) @@ -274,7 +308,7 @@ protected override DataTable GetSearchResults(PostData postData, int? page) if (page is int p) // page manually, because filtering post-search requires it. { - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; DataRow[] rows = dataTable.AsEnumerable() .Skip((p) * recordsPerPage) .Take(recordsPerPage) diff --git a/Source/Applications/SystemCenter/Model/Security/UserAccount.cs b/Source/Applications/SystemCenter/Model/Security/UserAccount.cs index 438efbf736..d5ee735dc1 100644 --- a/Source/Applications/SystemCenter/Model/Security/UserAccount.cs +++ b/Source/Applications/SystemCenter/Model/Security/UserAccount.cs @@ -242,7 +242,7 @@ protected override DataTable GetSearchResults(PostData postData, int? page) if (page is int p)// page manually, because filtering post-search requires it. { - int recordsPerPage = Take ?? 50; + int recordsPerPage = PageSize ?? 50; DataRow[] rows = dataTable.AsEnumerable() .Skip((p) * recordsPerPage) .Take(recordsPerPage) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx index 61ae1afcfb..3c5c63d608 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx @@ -23,9 +23,9 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; +import { Application } from '@gpa-gemstone/application-typings'; import { PhaseSlice, MeasurmentTypeSlice } from '../Store/Store' -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useAppSelector } from '../hooks'; import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; @@ -67,6 +67,10 @@ interface ChannelDetail { //TODO: Move to Gemstone const AssetChannelWindow = (props: IProps) => { const [assetChannels, setAssetChannels] = React.useState([]); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; @@ -75,39 +79,35 @@ const AssetChannelWindow = (props: IProps) => { const [ascending, setAscending] = React.useState(true); React.useEffect(() => { - let channelHandle = getChannels(); - - Promise.all([channelHandle]); - - return () => { - if (channelHandle != null && channelHandle.abort != null) - channelHandle.abort(); - } - }, [props.ID]); - - function getChannels(): JQuery.jqXHR { setStatus('loading'); - return $.ajax( + + const handle = $.ajax( { - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels/${page}`, contentType: "application/json; charset=utf-A", dataType: 'json', cache: true, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) } ).done( - (d: Array) => { - const sortedChannels = sortData(sortField, ascending, d); - setAssetChannels(sortedChannels) + (d) => { + setAssetChannels(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); setStatus('idle'); } ).fail(() => setStatus('error')); - } - function sortData(key: keyof ChannelDetail, ascending: boolean, data: ChannelDetail[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, [props.ID, sortField, ascending, page]); if (status == 'error' || pStatus == 'error' || mtStatus == 'error') return
@@ -153,100 +153,116 @@ const AssetChannelWindow = (props: IProps) => {

Channels:

+
+
+

+ {`Displaying Asset Channel(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + assetChannels.length} out of ${totalRecords}`} +

+
+
- - TableClass="table table-hover" - Data={assetChannels} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(assetChannels, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetChannels(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(assetChannels, [d.colKey], ["asc"]); - setAssetChannels(ordered); - } - }} - TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Label - - - Key={'MeterName'} - AllowSort={true} - Field={'MeterName'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - Content={(row) => {row.item.MeterName}} - > Meter Name - - - Key={'AssetName'} - AllowSort={true} - Field={'AssetName'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - Content={(row) => (row.item.AssetID !== props.ID ? - {row.item.AssetName} : - row.item.AssetName - )} - > Asset Name - - - Key={'MeasurementType'} - AllowSort={true} - Field={'MeasurementType'} - HeaderStyle={{ width: '8%' }} - RowStyle={{ width: '8%' }} - > Type - - - Key={'Phase'} - AllowSort={true} - Field={'Phase'} - HeaderStyle={{ width: '8%' }} - RowStyle={{ width: '8%' }} - > Phase - - - Key={'AssetID'} - AllowSort={true} - Field={'AssetID'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={row => row.item.AssetID !== props.ID ? : <>} - > Shared Via Asset Connection - - - Key={'Description'} - AllowSort={true} - Field={'Description'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Description - - +
+
+ + TableClass="table table-hover" + Data={assetChannels} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + > Label + + + Key={'MeterName'} + AllowSort={true} + Field={'MeterName'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + Content={(row) => {row.item.MeterName}} + > Meter Name + + + Key={'AssetName'} + AllowSort={true} + Field={'AssetName'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + Content={(row) => (row.item.AssetID !== props.ID ? + {row.item.AssetName} : + row.item.AssetName + )} + > Asset Name + + + Key={'MeasurementType'} + AllowSort={true} + Field={'MeasurementType'} + HeaderStyle={{ width: '8%' }} + RowStyle={{ width: '8%' }} + > Type + + + Key={'Phase'} + AllowSort={true} + Field={'Phase'} + HeaderStyle={{ width: '8%' }} + RowStyle={{ width: '8%' }} + > Phase + + + Key={'AssetID'} + AllowSort={true} + Field={'AssetID'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={row => row.item.AssetID !== props.ID ? : <>} + > Shared Via Asset Connection + + + Key={'Description'} + AllowSort={true} + Field={'Description'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Description + + +
+
+
+
+ setPage(p - 1)} + /> +
+
); } export default AssetChannelWindow -; \ No newline at end of file + ; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/LineSegmentWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/LineSegmentWindow.tsx index 021b807adf..03568e4528 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/LineSegmentWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/LineSegmentWindow.tsx @@ -24,10 +24,9 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import LineSegmentWizard from './FawgLineSegmentWizard/LineSegmentWizard'; -import moment from 'moment'; import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; import { ToolTip } from '@gpa-gemstone/react-forms'; @@ -37,33 +36,38 @@ function LineSegmentWindow(props: IProps): JSX.Element { const [segments, setSegments] = React.useState>([]); const [sortKey, setSortKey] = React.useState('AssetName'); const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [segmentStatus, setSegmentStatus] = React.useState('uninitiated'); const [showFawg, setShowFawg] = React.useState(false); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); React.useEffect(() => { - const h = getSegments(); - return () => { if (h != null && h.abort != null) h.abort(); } - }, [props.ID]); - - function getSegments() { - return $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Line/${props.ID}/LineSegments?_=${moment()}`, + setSegmentStatus('loading'); + const h = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/Line/${props.ID}/LineSegments/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true - }).done((data: Array) => { - const sortedSegments = sortData(sortKey, ascending, data); - setSegments(sortedSegments) + async: true, + data: JSON.stringify({ orderBy: sortKey, ascending: ascending, searches: [] }) + }).done((d) => { + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setSegments(JSON.parse(d.Data)) + setSegmentStatus('idle'); props.OnChange(); - }); - } - - function sortData(key: string, ascending: boolean, data: OpenXDA.Types.LineSegment[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }).fail(() => setSegmentStatus('error')); + return () => { if (h != null && h.abort != null) h.abort(); } + }, [props.ID, page, ascending, sortKey, refreshTrigger]); function hasPermissions(): boolean { if (roles.indexOf('Administrator') < 0 && roles.indexOf('Engineer') < 0) @@ -71,7 +75,22 @@ function LineSegmentWindow(props: IProps): JSX.Element { return true; } - let header = (

{"Line Segments: "}

); + let header = ( <> +
+
+

{"Line Segments: "}

+
+
+
+
+

+ {segmentStatus === 'error' ? 'Could not complete Search' : + segmentStatus === 'loading' ? 'Loading...' : + `Displaying Line Segment(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + segments.length} out of ${totalRecords}`} +

+
+
+ ) const tableContent = ( <> @@ -82,14 +101,10 @@ function LineSegmentWindow(props: IProps): JSX.Element { OnSort={(d) => { if (d.colKey == sortKey) { setAscending(!ascending); - const ordered = _.orderBy(segments, [d.colKey], [(!ascending ? "asc" : "desc")]); - setSegments(ordered); } else { setAscending(true); setSortKey(d.colField); - const ordered = _.orderBy(segments, [d.colKey], ["asc"]); - setSegments(ordered); } }} TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} @@ -181,7 +196,7 @@ function LineSegmentWindow(props: IProps): JSX.Element { > End? - {showFawg ? { setShowFawg(false); getSegments(); }} /> : null} + {showFawg ? { setShowFawg(false); setRefreshTrigger(val => !val)}} /> : null} ); const wizardButton = (); @@ -199,8 +214,21 @@ function LineSegmentWindow(props: IProps): JSX.Element {
{header}
-
+
+
+
{tableContent} +
+
+
+
+ setPage(p -1)} + /> +
+
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/SourceImpedanceWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/SourceImpedanceWindow.tsx index fd76a590b8..57fb366b9a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/SourceImpedanceWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetAttribute/SourceImpedanceWindow.tsx @@ -24,20 +24,19 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { AssetAttributes } from './Asset'; -import LineSegmentAttributes from './LineSegment'; -import { LoadingScreen, Modal, Warning, Search, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { LoadingScreen, Modal, Warning, Search, ServerErrorIcon, GenericController } from '@gpa-gemstone/react-interactive'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import moment from 'moment'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { LocationSlice, SourceImpedanceSlice } from '../Store/Store'; -import { IsInteger, IsNumber } from '@gpa-gemstone/helper-functions'; +import { LocationSlice } from '../Store/Store'; +import { IsNumber } from '@gpa-gemstone/helper-functions'; import { Input, Select, ToolTip } from '@gpa-gemstone/react-forms'; import { SelectRoles } from '../Store/UserSettings'; const newImpedance: OpenXDA.Types.SourceImpedance = { RSrc: 0, XSrc: 0, AssetLocationID: null, ID: 0 } +const SourceImpedanceController = new GenericController(`${homePath}api/OpenXDA/SourceImpedance`, "AssetLocationID", false); + function SourceImpedanceWindow(props: { ID: number }): JSX.Element { const dispatch = useAppDispatch(); const locations = useAppSelector(LocationSlice.Data); @@ -46,13 +45,18 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { const [assetLocations, setAssetLocations] = React.useState([]); const [aLStatus, setALStatus] = React.useState('uninitiated'); - const sourceImpedances = useAppSelector(SourceImpedanceSlice.SearchResults); - const sourceImpedanceStatus = useAppSelector(SourceImpedanceSlice.SearchStatus); + const [sourceImpedances, setSourceImpedances] = React.useState([]); + const [sourceImpedanceStatus, setSourceImpedanceStatus] = React.useState('uninitiated'); const [ascending, setAscending] = React.useState(true); const [sortKey, setSortKey] = React.useState('AssetLocationID'); - const [data, setData] = React.useState([]); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [showAdd, setShowAdd] = React.useState(false); const [showWarning, setshowWarning] = React.useState(false); @@ -65,16 +69,29 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { //#ToDo Swap Type to query React.useEffect(() => { + setSourceImpedanceStatus('loading'); const filter = [ { FieldName: "AssetLocationID", Operator: "IN", Type: "query", IsPivotColumn: false, SearchText: `(SELECT ID FROM AssetLocation WHERE AssetID=${props.ID})` } ] as Search.IFilter[] - dispatch(SourceImpedanceSlice.DBSearch({ filter })) - }, [props.ID]); - React.useEffect(() => { - const sortedData = sortData(sortKey, ascending, sourceImpedances); - setData(sortedData); - }, [sourceImpedances]); + const handle = SourceImpedanceController.PagedSearch(filter, sortKey, ascending, page); + + handle.done((d) => { + setSourceImpedances(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setSourceImpedanceStatus('idle'); + }) + + handle.fail(() => setSourceImpedanceStatus('error')) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + }, [props.ID, sortKey, ascending, page, refreshTrigger]); React.useEffect(() => { const h = getAssetLocations(props.ID); @@ -86,15 +103,6 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { dispatch(LocationSlice.Fetch()); }, [locationStatus]) - React.useEffect(() => { - if (sourceImpedanceStatus == 'changed' || sourceImpedanceStatus == 'uninitiated') { - const filter = [ - { FieldName: "AssetLocationID", Operator: "IN", Type: "query", IsPivotColumn: false, SearchText: `(SELECT ID FROM AssetLocation WHERE AssetID=${props.ID})` } - ] as Search.IFilter[] - dispatch(SourceImpedanceSlice.DBSearch({ filter })) - } - }, [sourceImpedanceStatus]) - function getAssetLocations(assetID: number) { setALStatus('loading'); return $.ajax({ @@ -111,10 +119,6 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { }); } - function sortData(key: keyof OpenXDA.Types.SourceImpedance, ascending: boolean, data: OpenXDA.Types.SourceImpedance[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getLocationName(si: OpenXDA.Types.SourceImpedance) { const al = assetLocations.find(al => al.ID == si.AssetLocationID); if (al == null) @@ -132,7 +136,7 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element {

Line Source Impedances:

- +
) @@ -167,88 +171,105 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { return ( <> -
-
-

Line Source Impedances:

-
-
- - TableClass="table table-hover" - Data={data} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortKey) { - setAscending(!ascending); - const ordered = _.orderBy(data, [d.colKey], [(!ascending ? "asc" : "desc")]); - setData(ordered); - } - else { - setAscending(true); - setSortKey(d.colKey as keyof OpenXDA.Types.SourceImpedance); - const ordered = _.orderBy(data, [d.colKey], ["asc"]); - setData(ordered); - } - }} - TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden', height: '100%'}} - TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'AssetLocationID'} - AllowSort={true} - Field={'AssetLocationID'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => getLocationName(item)} - > Substation - - - Key={'RSrc'} - AllowSort={true} - Field={'RSrc'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > R (pu) - - - Key={'XSrc'} - AllowSort={true} - Field={'XSrc'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > X (pu) - - - Key={'EditDelete'} - AllowSort={false} - HeaderStyle={{ width: 80, paddingLeft: 0, paddingRight: 5 }} - RowStyle={{ width: 80, paddingLeft: 0, paddingRight: 5 }} - Content={({ item }) => <> - - - } - >

- - -
-
+
+
+
+

Line Source Impedances:

+
+
+
+

+ {sourceImpedanceStatus === 'loading' ? 'Loading...' : + `Displaying Source Impedance(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + sourceImpedances.length} out of ${totalRecords}`} +

+
+
+
+
+
+ + TableClass="table table-hover" + Data={sourceImpedances} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortKey) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortKey(d.colKey as keyof OpenXDA.Types.SourceImpedance); + } + }} + TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden', height: '100%' }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'AssetLocationID'} + AllowSort={true} + Field={'AssetLocationID'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => getLocationName(item)} + > Substation + + + Key={'RSrc'} + AllowSort={true} + Field={'RSrc'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > R (pu) + + + Key={'XSrc'} + AllowSort={true} + Field={'XSrc'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > X (pu) + + + Key={'EditDelete'} + AllowSort={false} + HeaderStyle={{ width: 80, paddingLeft: 0, paddingRight: 5 }} + RowStyle={{ width: 80, paddingLeft: 0, paddingRight: 5 }} + Content={({ item }) => <> + + + } + >

+ + +
+
+
+ setPage(p - 1)} + Current={page + 1} + /> +
+
+
+
@@ -256,17 +277,17 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element {

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

+
-
{ if (confirm) dispatch(SourceImpedanceSlice.DBAction({ verb: 'DELETE', record: newEditImpedance })); setshowWarning(false); }} /> + CallBack={(confirm) => { if (confirm) SourceImpedanceController.DBAction('DELETE', newEditImpedance).then(() => setRefreshTrigger(val => !val)); setshowWarning(false); }} /> { if (confirm && newEdit == 'Edit') - dispatch(SourceImpedanceSlice.DBAction({ verb: 'PATCH', record: newEditImpedance })); + SourceImpedanceController.DBAction('PATCH', newEditImpedance).then(() => setRefreshTrigger(val => !val)); if (confirm && newEdit == 'New') - dispatch(SourceImpedanceSlice.DBAction({ verb: 'POST', record: newEditImpedance })); + SourceImpedanceController.DBAction('POST', newEditImpedance).then(() => setRefreshTrigger(val => !val)); setShowAdd(false); }} CancelText={'Close'} @@ -275,7 +296,7 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element { ConfirmShowToolTip={validImpedance(newEditImpedance).length > 0} ConfirmToolTipContent={validImpedance(newEditImpedance).map((t, i) =>

{t}

)} > -
+
Record={newEditImpedance} Label={'Substation'} Field={'AssetLocationID'} Options={assetLocations.map(al => ({ @@ -289,11 +310,11 @@ function SourceImpedanceWindow(props: { ID: number }): JSX.Element {
Record={newEditImpedance} Field={'RSrc'} Label={'R (pu)'} Feedback={'A valid Resistance is required.'} Valid={valid} Setter={(r) => setNewEditImpedance(r)} /> -
+
Record={newEditImpedance} Field={'XSrc'} Label={'X (pu)'} Feedback={'A valid Reactance is required.'} Valid={valid} Setter={(r) => setNewEditImpedance(r)} />
-
+
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx index 2ca4d51c7a..651c90bcc4 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx @@ -25,9 +25,9 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; -import { AssetGroupSlice, AssetTypeSlice } from '../Store/Store'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; +import { AssetTypeSlice } from '../Store/Store'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; @@ -37,15 +37,19 @@ import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; -function AssetAssetGroupWindow(props: { AssetGroupID: number}) { +function AssetAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); const [assetList, setAssetList] = React.useState>([]); const [sortKey, setSortKey] = React.useState('AssetName'); const [ascending, setAscending] = React.useState(true); const [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeAsset, setRemoveAsset] = React.useState(-1); - + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [assetStatus, setAssetStatus] = React.useState('uninitiated'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const assetType = useAppSelector(AssetTypeSlice.Data); const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); const dispatch = useAppDispatch(); @@ -54,42 +58,43 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { const roles = useAppSelector(SelectRoles); React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]); - - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - - function getData() { if (props.AssetGroupID == null) return () => { }; + setAssetStatus('loading'); + let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) }) - handle.done((data: Array) => { - const sortedData = sortData(sortKey, ascending, data); - setAssetList(sortedData); + handle.done((d) => { + setAssetList(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setAssetStatus('idle'); }); - + + handle.fail(() => setAssetStatus('error')) + return function cleanup() { if (handle.abort != null) handle.abort(); } - } + }, [props.AssetGroupID, refreshTrigger, ascending, page, sortKey]); - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedAsset[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + React.useEffect(() => { + if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') + dispatch(AssetTypeSlice.Fetch()); + }, [assetTypeStatus]); function saveItems(items: SystemCenter.Types.DetailedAsset[]) { @@ -103,7 +108,7 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } @@ -118,7 +123,7 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } function hasPermissions(): boolean { @@ -133,99 +138,113 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Transmission Assets:

+
+
+
+
+

Transmission Assets:

+
+ +
+
+
+

+ {assetStatus === 'error' ? 'Could not complete Search' : + assetStatus === 'loading' ? 'Loading...' : + `Displaying Asset(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + assetList.length} out of ${totalRecords}`} +

+
-
-
-
-
- - TableClass="table table-hover" - Data={assetList} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey === "Remove") - return; - - if (d.colKey === sortKey) { - setAscending(!ascending); - const ordered = _.orderBy(assetList, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetList(ordered); - } - else { - setAscending(true); - setSortKey(d.colKey); - const ordered = _.orderBy(assetList, [d.colKey], ["asc"]); - setAssetList(ordered); - } - }} - OnClick={handleSelect} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'AssetName'} - AllowSort={true} - Field={'AssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'AssetKey'} - AllowSort={true} - Field={'AssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Key - - - Key={'AssetType'} - AllowSort={true} - Field={'AssetType'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Type - - - Key={'Remove'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => <> - - } - >

- - +
+
+ + TableClass="table table-hover" + Data={assetList} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey === "Remove") + return; + + if (d.colKey === sortKey) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortKey(d.colKey); + } + }} + OnClick={handleSelect} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'AssetName'} + AllowSort={true} + Field={'AssetName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Name + + + Key={'AssetKey'} + AllowSort={true} + Field={'AssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Key + + + Key={'AssetType'} + AllowSort={true} + Field={'AssetType'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Type + + + Key={'Remove'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => <> + + } + >

+ + +
+
+
+ setPage(p - 1)} + /> +
+
-
-
-
+
+
-
+

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

-
+
{ @@ -234,7 +253,7 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { saveItems(selected.filter(items => assetList.findIndex(g => g.ID == items.ID) < 0)) }} /> -1} Title={'Remove Asset from Asset Group'} Message={'This will remove the Transmission Asset from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeAsset); setRemoveAsset(-1); }} /> - + ) } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx index 9faff924ad..c3f9bd8745 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx @@ -24,29 +24,32 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA } from '@gpa-gemstone/application-typings'; +import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { AssetGroupSlice } from '../Store/Store'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { Search, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; - -function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { +function AssetGroupAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); - const dispatch = useAppDispatch(); const [groupList, setGroupList] = React.useState>([]); + const [groupStatus, setGroupStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); const [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeGroup, setRemoveGroup] = React.useState(-1); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); @@ -61,37 +64,36 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { }; React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]); - function getData() { - if (props.AssetGroupID == null) - return () => { }; + setGroupStatus('loading'); let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) }); - handle.done((data: Array) => { - const sortedData = sortData(sortField, ascending, data); - setGroupList(sortedData); + handle.done((d) => { + setGroupList(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setGroupStatus('idle'); }); - + + handle.fail(() => setGroupStatus('error')) + return function cleanup() { if (handle.abort != null) handle.abort(); } - } - - function sortData(key: string, ascending: boolean, data: OpenXDA.Types.AssetGroup[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }, [props.AssetGroupID, refreshTrigger, page, sortField, ascending]); function getEnum(setOptions, field) { let handle = null; @@ -123,11 +125,11 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } function saveItems(items: OpenXDA.Types.AssetGroup[]) { - + let handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddAssetGroups`, @@ -138,7 +140,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } @@ -151,92 +153,105 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Asset Groups in Asset Group:

+
+
+
+
+

Asset Groups in Asset Group:

+
+
+
+
+

+ {groupStatus === 'error' ? 'Could not complete Search' : + groupStatus === 'loading' ? 'Loading...' : + `Displaying Subgroups(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + groupList.length} out of ${totalRecords}`} +

+
-
-
-
- - TableClass="table table-hover" - Data={groupList} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(groupList, [d.colKey], [(!ascending ? "asc" : "desc")]); - setGroupList(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(groupList, [d.colKey], ["asc"]); - setGroupList(ordered); - } - }} - OnClick={(data) => { navigate(`${homePath}index.cshtml?name=AssetGroup&AssetGroupID=${data.row.ID}`); }} - TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - TbodyStyle={{ display: 'block', width: '100%', overflowY: 'auto', flex: 1 }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'Assets'} - AllowSort={true} - Field={'Assets'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Num. of Assets - - - Key={'Meters'} - AllowSort={true} - Field={'Meters'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Num. of Meters - - - Key={'AssetGroups'} - AllowSort={true} - Field={'AssetGroups'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Num. of Asset Groups - - - Key={'Remove'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => <> - - } - >

- - +
+
+ + TableClass="table table-hover" + Data={groupList} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + OnClick={(data) => { navigate(`${homePath}index.cshtml?name=AssetGroup&AssetGroupID=${data.row.ID}`); }} + TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', width: '100%', overflowY: 'auto', flex: 1 }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Name + + + Key={'Assets'} + AllowSort={true} + Field={'Assets'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Num. of Assets + + + Key={'Meters'} + AllowSort={true} + Field={'Meters'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Num. of Meters + + + Key={'AssetGroups'} + AllowSort={true} + Field={'AssetGroups'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Num. of Asset Groups + + + Key={'Remove'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => <> + + } + >

+ + +
+
+
+ setPage(p - 1)} + /> +
+
- -
-
+
@@ -270,7 +285,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { -1} Title={'Remove Asset Group from Asset Group'} Message={'This will remove the Asset Group from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeGroup); setRemoveGroup(-1); }} />
- + ); } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx index 3f543a15f4..e896dfd140 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx @@ -25,65 +25,67 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ByMeterSlice } from '../Store/Store'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { Search, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { SelectRoles } from '../Store/UserSettings'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { AssetGroupSlice } from '../Store/Store'; +import { useAppSelector } from '../hooks'; declare var homePath: string; -function MeterAssetGroupWindow(props: { AssetGroupID: number}) { +function MeterAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); - const dispatch = useAppDispatch(); const [meterList, setMeterList] = React.useState>([]); + const [meterStatus, setMeterStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeMeter, setRemoveMeter] = React.useState(-1); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]) - function getData() { - if (props.AssetGroupID == null) - return () => { }; + setMeterStatus('loading'); let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Meters`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Meters/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) }); - handle.done((data: Array) => { - const sortedData = sortData(sortField, ascending, data); - setMeterList(sortedData); + handle.done((d) => { + setMeterList(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setMeterStatus('idle') }); - + + handle.fail(() => setMeterStatus('error')) + return function cleanup() { if (handle.abort != null) handle.abort(); } - } - - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedMeter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }, [props.AssetGroupID, page, sortField, ascending, refreshTrigger]) function getEnum(setOptions, field) { let handle = null; @@ -146,7 +148,7 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } function removeItem(id: number) { @@ -159,7 +161,7 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => setRefreshTrigger(val => !val)) } function hasPermissions(): boolean { @@ -174,89 +176,102 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Meters in Asset Group:

+
+
+
+
+

Meters in Asset Group:

+
+
+
+
+

+ {meterStatus === 'error' ? 'Could not complete Search' : + meterStatus === 'loading' ? 'Loading...' : + `Displaying Meter(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + meterList.length} out of ${totalRecords}`} +

+
-
-
-
- - TableClass="table table-hover" - Data={meterList} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Remove') return; - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(meterList, [d.colKey], [(!ascending ? "asc" : "desc")]); - setMeterList(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(meterList, [d.colKey], ["asc"]); - setMeterList(ordered); - } - }} - OnClick={handleSelect} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'Location'} - AllowSort={true} - Field={'Location'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Substation - - - Key={'Remove'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => - - } - >

- - +
+
+ + TableClass="table table-hover" + Data={meterList} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == 'Remove') return; + if (d.colKey == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + OnClick={handleSelect} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Name + + + Key={'Location'} + AllowSort={true} + Field={'Location'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Substation + + + Key={'Remove'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => + + } + >

+ + +
+
+
+ setPage(p - 1)} + /> +
+
- -
-
-
+
+

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

-
+
Model - -1} Title={'Remove Meter from Asset Group'} Message={'This will remove the Meter from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeMeter); setRemoveMeter(-1); }} /> + -1} Title={'Remove Meter from Asset Group'} Message={'This will remove the Meter from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeMeter); setRemoveMeter(-1); }} /> ); } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx index 239de2c326..8533e425f2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx @@ -29,7 +29,7 @@ import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { useAppSelector, useAppDispatch } from '../../hooks'; import { MeasurementCharacteristicSlice, MeasurmentTypeSlice, PhaseSlice } from '../../Store/Store'; import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ChannelScalingWrapper, ChannelScalingType, IMultiplier } from './ChannelScalingWrapper'; import { Input, ToolTip } from '@gpa-gemstone/react-forms'; import { SelectRoles } from '../../Store/UserSettings'; @@ -40,7 +40,10 @@ interface IProps { Channels: OpenXDA.Types.Channel[], UpdateChannels: (channels: OpenXDA.Types.Channel[]) => void, ChannelStatus?: Application.Types.Status, - Key?: string + Key?: string, + Page?: number, + SetPage?: React.Dispatch>, + TotalPages?: number } @@ -201,7 +204,7 @@ const ChannelScalingForm = (props: IProps) => { }} Valid={(f) => true} />
-
+
TableClass="table table-hover" Data={Wrappers} @@ -270,7 +273,16 @@ const ChannelScalingForm = (props: IProps) => { > If Adjusted -
+ {props.Page != null && props.SetPage != null && props.TotalPages != null ? +
+
+ props.SetPage(p - 1)} /> +
+
: null} +
return ( diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingWindow.tsx index 63fc4a8a38..e1f85be643 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingWindow.tsx @@ -27,9 +27,6 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import ChannelScalingForm from './ChannelScalingForm'; -import { TrendChannelSlice } from '../../Store/Store'; -import { useAppDispatch } from '../../hooks'; -import { SetChanged } from '../../Store/EventChannelSlice'; declare let homePath: string; @@ -41,18 +38,19 @@ interface IProps { const ChannelScalingWindow = (props: IProps) => { const [status, setStatus] = React.useState('uninitiated'); const [channels, setChannels] = React.useState([]); - const dispatch = useAppDispatch(); + const [page, setPage] = React.useState(0); + const [pageInfo, setPageInfo] = React.useState<{ TotalPages: number, TotalRecords: number, RecordsPerPage: number }>({ TotalPages: 0, TotalRecords: 0, RecordsPerPage: 0 }) React.useEffect(() => { if (props.IsVisible) - return loadChannels(); - }, [props.IsVisible]); + return loadChannels(page); + }, [props.IsVisible, page]); - function loadChannels() { + function loadChannels(page: number) { setStatus('loading'); const handle = $.ajax({ type: "GET", - url: `${homePath}api/OpenXDA/Meter/${props.Meter.ID}/Channels`, + url: `${homePath}api/OpenXDA/Meter/${props.Meter.ID}/Channels/${page}`, contentType: "application/json; charset=utf-8", dataType: "json", cache: false, @@ -60,8 +58,11 @@ const ChannelScalingWindow = (props: IProps) => { }); handle.done((d) => { - setChannels(d); + setChannels(JSON.parse(d.Data)); setStatus('idle'); + setPageInfo({ TotalPages: d.NumberOfPages, TotalRecords: d.TotalRecords, RecordsPerPage: d.RecordsPerPage }) + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); }); handle.fail(() => { setStatus('error'); }); @@ -84,12 +85,9 @@ const ChannelScalingWindow = (props: IProps) => { h.done(() => { setStatus('idle'); setChannels(channels); - dispatch(TrendChannelSlice.SetChanged()); - // This one is for event channels - dispatch(SetChanged()); }); h.fail(() => setStatus('error')); - + } return ( @@ -100,8 +98,17 @@ const ChannelScalingWindow = (props: IProps) => {

Channel Scaling:

+
+
+

+ {status === 'error' ? 'Could not complete Search' : + status === 'loading' ? 'Loading...' : + `Displaying Channel(s) ${pageInfo.TotalRecords > 0 ? (pageInfo.RecordsPerPage * page + 1) : 0} - ${pageInfo.RecordsPerPage * page + channels.length} out of ${pageInfo.TotalRecords}`} +

+
+
- +
); } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx index 211787dd67..20408154fb 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -23,19 +23,17 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { Application, OpenXDA as GemstoneOpenXDA} from '@gpa-gemstone/application-typings'; +import { Application, OpenXDA as GemstoneOpenXDA } from '@gpa-gemstone/application-typings'; import { PhaseSlice, MeasurmentTypeSlice } from '../Store/Store' import { useAppSelector, useAppDispatch } from '../hooks'; -import { LoadingIcon, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingIcon, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { Input, Select, ToolTip } from '@gpa-gemstone/react-forms'; import { AssetAttributes } from '../AssetAttribute/Asset'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { OpenXDA } from '../global'; -import { SelectAscending, SelectSortKey, SelectEventChannels, SelectEventChannelStatus, SelectMeterID, dBAction } from '../Store/EventChannelSlice'; -import { FetchChannels } from '../Store/EventChannelSlice'; import { IsNumber } from '@gpa-gemstone/helper-functions'; import { cloneDeep } from 'lodash'; -import { ConfigurableTable, ConfigurableColumn, Column } from '@gpa-gemstone/react-table'; +import { ConfigurableTable, ConfigurableColumn, Column, Paging } from '@gpa-gemstone/react-table'; import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; @@ -43,16 +41,21 @@ declare var homePath: string; interface IProps { Meter: GemstoneOpenXDA.Types.Meter, IsVisible: boolean } type RecordChange = Map>; +const EventChannelController = new GenericController(`${homePath}api/OpenXDA/EventChannel`, "Name"); + const MeterEventChannelWindow = (props: IProps) => { const dispatch = useAppDispatch(); - const data = useAppSelector(SelectEventChannels); - const sortKey = useAppSelector(SelectSortKey) - const ascending = useAppSelector(SelectAscending) - const status = useAppSelector(SelectEventChannelStatus); - const meterID = useAppSelector(SelectMeterID); - + const [data, setData] = React.useState([]); + const [sortKey, setSortKey] = React.useState('Name'); + const [ascending, setAscending] = React.useState(false); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [recordChanges, setRecordChanges] = React.useState(new Map>()); + const [status, setStatus] = React.useState('uninitiated'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const phases = useAppSelector(PhaseSlice.Data) as GemstoneOpenXDA.Types.Phase[]; const measurementTypes = useAppSelector(MeasurmentTypeSlice.Data) as GemstoneOpenXDA.Types.MeasurementType[]; @@ -62,14 +65,12 @@ const MeterEventChannelWindow = (props: IProps) => { const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; const [assetStatus, setAssetStatus] = React.useState('idle') - const [removeRecord, setRemoveRecord] = React.useState(null); + const [removeRecord, setRemoveRecord] = React.useState(null); const [errors, setErrors] = React.useState([]); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None' | 'Add')>('None'); const roles = useAppSelector(SelectRoles); - - React.useEffect(() => { if (pStatus == 'uninitiated' || pStatus == 'changed') dispatch(PhaseSlice.Fetch()); @@ -81,9 +82,19 @@ const MeterEventChannelWindow = (props: IProps) => { }, [mtStatus]) React.useEffect(() => { - if (status == 'uninitiated' || meterID !== props.Meter.ID || status == 'changed') - dispatch(FetchChannels({ meterId: props.Meter.ID })); - }, [props.Meter,status]) + setStatus('loading'); + const handle = EventChannelController.PagedSearch([], sortKey, ascending, page, props.Meter.ID); + handle.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setStatus('idle'); + }).fail(() => setStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [props.Meter, sortKey, ascending, page, refreshTrigger]) React.useEffect(() => { if (!props.IsVisible) @@ -111,7 +122,7 @@ const MeterEventChannelWindow = (props: IProps) => { if (k == 'ConnectionPriority' && val != null && !AssetAttributes.isRealNumber(val)) e.push('All Connection Priorities must be numeric values.') - if(k == 'Adder' && val == null) + if (k == 'Adder' && val == null) e.push('All Channels must have an Adder.') if (k == 'Multiplier' && val == null) e.push('All Channels must have a Multiplier.') @@ -151,7 +162,7 @@ const MeterEventChannelWindow = (props: IProps) => { for (let k of recordChanges.get(id).keys()) { original[k] = (recordChanges.get(id).get(k as keyof OpenXDA.EventChannel)) as any } - dispatch(dBAction({ record: original, verb: 'PATCH' })); + EventChannelController.DBAction("PATCH", original).then(() => setRefreshTrigger(val => !val)); } setRecordChanges(new Map>()); @@ -172,7 +183,7 @@ const MeterEventChannelWindow = (props: IProps) => { let update = cloneDeep(original); if (!update.has(record.ID)) update.set(record.ID, new Map()); - update.get(record.ID).set(field, record[field] as string|number); + update.get(record.ID).set(field, record[field] as string | number); return update; }) @@ -206,6 +217,13 @@ const MeterEventChannelWindow = (props: IProps) => {

Event Channels:

+
+
+

+ {'Could not complete Search'} +

+
+
@@ -224,6 +242,13 @@ const MeterEventChannelWindow = (props: IProps) => {

Event Channels:

+
+
+

+ {'Loading...'} +

+
+
@@ -242,9 +267,16 @@ const MeterEventChannelWindow = (props: IProps) => {

Event Channels:

+
+
+

+ {`Displaying Event Channel(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} +

+
+
-
-
+
+
LocalStorageKey="MeterEventChannelConfigTable" TableClass="table table-hover" @@ -258,10 +290,8 @@ const MeterEventChannelWindow = (props: IProps) => { SortKey={sortKey} Ascending={ascending} OnSort={(d) => { - if (d.colKey === sortKey) - dispatch(FetchChannels({ sortField: d.colField, ascending: !ascending, meterId: props.Meter.ID })); - else - dispatch(FetchChannels({ sortField: d.colField, ascending: true, meterId: props.Meter.ID })); + if (d.colKey === sortKey) setAscending(a => !a); + else setSortKey(d.colField); }} > @@ -272,7 +302,7 @@ const MeterEventChannelWindow = (props: IProps) => { Record={item} Field={'SourceIndices'} Label={''} Setter={(r) => createChange(r, 'SourceIndices')} - Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()}/>}> + Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()} />}> Identifier @@ -293,7 +323,7 @@ const MeterEventChannelWindow = (props: IProps) => { Content={({ item }) => ({ Label: d.Name, Value: d.ID.toString() }))} - Setter={(r) => createChange(r, 'PhaseID')} Disabled={!hasPermissions()}/>}> + Setter={(r) => createChange(r, 'PhaseID')} Disabled={!hasPermissions()} />}> @@ -317,7 +347,7 @@ const MeterEventChannelWindow = (props: IProps) => { Content={({ item }) => Record={item} Field={'Adder'} Type={'number'} Label={''} Setter={(r) => createChange(r, 'Adder')} - Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()}/>}> + Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()} />}> @@ -329,7 +359,7 @@ const MeterEventChannelWindow = (props: IProps) => { Content={({ item }) => Record={item} Field={'Multiplier'} Type={'number'} Label={''} Setter={(r) => createChange(r, 'Multiplier')} - Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()}/>}> + Valid={(f) => isValid(f, item)} Disabled={!hasPermissions()} />}> @@ -360,7 +390,7 @@ const MeterEventChannelWindow = (props: IProps) => { Content={({ item }) =>