diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 97c92e7147..aa27264813 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -33,15 +33,14 @@ using System.Collections.Generic; using System.Data; using System.Linq; -using System.Net.Http; -using System.Transactions; +using System.Reflection; using System.Web.Http; using GSF.Data; using GSF.Data.Model; using GSF.Web.Model; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using openXDA.Model; -using SystemCenter.Model; namespace SystemCenter.Controllers.OpenXDA { @@ -105,6 +104,78 @@ GROUP BY return Unauthorized(); } + [HttpPost, Route("{assetGroupID:int}/Assets/{page:int}")] + public IHttpActionResult GetAssetsPaged([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) + { + + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + results.RecordsPerPage = recordsPerPage; + + string orderBy = "ID"; + + if (typeof(Asset).GetProperties().Select(prop => prop.Name).Contains(postData.OrderBy) || postData.OrderBy == "AssetType") + { + orderBy = postData.OrderBy; + } + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $@"SELECT + DISTINCT + Asset.ID, + AssetAssetGroup.AssetGroupID, + Asset.AssetKey, + Asset.AssetName, + Asset.VoltageKV, + AssetType.Name as AssetType, + COUNT(DISTINCT Meter.ID) as Meters, + COUNT(DISTINCT Location.ID) as Locations + FROM + Asset Join + AssetType ON Asset.AssetTypeID = AssetType.ID LEFT JOIN + MeterAsset ON MeterAsset.AssetID = Asset.ID LEFT JOIN + Meter ON MeterAsset.MeterID = Meter.ID LEFT JOIN + AssetLocation ON AssetLocation.AssetID = Asset.ID LEFT JOIN + Location ON AssetLocation.LocationID = Location.ID LEFT JOIN + AssetAssetGroup ON Asset.ID = AssetAssetGroup.AssetID + GROUP BY + Asset.ID, + Asset.AssetKey, + Asset.AssetName, + Asset.VoltageKV, + AssetType.Name, + AssetAssetGroup.AssetGroupID + HAVING AssetAssetGroup.AssetGroupID = {{0}} + ORDER BY {orderBy} {(postData.Ascending ? "ASC" : "DESC")} + "; + + DataTable records = connection.RetrieveData(sql, assetGroupID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddAssets")] public IHttpActionResult AddAssets(int assetGroupID, [FromBody] IEnumerable assets) { @@ -205,6 +276,73 @@ GROUP BY return Unauthorized(); } + [HttpPost, Route("{assetGroupID:int}/Meters/{page:int}")] + public IHttpActionResult GetMetersPaged([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + results.RecordsPerPage = recordsPerPage; + + string orderBy = "ID"; + + if (typeof(Meter).GetProperties().Select(prop => prop.Name).Contains(postData.OrderBy)) + orderBy = postData.OrderBy; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $@"SELECT DISTINCT + Meter.ID, + MeterAssetGroup.AssetGroupID, + Meter.AssetKey, + Meter.Name, + Meter.Make, + 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 {orderBy} {(postData.Ascending ? "ASC" : "DESC")} + "; + + DataTable records = connection.RetrieveData(sql, assetGroupID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddMeters")] public IHttpActionResult AddMeters(int assetGroupID, [FromBody] IEnumerable meters) { @@ -293,7 +431,6 @@ public IHttpActionResult GetSubGroups(int assetGroupID) try { IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID); - return Ok(records); } catch (Exception ex) @@ -306,6 +443,33 @@ public IHttpActionResult GetSubGroups(int assetGroupID) return Unauthorized(); } + [HttpPost, Route("{assetGroupID:int}/AssetGroups/{page:int}")] + public IHttpActionResult GetSubGroupsPaged([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID); + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record) ?? record.ID); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record) ?? record.ID); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddAssetGroups")] public IHttpActionResult AddSubgroups(int assetGroupID, [FromBody] IEnumerable subGroups) { diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index fabcc4c32f..35606ba45b 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -25,6 +25,8 @@ using System.Collections.Generic; using System.Data; using System.Diagnostics; +using Newtonsoft.Json; +using System.Reflection; using System.Linq; using System.Transactions; using System.Web.Http; @@ -65,6 +67,34 @@ public IHttpActionResult GetAssetLocations(int assetID) return Unauthorized(); } + [HttpPost, Route("{assetID:int}/Locations/{page:int}")] + public IHttpActionResult GetAssetLocationsPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = {0})", assetID); + + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + return Ok(results); + } + [HttpGet, Route("{assetID:int}/AssetLocations")] public IHttpActionResult GetAssetLocationModels(int assetID) { @@ -122,6 +152,33 @@ public IHttpActionResult GetAssetMeters(int assetID) } return Unauthorized(); } + [HttpPost, Route("{assetID:int}/Meters/{page:int}")] + public IHttpActionResult GetAssetMetersPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID IN (SELECT MeterID FROM MeterAsset WHERE AssetID = {0})", assetID); + + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + return Ok(results); + } [HttpGet, Route("{assetID:int}/AssetConnections")] public IHttpActionResult GetAssetAssetConnections(int assetID) @@ -163,6 +220,64 @@ ELSE AssetRelationship.ParentID return Unauthorized(); } + [HttpPost, Route("{assetID:int}/AssetConnections/{page:int}")] + public IHttpActionResult GetAssetAssetConnectionsPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + + string orderBy = "ID"; + + if (typeof(Asset).GetProperties().Select(prop => prop.Name).Contains(postData.OrderBy)) + orderBy = postData.OrderBy; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + DataTable records = connection.RetrieveData(@$" + SELECT + AssetRelationship.AssetRelationshipTypeID, + AssetRelationshipType.Name, + Asset.ID as AssetID, + Asset.AssetKey, + Asset.AssetName + FROM + AssetRelationship JOIN + AssetRelationshipType ON AssetRelationship.AssetRelationshipTypeID = AssetRelationshipType.ID JOIN + Asset ON Asset.ID = ( + CASE + WHEN ParentID = {{0}} THEN AssetRelationship.ChildID + ELSE AssetRelationship.ParentID + END + ) + WHERE + ParentID = {{0}} OR ChildID = {{0}} + ORDER BY {orderBy} {(postData.Ascending ? "ASC" : "DESC")} + ", assetID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpGet, Route("{assetID:int}/OtherLocations")] public IHttpActionResult GetOtherLocations(int assetID) { @@ -575,13 +690,8 @@ public IHttpActionResult PostExistingMeterForAsset(int assetID, int meterID) } } - [HttpGet, Route("{assetID:int}/ConnectedChannels")] - public IHttpActionResult GetAssetChannels(int assetID) + public IEnumerable GetAssetChannels(int assetID) { - if (GetRoles == string.Empty || User.IsInRole(GetRoles)) - { - try - { using (AdoDataConnection connection = new AdoDataConnection(Connection)) { Asset asset = new TableOperations(connection).QueryRecordWhere("ID={0}", assetID); @@ -601,20 +711,51 @@ public IHttpActionResult GetAssetChannels(int assetID) .QueryRecordsWhere($"ID in ({string.Join(", ", connectedChannels.Select(channels => channels.ID))})") .DistinctBy(c => c.ID); - return Ok(uniqueChannels); + return uniqueChannels; } else { - return Ok(new List()); + return new List(); } + } - } catch (Exception ex) + } + + [HttpPost, Route("{assetID:int}/ConnectedChannels/{page:int}")] + public IHttpActionResult GetConnectedChannelsPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) { - return InternalServerError(ex); - } - } - else + if (!GetAuthCheck()) return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + IEnumerable uniqueChannels = GetAssetChannels(assetID); + + int totalRecords = uniqueChannels.Count(); + + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + + if (postData.OrderBy == "Phase" || postData.OrderBy == "MeasurementType") + bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + if (postData.Ascending) + uniqueChannels = uniqueChannels.OrderBy(item => item.GetType().GetProperty(postData.OrderBy, bindingFlags).GetValue(item)); + else + uniqueChannels = uniqueChannels.OrderByDescending(item => item.GetType().GetProperty(postData.OrderBy, bindingFlags).GetValue(item)); + + uniqueChannels = uniqueChannels + .Skip(recordsPerPage * page) + .Take(recordsPerPage); + + PagedResults pagedResults = new PagedResults() + { + Data = JsonConvert.SerializeObject(uniqueChannels), + RecordsPerPage = recordsPerPage, + TotalRecords = totalRecords, + NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage + }; + + return Ok(pagedResults); } diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs index 2a15dbcd27..652ee5ea63 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs @@ -307,8 +307,8 @@ ORDER BY {orderByExpression} } } - [HttpGet, Route("{locationID:int}/Images")] - public IHttpActionResult GetImagesForLocation(int locationID) + [HttpGet, Route("{locationID:int}/Images/{page:int}")] + public IHttpActionResult GetImagesForLocation(int locationID, int page) { try { @@ -321,9 +321,20 @@ public IHttpActionResult GetImagesForLocation(int locationID) if (path == null) return BadRequest("ImageDirectory.Path not set in settings table."); if (Directory.Exists(Path.Combine(path, key))) - return Ok(Directory.GetFiles(Path.Combine(path, key)).Select(fp => new FileInfo(fp).Name)); + { + IEnumerable imagePaths = Directory.GetFiles(Path.Combine(path, key)).Select(fp => new FileInfo(fp).Name); + return Ok(PageImagePaths(imagePaths, page)); + } + + else - return Ok(new string[] { }); + return Ok(new PagedResults() + { + Data = JsonConvert.SerializeObject(new string[0]), + TotalRecords = 0, + NumberOfPages = 0, + RecordsPerPage = Take ?? 48 + }); } else return Unauthorized(); @@ -332,7 +343,27 @@ public IHttpActionResult GetImagesForLocation(int locationID) { return InternalServerError(ex); } + } + + +public static PagedResults PageImagePaths(IEnumerable imagePaths, int page) + { + int recordsPerPage = 48; // the image page likes columns of 6, despite the max value. + int totalImages = imagePaths.Count(); + + IEnumerable pagedImagePaths = imagePaths + .OrderBy(fp => fp) + .Skip((page) * recordsPerPage) + .Take(recordsPerPage); + + return new PagedResults() + { + Data = JsonConvert.SerializeObject(pagedImagePaths), + TotalRecords = totalImages, + NumberOfPages = (totalImages + recordsPerPage - 1) / recordsPerPage, + RecordsPerPage = recordsPerPage + }; } [HttpGet, Route("{locationID:int}/Images/{file}")] diff --git a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs index 2906643a64..5346391136 100644 --- a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs +++ b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs @@ -100,6 +100,55 @@ 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(); + + string orderBySwitch(string orderBy) + { + String[] valid = ["Phone", "Email", "FirstName", "LastName"]; + if (valid.Contains(orderBy)) + return orderBy; + else + return "Name"; + } + + PagedResults pagedResults = new PagedResults(); + + int recordsPerPage = Take ?? 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 {orderBySwitch(postData.OrderBy)} {(postData.Ascending ? "ASC" : "DESC")}"; + 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); + + DataRow[] rows = results.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = results.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results = pagedTable; + + // paged results setting + pagedResults.Data = JsonConvert.SerializeObject(results); + pagedResults.TotalRecords = count; + pagedResults.NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage; + } + + return Ok(pagedResults); + } + [HttpPost] [Route("{groupID}/PostRoles")] public IHttpActionResult PostGroupRoles([FromBody] IEnumerable record, string groupID) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx index d15a410f8a..3556f86841 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx @@ -47,10 +47,8 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { const dispatch = useAppDispatch(); const data = useAppSelector(AdditionalFieldsSlice.SearchResults); - const status = useAppSelector(AdditionalFieldsSlice.SearchStatus); + const status = useAppSelector(AdditionalFieldsSlice.PagedStatus); const search = useAppSelector(AdditionalFieldsSlice.SearchFilters); - const sortField = useAppSelector(AdditionalFieldsSlice.SortField); - const ascending = useAppSelector(AdditionalFieldsSlice.Ascending); const parentID = useAppSelector(AdditionalFieldsSlice.ParentID); const currentPage = useAppSelector(AdditionalFieldsSlice.CurrentPage); const totalPages = useAppSelector(AdditionalFieldsSlice.TotalPages); @@ -59,6 +57,9 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { const valueListGroupData = useAppSelector(ValueListGroupSlice.Data); const valueListGroupStatus = useAppSelector(ValueListGroupSlice.Status); + + const [sortField, setSortField] = React.useState("FieldName") + const [ascending, setAscending] = React.useState(false) const [errors, setErrors] = React.useState([]); const [warnings, setWarnings] = React.useState([]); const [mode, setMode] = React.useState<'View' | 'Add' | 'Edit'>('View'); @@ -122,6 +123,21 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField, ascending, page: page - 1})) }, [search, sortField, ascending]) + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) + }, [search, ascending, sortField]) + useBoundPaging(currentPage, totalPages, setPage) return ( @@ -162,10 +178,7 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { Data={data} SortKey={sortField as string} Ascending={ascending} - OnSort={(d) => { - if (d.colKey === null) return; - dispatch(AdditionalFieldsSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + OnSort={sort} OnClick={(item) => { setRecord(item.row); setMode('Edit'); }} TableStyle={{ padding: 0, width: 'calc(100%)', height: 'calc(100% - 16px)', 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..9909db44b3 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } 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'; @@ -73,6 +73,8 @@ const AssetChannelWindow = (props: IProps) => { const [status, setStatus] = React.useState('idle'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); React.useEffect(() => { let channelHandle = getChannels(); @@ -83,32 +85,29 @@ const AssetChannelWindow = (props: IProps) => { if (channelHandle != null && channelHandle.abort != null) channelHandle.abort(); } - }, [props.ID]); + }, [props.ID, ascending, page, sortField]); function getChannels(): JQuery.jqXHR { setStatus('loading'); return $.ajax( { - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels`, - contentType: "application/json; charset=utf-A", + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels/${page}`, + contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true + async: true, + data: JSON.stringify({OrderBy: sortField, Ascending: ascending, Searches: []}) } ).done( - (d: Array) => { - const sortedChannels = sortData(sortField, ascending, d); - setAssetChannels(sortedChannels) + (d) => { + setAssetChannels(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages) setStatus('idle'); } ).fail(() => setStatus('error')); } - function sortData(key: keyof ChannelDetail, ascending: boolean, data: ChannelDetail[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - if (status == 'error' || pStatus == 'error' || mtStatus == 'error') return
@@ -155,22 +154,21 @@ const AssetChannelWindow = (props: IProps) => {
+
+
TableClass="table table-hover" Data={assetChannels} SortKey={sortField} Ascending={ascending} OnSort={(d) => { + setPage(0) 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' }} @@ -244,6 +242,17 @@ const AssetChannelWindow = (props: IProps) => {
+
+
+
+ { setPage(p - 1) }} + Total={totalPages} + /> +
+
+
); } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx index 428cd3ef10..604914bbc3 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import _ from 'lodash'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { LoadingIcon, Modal, Search, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; @@ -62,12 +62,14 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number const [trigger, setTrigger] = React.useState(0); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { let handle = getAssetConnections(); - return () => { if (handle != null || handle.abort != null) handle.abort();} - }, [props.ID, trigger]) + return () => { if (handle != null && handle.abort != null) handle.abort();} + }, [props.ID, trigger, page, sortKey, ascending]) React.useEffect(() => { if (props.ID > 0) { @@ -111,23 +113,20 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number function getAssetConnections(): JQuery.jqXHR { setStatus('loading'); return $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) }).done((d) => { setStatus('idle') - const sortedConnections = sortData(sortKey, ascending, d); - setAssetConnections(sortedConnections) + setAssetConnections(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }).fail(() => setStatus('error')); } - function sortData(key: string, ascending: boolean, data: AssetConnection[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getAssets(): JQuery.jqXHR { const filter = [ { FieldName: 'ID', SearchText: `(SELECT AssetID FROM AssetLocation WHERE LocationID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = ${props.ID}))`, Operator: 'IN', Type: 'query', IsPivotColumn: false }, @@ -239,7 +238,8 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number -
+
+
TableClass="table table-hover" Data={assetConnections} @@ -251,14 +251,10 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number if (d.colKey === sortKey) { setAscending(!ascending); - const ordered = _.orderBy(assetConnections, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetConnections(ordered); } else { setAscending(true); setSortKey(d.colKey); - const ordered = _.orderBy(assetConnections, [d.colKey], ["asc"]); - setAssetConnections(ordered); } }} TableStyle={{ height: '100%' }} @@ -306,7 +302,17 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number } >

- + +
+
+
+ setPage(page - 1)} + /> +
+
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx index c63fd3186c..f76f5e4b55 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { useAppSelector } from '../hooks'; @@ -34,15 +34,17 @@ import { ToolTip } from '@gpa-gemstone/react-forms'; declare var homePath: string; -function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ +function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element { let navigate = useNavigate(); const [locations, setLocations] = React.useState>([]); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); const [allLocations, setAllLocations] = React.useState>([]); const [newLocation, setNewLocation] = React.useState(); - const [hover, setHover] = React.useState(undefined); + const [hover, setHover] = React.useState(undefined); const [showModal, setShowModal] = React.useState(false); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); function hasPermissions(): boolean { @@ -51,6 +53,10 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element return true; } + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + React.useEffect(() => { getData(); }, [props.Asset]); @@ -62,22 +68,19 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element function getLocations(): void { $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true - }).done(data => { - const sortedLocations = sortData(sortField, ascending, data); - setLocations(sortedLocations); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) + }).done(d => { + setLocations(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }); } - function sortData(key: keyof OpenXDA.Types.Location, ascending: boolean, data: OpenXDA.Types.Location[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getAllOtherLocations(): void { $.ajax({ type: "GET", @@ -139,96 +142,103 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element
-
- - TableClass="table table-hover" - Data={locations} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(locations, [d.colKey], [(!ascending ? "asc" : "desc")]); - setLocations(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(locations, [d.colKey], ["asc"]); - setLocations(ordered); - } - }} - TableStyle={{ height: '100%' }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - OnClick={handleSelect} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: '30%' }} - RowStyle={{ width: '30%' }} - > Name - - - Key={'LocationKey'} - AllowSort={true} - Field={'LocationKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Key - - - Key={'Latitude'} - AllowSort={true} - Field={'Latitude'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Latitude - - - Key={'Longitude'} - AllowSort={true} - Field={'Longitude'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Longitude - - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => <> - - } - >

- - +
+
+ + TableClass="table table-hover" + Data={locations} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + TableStyle={{ height: '100%' }} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + OnClick={handleSelect} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: '30%' }} + RowStyle={{ width: '30%' }} + > Name + + + Key={'LocationKey'} + AllowSort={true} + Field={'LocationKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Key + + + Key={'Latitude'} + AllowSort={true} + Field={'Latitude'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + > Latitude + + + Key={'Longitude'} + AllowSort={true} + Field={'Longitude'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + > Longitude + + + Key={'Delete'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => <> + + } + >

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

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

@@ -241,19 +251,19 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element setShowModal(false) }} ConfirmText={'Save'} DisableConfirm={allLocations.length === 0}> - -
- - -
+ +
+ + +
- + ); } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx index 9d1d7ca019..07ca5ef952 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { ByMeterSlice } from '../Store/Store'; @@ -46,6 +46,8 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ const mStatus = useAppSelector(ByMeterSlice.Status); const mParentID = useAppSelector(ByMeterSlice.ParentID); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -54,28 +56,29 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ }, [mStatus, mParentID]); + React.useEffect(() => { + return getMeters(); + }, [ascending, sortField, page]) + React.useEffect(() => { getMeters(); }, [props.Asset]); function getMeters(): void { $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true - }).done(meters => { - const sortedMeters = sortData(sortField, ascending, meters); - setMeters(sortedMeters); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) + }).done(d => { + setMeters(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }); } - function sortData(key: keyof OpenXDA.Types.Meter, ascending: boolean, data: OpenXDA.Types.Meter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function addMeter(meterID: number) { return $.ajax({ type: "POST", @@ -112,7 +115,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } @@ -141,7 +144,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } @@ -176,7 +179,8 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ -
+
+
TableClass="table table-hover" Data={meters} @@ -185,14 +189,10 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ OnSort={(d) => { if (d.colKey == sortField) { setAscending(!ascending); - const ordered = _.orderBy(meters, [d.colKey], [(!ascending ? "asc" : "desc")]); - setMeters(ordered); } else { setAscending(true); setSortField(d.colField); - const ordered = _.orderBy(meters, [d.colKey], ["asc"]); - setMeters(ordered); } }} TableStyle={{ height: '100%' }} @@ -234,7 +234,17 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ RowStyle={{ width: '20%' }} > Model - + +
+
+
+ setPage(page - 1)} + /> +
+
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx index 0031736e61..539b41b48b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx @@ -83,7 +83,7 @@ export default function AssetSelect(props: IProps) { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } @@ -112,7 +112,7 @@ export default function AssetSelect(props: IProps) { setFields(ordered); }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx index 77dac7789d..b8db1025d2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx @@ -175,7 +175,7 @@ const ByAsset: Application.Types.iByComponent = (props) => { setAddlFieldCols(ordered); }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; }, []); @@ -257,7 +257,7 @@ const ByAsset: Application.Types.iByComponent = (props) => { }); handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) - return () => { if (handle != null && handle.abort == null) handle.abort(); } + return () => { if (handle != null && handle.abort != null) handle.abort(); } } // ToDo: This search bar is not the default select. Default select should probably be paged in the future if it doesn't break anything else 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..005bbc42f7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx @@ -25,7 +25,7 @@ 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 { AssetGroupSlice, AssetTypeSlice } from '../Store/Store'; import { SystemCenter } from '@gpa-gemstone/application-typings'; import { Warning } from '@gpa-gemstone/react-interactive'; @@ -37,7 +37,7 @@ 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'); @@ -45,6 +45,8 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { 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 assetType = useAppSelector(AssetTypeSlice.Data); const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); @@ -63,34 +65,36 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { dispatch(AssetTypeSlice.Fetch()); }, [assetTypeStatus]); + + React.useEffect(() => { + return getData(); + }, [ascending, sortKey, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; 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((r) => { + setAssetList(JSON.parse(r.Data)); + setTotalPages(r.NumberOfPages); }); - + return function cleanup() { if (handle.abort != null) handle.abort(); } } - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedAsset[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function saveItems(items: SystemCenter.Types.DetailedAsset[]) { let handle = $.ajax({ @@ -133,99 +137,104 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Transmission Assets:

+
+
+
+
+

Transmission Assets:

+
+
-
-
-
-
- - 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(page -1)} + /> +
+
-
-
-
+
+
-
+

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

-
+
{ @@ -234,7 +243,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..d61283ff11 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { 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'; @@ -38,7 +38,7 @@ 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>([]); @@ -47,6 +47,8 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { 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 [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); @@ -65,34 +67,35 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return getData(); }, [props.AssetGroupID, counter]); + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; 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((r) => { + setGroupList(JSON.parse(r.Data)); + setTotalPages(r.NumberOfPages); }); - + 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")]); - } - function getEnum(setOptions, field) { let handle = null; if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) @@ -109,7 +112,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } @@ -127,7 +130,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { } function saveItems(items: OpenXDA.Types.AssetGroup[]) { - + let handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddAssetGroups`, @@ -151,92 +154,95 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Asset Groups in Asset Group:

+
+
+
+
+

Asset Groups in Asset Group:

+
-
-
-
- - 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(a => !a); + } + else { + 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(page - 1)} + Total={totalPages} + /> +
+
- -
-
+
@@ -270,7 +276,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/ByAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx index 8b453ad23d..7fbbaccad6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx @@ -22,15 +22,15 @@ //****************************************************************************************************** import * as React from 'react'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import * as _ from 'lodash'; import { useNavigate } from "react-router-dom"; import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings' -import { Search, Modal } from '@gpa-gemstone/react-interactive'; +import { GenericController, Modal, Search, SearchBar } from '@gpa-gemstone/react-interactive'; import { CheckBox, Input } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { AssetGroupSlice, ByAssetSlice, ByMeterSlice, AssetTypeSlice } from '../Store/Store'; -import { DefaultSearch, DefaultSelects } from '@gpa-gemstone/common-pages'; +import { AssetGroupSlice, ByMeterSlice, AssetTypeSlice } from '../Store/Store'; +import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { useAppDispatch, useAppSelector } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; @@ -47,32 +47,41 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); const dispatch = useAppDispatch(); - const data = useAppSelector(AssetGroupSlice.SearchResults); - const sortKey = useAppSelector(AssetGroupSlice.SortField); - const ascending = useAppSelector(AssetGroupSlice.Ascending); - const searchStatus = useAppSelector(AssetGroupSlice.SearchStatus); - const searchFields = useAppSelector(AssetGroupSlice.SearchFilters) - const status = useAppSelector(AssetGroupSlice.Status); + const [data, setData] = React.useState([]); + const [searchFilters, setSearchFilters] = React.useState[]>([]); + const [status, setStatus] = React.useState('uninitiated'); const allAssetGroups = useAppSelector(AssetGroupSlice.Data); - const assetType = useAppSelector(AssetTypeSlice.Data); const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - const [showFilter, setFilter] = React.useState<('None' | 'Meter' | 'Asset' | 'Asset Group' | 'Station')>('None'); - const [newAssetGroup, setNewAssetGroup] = React.useState(_.cloneDeep(emptyAssetGroup)); const [showNewGroup, setShowNewGroup] = React.useState(false); const [assetGrpErrors, setAssetGrpErrors] = React.useState([]); + const [sortKey, setSortKey] = 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); - React.useEffect(() => { - if (status == 'changed' || status == 'uninitiated') - dispatch(AssetGroupSlice.Fetch()); - }, [status]); + const assetGroupController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/AssetGroup`, "Name", true),[]) React.useEffect(() => { - if (searchStatus == 'changed' || searchStatus == 'uninitiated') - dispatch(AssetGroupSlice.DBSearch({ filter: searchFields })); - }, [searchStatus]); + setStatus('loading') + const h = assetGroupController.PagedSearch(searchFilters, sortKey, ascending, page); + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + setStatus('idle'); + }) + h.fail(() => setStatus('error')) + + return () => { + if (h != null && h.abort != null) h.abort(); + } + }, [sortKey, ascending, page, searchFilters]) React.useEffect(() => { if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') @@ -113,36 +122,7 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { }); return () => { - if (handle != null && handle.abort == null) handle.abort(); - }; - } - - function getAdditionalAssetFields(setFields) { - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/AdditionalFieldView/ParentTable/Asset/FieldName/0`, - contentType: "application/json; charset=utf-8", - cache: false, - async: true - }); - - function ConvertType(type: string) { - if (type == 'string' || type == 'integer' || type == 'number' || type == 'datetime' || type == 'boolean') - return { type: type } - return { - type: 'enum', enum: [{ Label: type, Value: type }] - } - } - - handle.done((d: Array) => { - - let ordered = _.orderBy(d.filter(item => item.Searchable).map(item => ( - { label: `[AF${item.ExternalDB != undefined ? " " + item.ExternalDB : ''}] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type), isPivotField: true } as Search.IField - )), ['label'], ["asc"]); - setFields(ordered); - }); - return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } @@ -198,7 +178,6 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); - } function handleSelect(item) { @@ -233,26 +212,43 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } return ( <>
- { return () => {}}} > -
+ + - +
TableClass="table table-hover" @@ -261,9 +257,10 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { Ascending={ascending} OnSort={(d) => { if (d.colKey === sortKey) - dispatch(AssetGroupSlice.Sort({ SortField: sortKey, Ascending: ascending })); + setAscending(a => !a) else { - dispatch(AssetGroupSlice.Sort({ SortField: d.colField as keyof OpenXDA.Types.AssetGroup, Ascending: true })); + setAscending(true); + setSortKey(d.colField); } }} OnClick={handleSelect} @@ -320,6 +317,15 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => {
+
+
+ {setPage(page - 1)} } + /> +
+
(0); const [removeMeter, setRemoveMeter] = React.useState(-1); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -57,34 +58,36 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return getData(); }, [props.AssetGroupID, counter]) + + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; 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); }); - + 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")]); - } - function getEnum(setOptions, field) { let handle = null; if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) @@ -101,7 +104,7 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } @@ -130,7 +133,7 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } @@ -174,89 +177,93 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Meters in Asset Group:

+
+
+
+
+

Meters in Asset Group:

+
-
-
-
- - 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(page - 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/ChannelGroup/ChannelGroupItem.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItem.tsx index 318cddded3..161c6cf2fc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItem.tsx @@ -24,10 +24,10 @@ import * as React from 'react'; import * as _ from 'lodash'; import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useAppDispatch, useBoundPaging } from '../hooks'; import { ChannelGroupDetailsSlice } from '../Store/Store'; import ChannelGroupItemForm from './ChannelGroupItemForm'; -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 { Modal, Warning } from '@gpa-gemstone/react-interactive'; @@ -35,11 +35,11 @@ interface IProps { Record: SystemCenter.Types.ChannelGroup } export default function ChannelGroupDetails(props: IProps) { const dispatch = useAppDispatch(); - const data = useAppSelector(ChannelGroupDetailsSlice.Data); - const sortKey = useAppSelector(ChannelGroupDetailsSlice.SortField); - const asc = useAppSelector(ChannelGroupDetailsSlice.Ascending); + const data = useAppSelector(ChannelGroupDetailsSlice.SearchResults); const status = useAppSelector(ChannelGroupDetailsSlice.Status); - const parentID= useAppSelector(ChannelGroupDetailsSlice.ParentID); + const parentID = useAppSelector(ChannelGroupDetailsSlice.ParentID); + const currentPage = useAppSelector(ChannelGroupDetailsSlice.CurrentPage); + const totalPages = useAppSelector(ChannelGroupDetailsSlice.TotalPages); const emptyRecord: SystemCenter.Types.ChannelGroupDetails = { @@ -57,6 +57,8 @@ export default function ChannelGroupDetails(props: IProps) { const [showWarning, setShowWarning] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(false); React.useEffect(() => { if (status == 'uninitiated' || status == 'changed' || parentID != props.Record.ID) @@ -69,6 +71,29 @@ export default function ChannelGroupDetails(props: IProps) { setRecord(emptyRecord); } + const setPage = React.useCallback((page) => { + dispatch(ChannelGroupDetailsSlice.PagedSearch({ filter: [], sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) + }, [sortField, ascending]) + + useBoundPaging(currentPage, totalPages, setPage) + + const sort = React.useCallback((d) => { + if (d.colField === 'btns') + return + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(ChannelGroupDetailsSlice.PagedSearch({ filter: [], sortField: sort, ascending: asc, page: 0 })) + }, [ascending, sortField]) + return (
@@ -84,13 +109,9 @@ export default function ChannelGroupDetails(props: IProps) { TableClass="table table-hover" Data={data} - SortKey={sortKey} - Ascending={asc} - OnSort={(d) => { - if (d.colKey == 'btns') - return; - dispatch(ChannelGroupDetailsSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + SortKey={sortField} + Ascending={ascending} + OnSort={sort} 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, width: '100%' }} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsTable.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsTable.tsx index b926db5558..56992bdde2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsTable.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsTable.tsx @@ -97,108 +97,106 @@ function AdditionalFieldsTable(props: IProps): JSX.Element { return ( <> -
-
- - TableClass="table table-hover" - Data={additionalFields} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colKey); - } - }} - Selected={() => false} - KeySelector={(item) => item.ID} - > - - Key={'FieldName'} - AllowSort={true} - Field={'FieldName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'Type'} - AllowSort={true} - Field={'Type'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => `${item.Type}${item.IsKey ? " (external key)" : ""}`} - > Type - - - Key={'ExternalDB'} - AllowSort={true} - Field={'ExternalDB'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Ext Database - - - Key={'ExternalTable'} - AllowSort={true} - Field={'ExternalTable'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Ext Table - - - Key={'Searchable'} - AllowSort={true} - Field={'Searchable'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Searchable ? : ''} - > Searchable - - - Key={'Value'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => <> - props.SetValues(v.find(x => x.AdditionalFieldID == item.ID), item)} /> - } - > Value - - - Key={'IsKey'} - AllowSort={false} - Field={'IsKey'} - HeaderStyle={{ width: '100px' }} - RowStyle={{ width: '100px', paddingLeft: '0px' }} - Content={({ item }) => - item.IsKey ? - <> - - - : null} - >

- - -
+
+ + TableClass="table table-hover" + Data={additionalFields} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey === sortKey) + setAscending(!ascending); + else { + setAscending(true); + setSortKey(d.colKey); + } + }} + Selected={() => false} + KeySelector={(item) => item.ID} + > + + Key={'FieldName'} + AllowSort={true} + Field={'FieldName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Name + + + Key={'Type'} + AllowSort={true} + Field={'Type'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => `${item.Type}${item.IsKey ? " (external key)" : ""}`} + > Type + + + Key={'ExternalDB'} + AllowSort={true} + Field={'ExternalDB'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Ext Database + + + Key={'ExternalTable'} + AllowSort={true} + Field={'ExternalTable'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Ext Table + + + Key={'Searchable'} + AllowSort={true} + Field={'Searchable'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Searchable ? : ''} + > Searchable + + + Key={'Value'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => <> + props.SetValues(v.find(x => x.AdditionalFieldID == item.ID), item)} /> + } + > Value + + + Key={'IsKey'} + AllowSort={false} + Field={'IsKey'} + HeaderStyle={{ width: '100px' }} + RowStyle={{ width: '100px', paddingLeft: '0px' }} + Content={({ item }) => + item.IsKey ? + <> + + + : null} + >

+ +
diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsWindow.tsx index 05948f3dd1..38145846a6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/AdditionalFieldsWindow.tsx @@ -102,7 +102,7 @@ function AdditionalFieldsWindow(props: IProps): JSX.Element {

Additional Fields:

-
+
{ - if (statusMap.get(item.ID) === 'loading') return - if (statusMap.get(item.ID) === 'error') return + if (statusMap.get(item.ID) === 'loading') return + if (statusMap.get(item.ID) === 'error') return return (
return <> -
-
-
-
-

Assigned Assets:

+
+
+
+
+

Assigned Assets:

+
-
-
-
- - TableClass="table table-hover" - Data={data} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Remove') - return; - dispatch(CustomerAssetSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} - 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={data} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(a => !a); + } + else { + setSortField(d.colField); + } + }} + 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(page - 1)} + Current={page + 1} + /> +
+
-
-
-
+
+
-
+ onMouseEnter={() => setHover('Update')} onMouseLeave={() => setHover('None')} onClick={() => { + if (hasPermissions()) + setShowAdd(true); + }}>Add Assets +

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

+
-
- { if (c) dispatch(CustomerAssetSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> + { if (c) assetController.DBAction('DELETE', removeRecord).done((d) => { setRefreshTrigger((val) => !val); setRemoveRecord(null); }) }} /> { setShowAdd(false) if (!conf) return saveCustomerAssets(selected.filter(items => data.findIndex(g => g.AssetID == items.ID) < 0)); + setRefreshTrigger((val) => !val); }} /> } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx index 68f95bdb0c..efd240e7d3 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx @@ -24,12 +24,12 @@ import * as React from 'react'; import * as _ from 'lodash'; import { PQView, OpenXDA as LocalXDA } from '../global'; -import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings' +import { OpenXDA, SystemCenter, Application } from '@gpa-gemstone/application-typings' import { useAppDispatch, useAppSelector } from '../hooks'; -import { ByMeterSlice, CustomerMeterSlice } from '../Store/Store' -import { Table, Column } from '@gpa-gemstone/react-table'; +import { ByMeterSlice } from '../Store/Store' +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { LoadingIcon, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingIcon, Search, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { SelectRoles } from '../Store/UserSettings'; @@ -38,38 +38,30 @@ declare var homePath: string; interface IProps { Customer: OpenXDA.Types.Customer } const CustomerMeterWindow = (props: IProps) => { const dispatch = useAppDispatch(); - const data = useAppSelector(CustomerMeterSlice.SearchResults); - const status = useAppSelector(CustomerMeterSlice.SearchStatus); + const [data, setData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated') const [showAdd, setShowAdd] = React.useState(false); - - const sortField = useAppSelector(CustomerMeterSlice.SortField); - const ascending = useAppSelector(CustomerMeterSlice.Ascending); - + const [sortField, setSortField] = React.useState('MeterName') + const [ascending, setAscending] = React.useState(true) + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const [removeRecord, setRemoveRecord] = React.useState(null); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); + const customerMeterController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/CustomerMeter`, 'MeterName', true), []) React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed') - getData(); - }, [status]) + setStatus('loading') + const h = customerMeterController.PagedSearch([], sortField, ascending, page, props.Customer.ID) + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setStatus('idle') + }).fail((d) => setStatus('error')) + }, [props.Customer.ID, sortField, ascending, page, refreshTrigger]) - React.useEffect(() => { - getData(); - }, [props.Customer.ID]) - - function getData() { - dispatch(CustomerMeterSlice.DBSearch({ - filter: [{ - FieldName: 'CustomerID', - SearchText: props.Customer.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }], sortField, ascending - })); - } function getEnum(setOptions, field) { let handle = null; @@ -87,7 +79,7 @@ const CustomerMeterWindow = (props: IProps) => { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } @@ -116,14 +108,14 @@ const CustomerMeterWindow = (props: IProps) => { }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } function saveCustomerMeters(m: SystemCenter.Types.DetailedMeter[]) { - m.forEach((meter) => { - dispatch(CustomerMeterSlice.DBAction({ - verb: 'POST', record: { + Promise.all(m.map((meter) => { + customerMeterController.DBAction( + 'POST', { ID: 0, CustomerKey: props.Customer.CustomerKey, CustomerName: props.Customer.Name, @@ -133,8 +125,8 @@ const CustomerMeterWindow = (props: IProps) => { MeterName: meter.Name, MeterID: meter.ID } - })) - }) + ) + })).then(() => setRefreshTrigger((val) => !val)) } function hasPermissions(): boolean { @@ -180,84 +172,100 @@ const CustomerMeterWindow = (props: IProps) => {
return <> -
-
-
-
-

Assigned Meters:

+
+
+
+
+

Assigned Meters:

+
-
-
-
- - TableClass="table table-hover" - Data={data} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Remove') - return; - dispatch(CustomerMeterSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); +
+
+ + TableClass="table table-hover" + Data={data} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(a => !a); + } + else { + setSortField(d.colField); + } }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'MeterName'} - AllowSort={true} - Field={'MeterName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'MeterKey'} - AllowSort={true} - Field={'MeterKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Key - - - Key={'MeterLocation'} - AllowSort={true} - Field={'MeterLocation'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Substation - - - Key={'Remove'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => - - } - >

- - + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'MeterName'} + AllowSort={true} + Field={'MeterName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Name + + + Key={'MeterKey'} + AllowSort={true} + Field={'MeterKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Key + + + Key={'MeterLocation'} + AllowSort={true} + Field={'MeterLocation'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Substation + + + Key={'Remove'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => + + } + >

+ + +
+
+ setPage(page - 1)} + Current={page + 1} + /> +
+
+
-
-
-
+
+
-
+ onMouseEnter={() => setHover('Update')} onMouseLeave={() => setHover('None')} onClick={() => { + if (hasPermissions()) + setShowAdd(true); + }}>Add Meters +

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

+
-
- { if (c) dispatch(CustomerMeterSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> + { if (c) customerMeterController.DBAction('DELETE', removeRecord).then(() => { setRemoveRecord(null); setRefreshTrigger((val) => !val) } )}} /> { }); handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) - return () => { if (handle != null && handle.abort == null) handle.abort(); } + return () => { if (handle != null && handle.abort != null) handle.abort(); } }} >
  • diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx index e707ad1a11..69f4daae88 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx @@ -27,9 +27,9 @@ import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import { useAppSelector, useAppDispatch } from '../hooks'; import { AdditionalFieldsSlice, ValueListGroupSlice } from '../Store/Store'; import AdditionalFieldForm from '../AdditionalFields/AdditionalFieldForm'; -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 { LoadingScreen, Modal, SearchBar, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingScreen, Modal, SearchBar, ServerErrorIcon, Warning, GenericController, Search } from '@gpa-gemstone/react-interactive'; import { SelectPopup } from '@gpa-gemstone/common-pages'; const emptyRecord: SystemCenter.Types.AdditionalFieldView = { @@ -50,8 +50,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const dispatch = useAppDispatch(); - const data = useAppSelector(AdditionalFieldsSlice.Data); - const status = useAppSelector(AdditionalFieldsSlice.Status); + // needed for the selection popup const searchData = useAppSelector(AdditionalFieldsSlice.SearchResults); const searchStatus = useAppSelector(AdditionalFieldsSlice.SearchStatus); @@ -62,7 +61,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const parentID = React.useRef(-1); const [tableStatus, setTableStatus] = React.useState('uninitiated'); const [asc, setAsc] = React.useState(true); - const [sortKey, setSortKey] = React.useState('FieldName'); + const [sortKey, setSortKey] = React.useState('FieldName'); const [record, setRecord] = React.useState(emptyRecord); const [showRemove, setShowRemove] = React.useState(false); @@ -73,71 +72,54 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const [overWriteFields, setOverWriteFields] = React.useState([]); - React.useEffect(() => { - if (status !== 'idle') return; - if (tableStatus === 'uninitiated' || tableStatus === 'changed' || parentID.current !== props.ID) { - setTableStatus('loading'); - parentID.current = props.ID; - const handle = $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/AdditionalFieldView/${parentID.current}/${sortKey}/${asc ? '1' : '0'}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: false, - async: true - }); - handle.done((results) => { - sortData(JSON.parse(results.toString())); - setTableStatus('idle'); - }); - handle.fail(() => { - setTableStatus('error'); - }); - } - }, [tableStatus, props.ID, status]); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); - React.useEffect(() => { - sortData(fieldsInTable); - }, [sortKey, asc]); + const additionalFieldsController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalFieldView`, "FieldName", true),[]) React.useEffect(() => { - if (status === 'uninitiated' || status === 'changed') - dispatch(AdditionalFieldsSlice.Fetch()); - }, [status]); + setTableStatus('loading'); + parentID.current = props.ID; + const filters: Search.IFilter[] = [{ SearchText: props.TableName, Operator: "=", IsPivotColumn: false, FieldName: "ExternalTable", Type: 'string' }] + const handle = additionalFieldsController.PagedSearch(filters, sortKey, asc, page) + handle.done((results) => { + setFieldsInTable(JSON.parse(results.Data as unknown as string)); + setTotalPages(results.NumberOfPages); + setTableStatus('idle'); + }); + handle.fail(() => { + setTableStatus('error'); + }); + return () => { + if (handle != null && handle.abort != null) handle.abort(); + }; + }, [sortKey, asc, props.ID, page, refreshTrigger]); React.useEffect(() => { if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') dispatch(ValueListGroupSlice.Fetch()); }, [valueListGroupStatus]); - /* TODO: we don't have any way to filter for null values here... this might need a gsf update - React.useEffect(() => { - localStorage.setItem(filterStorage, JSON.stringify([{ - FieldName: "ExternalDB", - SearchText: ``, - Operator: "LIKE", - Type: "integer", - IsPivotColumn: false - }])); - }, [props.ID]); - */ - - const sortData = React.useCallback((sortData: SystemCenter.Types.AdditionalFieldView[]) => { - setFieldsInTable(_.orderBy(sortData, [sortKey], [(asc ? "asc" : "desc")])); - }, [setFieldsInTable, sortKey, asc]); - function Delete() { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'DELETE', record: { ...record } })); - setRecord(emptyRecord); + additionalFieldsController.DBAction('DELETE', { ...record }).then(() => { + setRecord(emptyRecord); + setRefreshTrigger((val) => !val) + }); } function AssociateField(fld) { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record: { ...fld, ExternalDBTableID: props.ID } })); + additionalFieldsController.DBAction('PATCH', { ...fld, ExternalDBTableID: props.ID }).then(() => { + setRecord(emptyRecord); + setRefreshTrigger((val) => !val) + }) ; } function DisassociateField(fld) { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record: { ...fld, ExternalDBTableID: null } })); - setRecord(emptyRecord); + additionalFieldsController.DBAction('PATCH', { ...fld, ExternalDBTableID: null }).then(() => { + setRecord(emptyRecord); + setRefreshTrigger((val) => !val) + }); } return ( @@ -166,7 +148,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu OnSort={(d) => { if (d.colKey == 'btns') return; if (d.colKey === sortKey) setAsc(prev => !prev); - else setSortKey(d.colKey); + else setSortKey(d.colKey as keyof SystemCenter.Types.AdditionalFieldView); }} TheadStyle={{ fontSize: 'smaller' }} RowStyle={{ fontSize: 'smaller' }} @@ -204,7 +186,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu Field={'Searchable'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Searchable ? : } + Content={({ item }) => item.Searchable ? : } > Searchable @@ -213,7 +195,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu Field={'IsSecure'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} - Content={({ item }) => item.IsSecure ? : } + Content={({ item }) => item.IsSecure ? : } > Secure @@ -222,7 +204,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu Field={'IsInfo'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} - Content={({ item }) => item.IsInfo ? : } + Content={({ item }) => item.IsInfo ? : } > Info @@ -231,7 +213,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu Field={'IsKey'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} - Content={({ item }) => item.IsKey ? : } + Content={({ item }) => item.IsKey ? : } > Key @@ -256,6 +238,15 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu }
  • +
    +
    + { setPage(page - 1) }} + Total={totalPages} + /> +
    +
    @@ -310,8 +301,16 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu setShowNew(false); if (conf) { setTableStatus('changed'); - if (record.ID > 0) dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record })); - else if (record.ID == 0) dispatch(AdditionalFieldsSlice.DBAction({ verb: 'POST', record })); + if (record.ID > 0) { + additionalFieldsController.DBAction('PATCH', record).then(() => { + setRefreshTrigger((val) => !val) + }) + } + else if (record.ID == 0) { + additionalFieldsController.DBAction('POST', record).then(() => { + setRefreshTrigger((val) => !val) + }) + } } }} > @@ -358,31 +357,32 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu ResultNote={searchStatus == 'error' ? 'Could not complete Search' : 'Found ' + searchData.length + ' Additional Field(s)'} > {children} - Name - Parent Type - Field Type - External Database - External Table - row.item ? : } - >Searchable - row.item ? : } - >Secure - row.item ? : } - >Info - row.item ? : } - >Key } - /> + > + Name + Parent Type + Field Type + External Database + External Table + row.item ? : } + >Searchable + row.item ? : } + >Secure + row.item ? : } + >Info + row.item ? : } + >Key +
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx index 06441b4028..a25086f852 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx @@ -25,33 +25,36 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from "react-router-dom"; import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useAppDispatch, useBoundPaging } from '../hooks'; import { ExternalDBTablesSlice } from '../Store/Store'; import ExternalDBTableForm from './ExternalDBTableForm'; -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 { Modal, Warning } from '@gpa-gemstone/react-interactive'; +import { Modal, Warning, Search } from '@gpa-gemstone/react-interactive'; export default function ExternalDBTables(props: { ID: number }) { let navigate = useNavigate(); const dispatch = useAppDispatch(); - const data = useAppSelector(ExternalDBTablesSlice.Data); - const sortKey = useAppSelector(ExternalDBTablesSlice.SortField); - const asc = useAppSelector(ExternalDBTablesSlice.Ascending); + const data = useAppSelector(ExternalDBTablesSlice.SearchResults); const status = useAppSelector(ExternalDBTablesSlice.Status); const parentID = useAppSelector(ExternalDBTablesSlice.ParentID); + const currentPage = useAppSelector(ExternalDBTablesSlice.CurrentPage); + const totalPages = useAppSelector(ExternalDBTablesSlice.TotalPages); const emptyRecord: SystemCenter.Types.extDBTables = { ID: 0, TableName: '', ExtDBID: 0, Query: '' }; const [record, setRecord] = React.useState(emptyRecord); const [showWarning, setShowWarning] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); + const [sortField, setSortField] = React.useState('ID'); + const [ascending, setAscending] = React.useState(true); + + const filters: Search.IFilter[] = React.useMemo(() => [{ SearchText: props.ID.toString(), FieldName: 'ExtDBID', Operator: "=", IsPivotColumn: false, Type: 'string' }],[props.ID]) React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed' || parentID != props.ID) - dispatch(ExternalDBTablesSlice.Fetch(props.ID)); - }, [status, parentID, props.ID]); + dispatch(ExternalDBTablesSlice.PagedSearch({ filter: filters, sortField, ascending, page: currentPage})); + }, [status, parentID, props.ID, sortField, ascending, currentPage]); React.useEffect(() => { let e = []; @@ -75,6 +78,27 @@ export default function ExternalDBTables(props: { ID: number }) { navigate(`${homePath}index.cshtml?name=ExternalTable&ID=${item.row.ID}`); } + const setPage = React.useCallback((page) => { + dispatch(ExternalDBTablesSlice.PagedSearch({ filter: filters, sortField, ascending, page: page - 1 })) + }, [sortField, ascending]) + + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(ExternalDBTablesSlice.PagedSearch({ filter: filters, sortField: sort, ascending: asc, page: 0 })) + }, [ascending, sortField]) + + useBoundPaging(currentPage, totalPages, setPage) + return (
    @@ -93,12 +117,9 @@ export default function ExternalDBTables(props: { ID: number }) { TableClass="table table-hover" Data={data} - SortKey={sortKey.toString()} - Ascending={asc} - OnSort={(d) => { - if (d.colKey == 'btns') return; - dispatch(ExternalDBTablesSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + SortKey={sortField} + Ascending={ascending} + OnSort={sort} OnClick={handleSelect} TableStyle={{ padding: 0, width: '100%', height: '100%', tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' }} TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} @@ -141,6 +162,15 @@ export default function ExternalDBTables(props: { ID: number }) {
    +
    +
    + +
    +
    @@ -160,9 +190,9 @@ export default function ExternalDBTables(props: { ID: number }) { DisableConfirm={errors.length > 0} ShowX={true} CallBack={(conf) => { setShowModal(false); - if (conf && record.ID > 0) + if (conf && record.ID > 0) dispatch(ExternalDBTablesSlice.DBAction({ verb: 'PATCH', record })); - else if (conf && record.ID == 0) + else if (conf && record.ID == 0) dispatch(ExternalDBTablesSlice.DBAction({ verb: 'POST', record })); }} > diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/LineSegment/ByLineSegment.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/LineSegment/ByLineSegment.tsx index 330f93a241..19b23a3eea 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/LineSegment/ByLineSegment.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/LineSegment/ByLineSegment.tsx @@ -123,7 +123,7 @@ const ByLineSegment: Application.Types.iByComponent = (props) => { }); handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) - return () => { if (handle != null && handle.abort == null) handle.abort(); } + return () => { if (handle != null && handle.abort != null) handle.abort(); } }, []); return ( diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/ByLocation.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/ByLocation.tsx index a42c32f0ee..c10ce6c60a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/ByLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/ByLocation.tsx @@ -29,36 +29,38 @@ import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { AssetAttributes } from '../AssetAttribute/Asset'; import ExternalDBUpdate from '../CommonComponents/ExternalDBUpdate'; -import { Search, Modal } from '@gpa-gemstone/react-interactive'; +import { GenericController, Search, Modal, SearchBar } from '@gpa-gemstone/react-interactive'; import { Input, TextArea } from '@gpa-gemstone/react-forms'; -import { DefaultSearch } from '@gpa-gemstone/common-pages'; -import { ByLocationSlice } from '../Store/Store'; -import { useAppDispatch, useAppSelector } from '../hooks'; declare var homePath: string; const ByLocation: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const data = useAppSelector(ByLocationSlice.SearchResults); - const ascending = useAppSelector(ByLocationSlice.Ascending); - const sortKey = useAppSelector(ByLocationSlice.SortField); - const searchFields = useAppSelector(ByLocationSlice.SearchFilters); - const allKeys = useAppSelector(ByLocationSlice.Data); - const searchStatus = useAppSelector(ByLocationSlice.SearchStatus); - const status = useAppSelector(ByLocationSlice.Status); + const [data, setData] = React.useState([]); + const [ascending, setAscending] = React.useState(true); + const [sortKey, setSortKey] = React.useState('Name'); + const [searchFields, setSearchFields] = React.useState[]>([]); + const [allKeys, setAllKeys] = React.useState([]); + const [searchStatus, setSearchStatus] = React.useState('uninitiated'); + const [status, setStatus] = React.useState('uninitiated'); const [newLocation, setNewLocation] = React.useState(getNewLocation()); const [newLocationErrors, setNewLocationErrors] = React.useState([]); const [showEXTModal, setShowExtModal] = React.useState(false); const extDbUpdateAll = React.useRef<() => (() => void)>(undefined); - const allPages = useAppSelector(ByLocationSlice.TotalPages); - const currentPage = useAppSelector(ByLocationSlice.CurrentPage); - const [page, setPage] = React.useState(currentPage); + const [totalPages, setTotalPages] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [page, setPage] = React.useState(0); + const [addlFields, setAddlFields] = React.useState[]>([]); const [showNew, setShowNew] = React.useState(false); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + const locationController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/ByLocation`, "Name", true),[]) + React.useEffect(() => { let errors = []; if (newLocation.LocationKey == null || newLocation.LocationKey.length < 1) @@ -91,18 +93,35 @@ const ByLocation: Application.Types.iByComponent = (props) => { }, [newLocation, allKeys]); React.useEffect(() => { - if (status == 'changed' || status == 'uninitiated') - dispatch(ByLocationSlice.Fetch()); - }, [dispatch, status]) + setStatus('loading'); + const h = locationController.Fetch(); + h.done((d) => { + setAllKeys(d); + setStatus('idle'); + }) + h.fail(() => setStatus('error')) - React.useEffect(() => { - if (searchStatus == 'changed' || searchStatus == 'uninitiated') - dispatch(ByLocationSlice.PagedSearch({ filter: searchFields, sortField: sortKey, ascending, page })); - }, [searchStatus]); + return () => { + if (h != null || h.abort != null) h.abort(); + } + }, [locationController, refreshTrigger]) React.useEffect(() => { - dispatch(ByLocationSlice.PagedSearch({ filter: searchFields, sortField: sortKey, ascending, page })); - }, [searchFields, sortKey, ascending, page]); + setSearchStatus('loading'); + const h = locationController.PagedSearch(searchFields, sortKey, ascending, page); + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + setSearchStatus('idle'); + }) + h.fail(() => setSearchStatus('error')) + + return () => { + if (h != null || h.abort != null) h.abort(); + } + }, [locationController, searchFields, sortKey, ascending, page, refreshTrigger]); function getNewLocation() { return { @@ -119,7 +138,7 @@ const ByLocation: Application.Types.iByComponent = (props) => { } } - function getAdditionalFields(setFields) { + React.useEffect(() => { let handle = $.ajax({ type: "GET", url: `${homePath}api/SystemCenter/AdditionalFieldView/ParentTable/Location/FieldName/0`, @@ -140,13 +159,13 @@ const ByLocation: Application.Types.iByComponent = (props) => { let ordered = _.orderBy(d.filter(item => item.Searchable).map(item => ( { label: `[AF${item.ExternalDB != undefined ? " " + item.ExternalDB : ''}] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type), isPivotField: true } as Search.IField )), ['label'], ["asc"]); - setFields(ordered) + setAddlFields(ordered) }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; - } + }, []) function getEnum(setOptions, field) { let handle = null; @@ -164,7 +183,7 @@ const ByLocation: Application.Types.iByComponent = (props) => { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) + if (handle != null && handle.abort != null) handle.abort(); } } @@ -193,25 +212,42 @@ const ByLocation: Application.Types.iByComponent = (props) => { return (
    - + + CollumnList={[ + { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, + { label: 'Key', key: 'LocationKey', type: 'string', isPivotField: false }, + { label: 'Asset Key', key: 'Asset', type: 'string', isPivotField: false }, + { label: 'Meter Key', key: 'Meter', type: 'string', isPivotField: false }, + { label: 'Number of Assets', key: 'Assets', type: 'integer', isPivotField: false }, + { label: 'Number of Meters', key: 'Meters', type: 'integer', isPivotField: false }, + { label: 'Description', key: 'Description', type: 'string', isPivotField: false }, + ...addlFields]} + SetFilter={setSearchFields} + Direction={'left'} + defaultCollumn={{ label: 'Name', key: 'Name', type: 'string', isPivotField: false }} + Width={'50%'} + Label={'Search'} + ShowLoading={searchStatus === 'loading'} + ResultNote={searchStatus === 'error' ? 'Could not complete Search' : `Displaying Substation(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} + GetEnum={getEnum} + StorageID={'LocationsFilter'} + > - +
    @@ -221,9 +257,10 @@ const ByLocation: Application.Types.iByComponent = (props) => { Ascending={ascending} OnSort={(d) => { if (d.colKey === sortKey) - dispatch(ByLocationSlice.Sort({ SortField: sortKey, Ascending: ascending })); + setAscending(a => !a); else { - dispatch(ByLocationSlice.Sort({ SortField: d.colField as keyof SystemCenter.Types.DetailedLocation, Ascending: true })); + setAscending(true); + setSortKey(d.colField); } }} OnClick={handleSelect} @@ -269,7 +306,7 @@ const ByLocation: Application.Types.iByComponent = (props) => {
    - setPage(p - 1)} /> + setPage(p - 1)} />
    @@ -277,7 +314,7 @@ const ByLocation: Application.Types.iByComponent = (props) => { ShowX={true} CallBack={(conf) => { if (conf) - dispatch(ByLocationSlice.DBAction({ verb: "POST", record: newLocation })) + locationController.DBAction("POST", newLocation).done(() => setRefreshTrigger(val => !val)); setShowNew(false); }} ConfirmShowToolTip={newLocationErrors.length > 0} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx index c1b6ceae90..f4cf00f0bd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx @@ -26,25 +26,32 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA } from '@gpa-gemstone/application-typings'; import { Modal, LayoutGrid } from '@gpa-gemstone/react-interactive'; +import { Paging } from '@gpa-gemstone/react-table' declare var homePath: string; const LocationImagesWindow = (props: { Location: OpenXDA.Types.Location }) => { const [images, setImages] = React.useState([]); const [image, setImage] = React.useState(''); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(1) React.useEffect(() => { let handle = getImages(); - handle.done(i => {setImages(i)}); + handle.done(i => { + setImages(JSON.parse(i.Data)) + setTotalPages(i.NumberOfPages) + } + ); return () => { if (handle.abort != undefined) handle.abort(); }; - }, [props.Location.ID]); + }, [props.Location.ID, page]); function getImages(): JQuery.jqXHR { return $.ajax({ type: "GET", - url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images`, + url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, @@ -84,6 +91,14 @@ const LocationImagesWindow = (props: { Location: OpenXDA.Types.Location }) => { }
    +
    + { setPage(page - 1) }} + Current={page + 1} + Total={totalPages} + > + +
    0} ShowCancel={false} ShowX={true} ShowConfirm={false} Title={image} CallBack={() => setImage('') }> diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx index 1614e09431..8220659fbb 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx @@ -26,10 +26,7 @@ import { Table, Column } from '@gpa-gemstone/react-table'; import * as _ from 'lodash'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import ExternalDBUpdate from '../CommonComponents/ExternalDBUpdate'; -import { Search, Modal, LoadingScreen } from '@gpa-gemstone/react-interactive'; -import { DefaultSearch } from '@gpa-gemstone/common-pages'; -import { ByMeterSlice } from '../Store/Store'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { GenericController, Search, Modal, LoadingScreen, SearchBar } from '@gpa-gemstone/react-interactive'; import { useNavigate } from "react-router-dom"; import { Paging } from '@gpa-gemstone/react-table'; @@ -37,28 +34,39 @@ declare var homePath: string; const ByMeter: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const data = useAppSelector(ByMeterSlice.SearchResults); + const [data, setData] = React.useState([]); const [ascending, setAscending] = React.useState(true); const [sortKey, setSortKey] = React.useState("Name"); - - const cState = useAppSelector(ByMeterSlice.PagedStatus); - const allPages = useAppSelector(ByMeterSlice.TotalPages); - const currentPage = useAppSelector(ByMeterSlice.CurrentPage); - const [page, setPage] = React.useState(currentPage); + const [filters, setFilters] = React.useState[]>([]); + const [status, setStatus] = React.useState('uninitiated'); + const [totalPages, setTotalPages] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [page, setPage] = React.useState(0); + const [addlFields, setAddlFields] = React.useState[]>([]); const [showEXTModal, setShowExtModal] = React.useState(false); const extDbUpdateAll = React.useRef<() => (() => void)>(undefined); - React.useEffect(() => { - dispatch(ByMeterSlice.PagedSearch({ sortField: sortKey, ascending, page })); - }, [sortKey, ascending, page]); + const meterController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/ByMeter`, "Name", true), []) React.useEffect(() => { - if (cState === 'uninitiated' || cState === 'changed') - dispatch(ByMeterSlice.PagedSearch({ sortField: sortKey, ascending, page })); - }, [cState]); + setStatus('loading') + const h = meterController.PagedSearch(filters, sortKey, ascending, page); + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setRecordsPerPage(d.RecordsPerPage); + setTotalRecords(d.TotalRecords); + setStatus('idle'); + }) + h.fail(() => setStatus('error')) + + return () => { + if (h != null && h.abort != null) h.abort(); + } + }, [filters, sortKey, ascending, page, meterController]) function handleSelect(item) { navigate(`${homePath}index.cshtml?name=Meter&MeterID=${item.row.ID}`); @@ -67,8 +75,7 @@ const ByMeter: Application.Types.iByComponent = (props) => { navigate(`${homePath}index.cshtml?name=NewMeterWizard`); } - function getAdditionalFields(setFields) { - + React.useEffect(() => { let handle = $.ajax({ type: "GET", url: `${homePath}api/SystemCenter/AdditionalFieldView/ParentTable/Meter/FieldName/0`, @@ -77,27 +84,19 @@ const ByMeter: Application.Types.iByComponent = (props) => { async: true }); - function ConvertType(type: string) { - if (type == 'string' || type == 'integer' || type == 'number' || type == 'datetime' || type == 'boolean') - return { type: type } - return { - type: 'enum', enum: [{ Label: type, Value: type }] - } - } - handle.done((d: Array) => { let ordered = _.orderBy(d.filter(item => item.Searchable).map(item => ( { label: `[AF${item.ExternalDB != undefined ? " " + item.ExternalDB : ''}] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type), isPivotField: true } as Search.IField )), ['label'], ["asc"]); - setFields(ordered) + setAddlFields(ordered) }); return () => { - if (handle != null && handle.abort == null) { + if (handle != null && handle.abort != null) { handle.abort() }; }; - } + }, []) function getEnum(setOptions, field) { let handle = null; @@ -115,17 +114,37 @@ const ByMeter: Application.Types.iByComponent = (props) => { handle.done(d => setOptions(d.map(item => ({ Value: item.ID, Label: item.Value })))) return () => { - if (handle != null && handle.abort == null) + if (handle != null && handle.abort != null) handle.abort(); } } return (
    - +
    - + + CollumnList={[ + { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, + { label: 'Key', key: 'LocationKey', type: 'string', isPivotField: false }, + { label: 'Asset Key', key: 'Asset', type: 'string', isPivotField: false }, + { label: 'Meter Key', key: 'Meter', type: 'string', isPivotField: false }, + { label: 'Number of Assets', key: 'Assets', type: 'integer', isPivotField: false }, + { label: 'Number of Meters', key: 'Meters', type: 'integer', isPivotField: false }, + { label: 'Description', key: 'Description', type: 'string', isPivotField: false }, + ...addlFields + ]} + SetFilter={setFilters} + Direction={'left'} + defaultCollumn={{ label: 'Name', key: 'Name', type: 'string', isPivotField: false }} + Width={'50%'} + Label={'Search'} + ShowLoading={status === 'loading'} + ResultNote={status === 'error' ? 'Could not complete Search' : `Displaying Meter(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} + GetEnum={getEnum} + StorageID={"MetersFilter"} + >
  • Wizards: @@ -143,7 +162,7 @@ const ByMeter: Application.Types.iByComponent = (props) => {
  • -
    +
    @@ -218,7 +237,7 @@ const ByMeter: Application.Types.iByComponent = (props) => {
    - setPage(p - 1)} /> + setPage(p - 1)} />
    @@ -233,4 +252,12 @@ const ByMeter: Application.Types.iByComponent = (props) => { ) } -export default ByMeter; \ No newline at end of file +export default ByMeter; + +function ConvertType(type: string) { + if (type == 'string' || type == 'integer' || type == 'number' || type == 'datetime' || type == 'boolean') + return { type: type } + return { + type: 'enum', enum: [{ Label: type, Value: type }] + } +} \ No newline at end of file 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 fcae92e6c3..6cdd99c15a 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,13 +29,15 @@ 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'; declare let homePath: string; +const RecordsPerPage = 50; + interface IProps { Channels: OpenXDA.Types.Channel[], UpdateChannels: (channels: OpenXDA.Types.Channel[]) => void, @@ -59,6 +61,8 @@ const ChannelScalingForm = (props: IProps) => { const mcStatus = useAppSelector(MeasurementCharacteristicSlice.Status) as Application.Types.Status; const [status, setStatus] = React.useState('idle'); + //const totalPages = useAppSelector(TrendChannelSlice.TotalPages); + const [page, setPage] = React.useState(0); const [hover, setHover] = React.useState<('Reset' | 'None' | 'Replace' | 'Adjust')>('None'); const roles = useAppSelector(SelectRoles); @@ -201,10 +205,10 @@ const ChannelScalingForm = (props: IProps) => { }} Valid={(f) => true} />
    -
    +
    TableClass="table table-hover" - Data={Wrappers} + Data={Wrappers.slice(RecordsPerPage*page, RecordsPerPage*(page+1))} SortKey={''} Ascending={false} OnSort={(d) => { }} @@ -270,12 +274,21 @@ const ChannelScalingForm = (props: IProps) => { > If Adjusted -
    +
    +
    +
    + setPage(page - 1)} + /> +
    +
    return ( <> -
    +
    {cardBody}
    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..4d9b3e323e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -31,11 +31,11 @@ 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 { SelectEventChannels, SelectEventChannelStatus, SelectMeterID, dBAction, SelectPagedData, SelectNumberOfPages } from '../Store/EventChannelSlice'; +import { FetchChannels, PagedSearch } 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; @@ -47,8 +47,9 @@ const MeterEventChannelWindow = (props: IProps) => { const dispatch = useAppDispatch(); const data = useAppSelector(SelectEventChannels); - const sortKey = useAppSelector(SelectSortKey) - const ascending = useAppSelector(SelectAscending) + const pagedData = useAppSelector(SelectPagedData); + const [ascending, setAscending] = React.useState(true); + const [sortKey, setSortKey] = React.useState('Name') const status = useAppSelector(SelectEventChannelStatus); const meterID = useAppSelector(SelectMeterID); @@ -65,10 +66,14 @@ const MeterEventChannelWindow = (props: IProps) => { const [removeRecord, setRemoveRecord] = React.useState(null); const [errors, setErrors] = React.useState([]); + const totalPages = useAppSelector(SelectNumberOfPages); + const [page, setPage] = React.useState(0); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None' | 'Add')>('None'); const roles = useAppSelector(SelectRoles); - + React.useEffect(() => { + dispatch(PagedSearch({ meterId: props.Meter.ID, page: page, ascending: ascending, sortField: sortKey })); + }, [ascending, sortKey, page, props.Meter.ID]) React.useEffect(() => { if (pStatus == 'uninitiated' || pStatus == 'changed') @@ -83,7 +88,12 @@ const MeterEventChannelWindow = (props: IProps) => { React.useEffect(() => { if (status == 'uninitiated' || meterID !== props.Meter.ID || status == 'changed') dispatch(FetchChannels({ meterId: props.Meter.ID })); - }, [props.Meter,status]) + }, [props.Meter, status]) + + React.useEffect(() => { + if (status == 'uninitiated' || meterID !== props.Meter.ID || status == 'changed') + dispatch(PagedSearch({ meterId: props.Meter.ID, page: page, ascending: ascending, sortField: sortKey })); + }, [props.Meter, status]) React.useEffect(() => { if (!props.IsVisible) @@ -243,12 +253,12 @@ const MeterEventChannelWindow = (props: IProps) => {
    -
    -
    +
    +
    LocalStorageKey="MeterEventChannelConfigTable" TableClass="table table-hover" - Data={data.map(c => replicateChanges(c))} + Data={pagedData.map(c => replicateChanges(c))} 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 }} @@ -258,10 +268,12 @@ 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); + } }} > @@ -402,7 +414,15 @@ const MeterEventChannelWindow = (props: IProps) => {

    - +
    +
    +
    + setPage(page - 1)} + /> +
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx index cff4732234..fed2618f07 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx @@ -32,7 +32,7 @@ import { IsNumber } from '@gpa-gemstone/helper-functions'; import { TrendChannelSlice, PhaseSlice, MeasurmentTypeSlice, MeasurementCharacteristicSlice } from '../Store/Store'; import { AssetAttributes } from '../AssetAttribute/Asset'; import { useAppSelector, useAppDispatch } from '../hooks'; -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; @@ -42,9 +42,9 @@ interface IProps { Meter: GemstoneOpenXDA.Types.Meter, IsVisible: boolean } const MeterTrendChannelWindow = (props: IProps) => { const dispatch = useAppDispatch(); - const data = useAppSelector(TrendChannelSlice.Data); - const sortKey = useAppSelector(TrendChannelSlice.SortField) - const ascending = useAppSelector(TrendChannelSlice.Ascending) + const data = useAppSelector(TrendChannelSlice.SearchResults); + const [ascending, setAscending] = React.useState(true); + const [sortKey, setSortKey] = React.useState('Name') const status = useAppSelector(TrendChannelSlice.Status); const meterID = useAppSelector(TrendChannelSlice.ParentID); @@ -64,8 +64,13 @@ const MeterTrendChannelWindow = (props: IProps) => { const [errors, setErrors] = React.useState([]); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None' | 'Add')>('None'); + const totalPages = useAppSelector(TrendChannelSlice.TotalPages); + const [page, setPage] = React.useState(0); const roles = useAppSelector(SelectRoles); + const pagedSearch = React.useCallback(() => { + dispatch(TrendChannelSlice.PagedSearch({filter: [], sortField: sortKey, ascending, page})) + }, [page, ascending, sortKey, TrendChannelSlice.PagedSearch]) React.useEffect(() => { if (phaseStatus == 'uninitiated' || phaseStatus == 'changed') @@ -84,9 +89,18 @@ const MeterTrendChannelWindow = (props: IProps) => { React.useEffect(() => { if (status == 'uninitiated' || status == 'changed' || meterID !== props.Meter.ID) - dispatch(TrendChannelSlice.Fetch(props.Meter.ID)); + dispatch(TrendChannelSlice.Fetch(props.Meter.ID)); // left in because it sets the parent ID }, [props.Meter, status]); + React.useEffect(() => { + if (status == 'uninitiated' || status == 'changed' || meterID !== props.Meter.ID) + pagedSearch(); + }, [props.Meter, status, pagedSearch]); + + React.useEffect(() => { + pagedSearch(); + }, [page, sortKey, ascending]) + React.useEffect(() => { if (!props.IsVisible) return; let assetHandle = getAssets(); @@ -250,8 +264,8 @@ const MeterTrendChannelWindow = (props: IProps) => {
    -
    -
    +
    +
    LocalStorageKey="MeterTrendChannelConfigTable" TableClass="table table-hover" @@ -265,11 +279,12 @@ const MeterTrendChannelWindow = (props: IProps) => { SortKey={sortKey} Ascending={ascending} OnSort={(d) => { - - if (d.colKey === sortKey) - dispatch(TrendChannelSlice.Sort({ SortField: sortKey, Ascending: ascending })); - else - dispatch(TrendChannelSlice.Sort({ SortField: d.colField as keyof OpenXDA.TrendChannel, Ascending: true })); + if (d.colKey == sortKey) { + setAscending(a => !a); + } + else { + setSortKey(d.colField); + } }} > @@ -511,6 +526,15 @@ const MeterTrendChannelWindow = (props: IProps) => {
    +
    +
    + setPage(page - 1) } + /> +
    +
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/PropertyUI/MeterLocationProperties.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/PropertyUI/MeterLocationProperties.tsx index 967b23ba87..b660fa9adc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/PropertyUI/MeterLocationProperties.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/PropertyUI/MeterLocationProperties.tsx @@ -81,7 +81,7 @@ const MeterLocationProperties = (props: IProps) => { handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); } } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx index 8b4b2c8e6d..bc86da3e96 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import { RemoteXDAAssetSlice, ByAssetSlice } from '../Store/Store'; import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; @@ -45,15 +45,17 @@ const RemoteAssetTab = (props: IProps) => { const remoteAssetStatus = useAppSelector(RemoteXDAAssetSlice.Status); const searchResults = useAppSelector(RemoteXDAAssetSlice.SearchResults); const searchState = useAppSelector(RemoteXDAAssetSlice.SearchStatus); + const [page, setPage] = React.useState(0) + const totalPages = useAppSelector(RemoteXDAAssetSlice.TotalPages); const searchFilters: Search.IFilter[] = - [{ - FieldName: 'RemoteXDAInstanceID', - SearchText: props.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }] + [{ + FieldName: 'RemoteXDAInstanceID', + SearchText: props.ID.toString(), + Operator: '=', + Type: 'number', + IsPivotColumn: false + }] // Edit and Delete Form Consts const [newInstErrors, setNewInstErrors] = React.useState([]); @@ -76,13 +78,8 @@ const RemoteAssetTab = (props: IProps) => { }, [dispatch, remoteAssetStatus]); React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAAssetSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); - - React.useEffect(() => { - dispatch(RemoteXDAAssetSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + dispatch(RemoteXDAAssetSlice.PagedSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey, page })) + }, [ascending, sortKey, page, searchFilters]); React.useEffect(() => { if (assetStatus === 'uninitiated' || assetStatus === 'changed') @@ -106,120 +103,133 @@ const RemoteAssetTab = (props: IProps) => { cardBody = } else { cardBody = - - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalAssetName'} - AllowSort={true} - Field={'LocalAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'RemoteAssetName'} - AllowSort={true} - Field={'RemoteAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Name - - - Key={'RemoteAssetKey'} - AllowSort={true} - Field={'RemoteAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null} - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

    - - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

    - - + <> +
    + + TableClass="table table-hover" + Data={searchResults} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == 'Edit' || d.colKey == 'Delete') return; + if (d.colKey === sortKey) + setAscending(!ascending); + else { + setAscending(true); + setSortKey(d.colField); + } + }} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'LocalAssetName'} + AllowSort={true} + Field={'LocalAssetName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Local Name + + + Key={'LocalAssetKey'} + AllowSort={true} + Field={'LocalAssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Local Key + + + Key={'RemoteAssetName'} + AllowSort={true} + Field={'RemoteAssetName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Remote Name + + + Key={'RemoteAssetKey'} + AllowSort={true} + Field={'RemoteAssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Remote Key + + + Key={'Obsfucate'} + AllowSort={true} + Field={'Obsfucate'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Obsfucate ? : null} + > Obfuscated + + + Key={'Synced'} + AllowSort={true} + Field={'Synced'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Synced ? : null} + > Synced + + + Key={'Edit'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => (isEditable(item) ? + : null) + } + >

    + + + Key={'Delete'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => (isEditable(item) ? + : null) + } + >

    + + +
    +
    +
    + setPage(page - 1)} + Total={totalPages} + /> +
    +
    + } return ( diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx index cf73df784e..1b94499d59 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { SystemCenter, Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { RemoteXDAMeterSlice, ByMeterSlice, RemoteXDAAssetSlice } from '../Store/Store'; import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; @@ -45,6 +45,8 @@ const RemoteMeterTab = (props: IProps) => { const remoteMeterStatus = useAppSelector(RemoteXDAMeterSlice.Status) as Application.Types.Status; const searchResults = useAppSelector(RemoteXDAMeterSlice.SearchResults); const searchState = useAppSelector(RemoteXDAMeterSlice.SearchStatus); + const [page, setPage] = React.useState(0) + const totalPages = useAppSelector(RemoteXDAMeterSlice.TotalPages); const searchFilters: Search.IFilter[] = [{ @@ -94,13 +96,8 @@ const RemoteMeterTab = (props: IProps) => { }, [dispatch, meterStatus]); React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAMeterSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); - - React.useEffect(() => { - dispatch(RemoteXDAMeterSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + dispatch(RemoteXDAMeterSlice.PagedSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey, page })) + }, [searchFilters, ascending, sortKey, page]); function isEditable(item: OpenXDA.Types.RemoteXDAMeter): boolean { return item.RemoteXDAMeterID <= 0; @@ -141,137 +138,150 @@ const RemoteMeterTab = (props: IProps) => { cardBody = } else { cardBody = - - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalMeterName'} - AllowSort={true} - Field={'LocalMeterName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'LocalAlias'} - AllowSort={true} - Field={'LocalAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Alias - - - Key={'RemoteXDAName'} - AllowSort={true} - Field={'RemoteXDAName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? item.RemoteXDAName : item.LocalMeterName} - > Remote Name - - - Key={'RemoteXDAAssetKey'} - AllowSort={true} - Field={'RemoteXDAAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'RemoteAlias'} - AllowSort={true} - Field={'RemoteAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Alias - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null } - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

    - - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

    - - + <> +
    + + TableClass="table table-hover" + Data={searchResults} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == 'Edit' || d.colKey == 'Delete') return; + if (d.colKey === sortKey) + setAscending(!ascending); + else { + setAscending(true); + setSortKey(d.colField); + } + }} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'LocalMeterName'} + AllowSort={true} + Field={'LocalMeterName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Local Name + + + Key={'LocalAssetKey'} + AllowSort={true} + Field={'LocalAssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Local Key + + + Key={'LocalAlias'} + AllowSort={true} + Field={'LocalAlias'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Local Alias + + + Key={'RemoteXDAName'} + AllowSort={true} + Field={'RemoteXDAName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Obsfucate ? item.RemoteXDAName : item.LocalMeterName} + > Remote Name + + + Key={'RemoteXDAAssetKey'} + AllowSort={true} + Field={'RemoteXDAAssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Remote Key + + + Key={'RemoteAlias'} + AllowSort={true} + Field={'RemoteAlias'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Remote Alias + + + Key={'Obsfucate'} + AllowSort={true} + Field={'Obsfucate'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Obsfucate ? : null} + > Obfuscated + + + Key={'Synced'} + AllowSort={true} + Field={'Synced'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => item.Synced ? : null} + > Synced + + + Key={'Edit'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => (isEditable(item) ? + : null) + } + >

    + + + Key={'Delete'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => (isEditable(item) ? + : null) + } + >

    + + +
    +
    +
    + setPage(page - 1)} + Total={totalPages} + /> +
    +
    + } return ( @@ -308,7 +318,7 @@ const RemoteMeterTab = (props: IProps) => { CallBack={(conf) => { if (conf) dispatch(RemoteXDAMeterSlice.DBAction({ verb: 'DELETE', record: selectedMeter })); setShowDelete(false); - }}/> + }} /> { @@ -344,7 +354,7 @@ const RemoteMeterTab = (props: IProps) => { ShowX={true} Size={"sm"} ConfirmText={"Yes"} CancelText={"No"}> -

    Add { assetCount } Associated Assets?

    +

    Add {assetCount} Associated Assets?

    { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } @@ -171,7 +171,7 @@ export default function RemoteXDAInstanceForm(props: IProps) { }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts index 288617a56e..f9efb42ac2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts @@ -23,22 +23,78 @@ -import { createSlice, createAsyncThunk, PayloadAction, SerializedError } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, PayloadAction, SerializedError, AsyncThunk } from '@reduxjs/toolkit'; import { Application } from '@gpa-gemstone/application-typings'; import { OpenXDA } from '../global'; let fetchHandle = null; +let pagedHandle = null; -export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', async (args: { +const initialState: IEventChannelState = { + Status: 'uninitiated', + Error: null, + Data: [], + ActiveFetchID: [], + Asc: true, + Sort: 'Name', + ParentID: null, + NumberOfPages: 0, + PagedData: [] +} + +type Verb = 'POST' | 'DELETE' | 'PATCH' | 'SEARCH' + +interface IError { + Message: string, + Verb: Verb, + Time: string +} + +interface IEventChannelState { + Status: Application.Types.Status, + Error: IError | null, + Data: OpenXDA.EventChannel[], + ActiveFetchID: string[], + Asc: boolean, + Sort: keyof OpenXDA.EventChannel, + ParentID: string | number | null, + NumberOfPages: number, + PagedData: OpenXDA.EventChannel[] +} + +interface IPagedResults { + Data: string, + NumberOfPages: number, + RecordsPerPage: number, + TotalRecords: number +} + +interface IFetchArgs { sortField?: keyof OpenXDA.EventChannel, - ascending?: boolean, meterId: number -}, { getState, signal }) => { + ascending?: boolean, + meterId: number, +} + +interface IActionArgs { + verb: Verb, + record: OpenXDA.EventChannel +} + +interface IPagedSearchArgs extends IFetchArgs { + page: number +} + +type ThunkState = {EventChannels: IEventChannelState} + ; + + +export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', async (args: IFetchArgs, { getState, signal }) => { let sortfield = args.sortField; let asc = args.ascending; - sortfield = sortfield === undefined ? ((getState() as any).EventChannels).Sort : sortfield; - asc = asc === undefined ? (getState() as any).EventChannels.Ascending : asc; + sortfield = sortfield === undefined ? ((getState()).EventChannels).Sort : sortfield; + asc = asc === undefined ? (getState()).EventChannels.Asc : asc; if (fetchHandle != null && fetchHandle.abort != null) fetchHandle.abort('Prev'); @@ -53,7 +109,7 @@ export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', asy return await handle; }); -export const dBAction = createAsyncThunk(`EventChannels/DBAction`, async (args: { verb: 'POST' | 'DELETE' | 'PATCH', record: OpenXDA.EventChannel }, { signal }) => { +export const dBAction = createAsyncThunk(`EventChannels/DBAction`, async (args: IActionArgs, { signal }) => { const handle = Action(args.verb, args.record); signal.addEventListener('abort', () => { if (handle.abort !== undefined) handle.abort(); @@ -61,17 +117,30 @@ export const dBAction = createAsyncThunk(`EventChannels/DBAction`, async (args: return await handle }); +export const PagedSearch = createAsyncThunk('EventChannels/PagedSearch', async (args: IPagedSearchArgs, { getState, signal }) => { + + let sortfield = args.sortField; + let asc = args.ascending; + + sortfield = sortfield === undefined ? ((getState()).EventChannels).Sort : sortfield; + asc = asc === undefined ? (getState()).EventChannels.Asc : asc; + + if (pagedHandle != null && pagedHandle.abort != null) + pagedHandle.abort('Prev'); + + const handle = GetPagedRecords(asc, sortfield, args.meterId, args.page); + pagedHandle = handle; + + signal.addEventListener('abort', () => { + if (handle.abort !== undefined) handle.abort(); + }); + + return await handle; +}); + export const EventChannelSlice = createSlice({ name: 'EventChannel', - initialState: { - Status: 'unintiated' as Application.Types.Status, - Error: null, - Data: [] as OpenXDA.EventChannel[], - ActiveFetchID: [] as string[], - Asc: true as boolean, - Sort: 'Name' as keyof (OpenXDA.EventChannel), - ParentID: null - }, + initialState, reducers: { SetChanged: (state) => { state.Status = "changed"; @@ -96,7 +165,7 @@ export const EventChannelSlice = createSlice({ } }); - builder.addCase(FetchChannels.fulfilled, (state, action: PayloadAction) => { + builder.addCase(FetchChannels.fulfilled, (state, action: PayloadAction) => { state.ActiveFetchID = state.ActiveFetchID.filter(id => id !== action.meta.requestId); state.Status = 'idle'; state.Data = JSON.parse(action.payload); @@ -107,19 +176,42 @@ export const EventChannelSlice = createSlice({ builder.addCase(dBAction.pending, (state) => { state.Status = 'loading'; }); - builder.addCase(dBAction.rejected, (state, action: PayloadAction) => { + builder.addCase(dBAction.rejected, (state, action: PayloadAction) => { state.Status = 'error'; state.Error = { Message: (action.error.message == null ? '' : action.error.message), Verb: action.meta.arg.verb, Time: new Date().toString() } - }); builder.addCase(dBAction.fulfilled, (state) => { state.Status = 'changed'; state.Error = null; }); + builder.addCase(PagedSearch.pending, (state, action: PayloadAction) => { + state.Status = 'loading'; + state.ActiveFetchID.push(action.meta.requestId); + }); + builder.addCase(PagedSearch.rejected, (state, action: PayloadAction) => { + state.ActiveFetchID = state.ActiveFetchID.filter(id => id !== action.meta.requestId); + if (state.ActiveFetchID.length > 0) + return; + state.Status = 'error'; + state.Error = { + Message: (action.error.message == null ? '' : action.error.message), + Verb: 'SEARCH', + Time: new Date().toString() + } + }); + builder.addCase(PagedSearch.fulfilled, (state, action: PayloadAction) => { + state.ActiveFetchID = state.ActiveFetchID.filter(id => id !== action.meta.requestId); + state.Status = 'idle'; + state.PagedData = JSON.parse(action.payload.Data); + state.ParentID = action.meta.arg.meterId; + state.Sort = action.meta.arg.sortField ?? state.Sort; + state.Asc = action.meta.arg.ascending ?? state.Asc; + state.NumberOfPages = action.payload.NumberOfPages; + }); } }); @@ -131,6 +223,8 @@ export const SelectEventChannelStatus = (state) => state.EventChannels.Status as export const SelectMeterID = (state) => state.EventChannels.ParentID as number; export const SelectAscending = (state) => state.EventChannels.Asc as boolean; export const SelectSortKey = (state) => state.EventChannels.Sort as keyof OpenXDA.EventChannel; +export const SelectNumberOfPages = (state) => state.EventChannels.NumberOfPages as number; +export const SelectPagedData = (state) => state.EventChannels.PagedData as OpenXDA.EventChannel[]; function GetRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.EventChannel, parentID: number | void,): JQuery.jqXHR { return $.ajax({ @@ -143,9 +237,21 @@ function GetRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.E }); } -function Action(verb: 'POST' | 'DELETE' | 'PATCH', record: OpenXDA.EventChannel): JQuery.jqXHR { +function GetPagedRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.EventChannel, parentID: number | void, page: number): JQuery.jqXHR { + return $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/EventChannel${(parentID != null ? '/' + parentID : '')}/PagedList/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending, Searches: [] }) + }); +} + +function Action(verb: Verb, record: OpenXDA.EventChannel): JQuery.jqXHR { let action = ''; - if(verb === 'POST') action = 'Add'; + if (verb === 'POST') action = 'Add'; else if (verb === 'DELETE') action = 'Delete'; else if (verb === 'PATCH') action = 'Update'; diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts index 0a797447e8..0e123f16a2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts @@ -39,12 +39,15 @@ import { IApplicationRole, ISecurityGroup, IUserAccount } from '../User/Types'; import UserSettingsReducer from './UserSettings'; import { EventWidget } from '../../../../../EventWidgets/TSX/global'; import { IAPIAccessKey } from '../APIAccessKeys/APIAccessKeys' +import { combineReducers } from 'redux' declare var homePath: string; //Dispatch and Selector Types export type AppDispatch = typeof store.dispatch; -export type RootState = ReturnType + +// RootState purely from the reducer map +export type RootState = ReturnType /* Will combine those once we move to the generic ByPage */ export const ValueListGroupSlice = new GenericSlice('ValueListGroup', `${homePath}api/ValueListGroup`, 'Name'); @@ -138,80 +141,158 @@ export const ChannelTemplateSlice = new GenericSlice[] = [ { label: 'Username', key: 'DisplayName', type: 'string', isPivotField: false }, @@ -62,39 +62,48 @@ const newAcct: IUserAccount = { } const ByUser: Application.Types.iByComponent = (props) => { + + const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true), []) + const userAdditionalFieldController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalUserField`, "User"), []) + const valueListController = React.useMemo(() => new GenericController(`${homePath}api/ValueList`, 'SortOrder'), []) + const valueListGroupController = React.useMemo(() => new GenericController(`${homePath}api/ValueListGroup`, 'Name'), []) + let navigate = useNavigate(); const dispatch = useAppDispatch(); - const search = useAppSelector(UserAccountSlice.SearchFilters); + const [filters, setFilters] = React.useState[]>([]); - const data = useAppSelector(UserAccountSlice.SearchResults); - const userStatus: Application.Types.Status = useAppSelector(UserAccountSlice.Status); - const searchStatus: Application.Types.Status = useAppSelector(UserAccountSlice.SearchStatus); + const [users, setUsers] = React.useState([]); + const [userStatus, setUserStatus] = React.useState('uninitiated'); - const sortField = useAppSelector(UserAccountSlice.SortField) - const ascending = useAppSelector(UserAccountSlice.Ascending) - const currentPage = useAppSelector(UserAccountSlice.CurrentPage); - const totalPages = useAppSelector(UserAccountSlice.TotalPages); - const totalRecords = useAppSelector(UserAccountSlice.TotalRecords); + const [sortField, setSortField] = React.useState("DisplayName"); + const [ascending, setAscending] = React.useState(true); - const adlFields: Application.Types.iAdditionalUserField[] = useAppSelector(UserAdditionalFieldSlice.Fields) - const adlFieldStatus: Application.Types.Status = useAppSelector(UserAdditionalFieldSlice.FieldStatus) + 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 [adlFields, setAdlFields] = React.useState([]) + const [adlFieldStatus, setAdlFieldStatus] = React.useState('uninitiated'); const [filterableList, setFilterableList] = React.useState[]>(defaultSearchcols); const [showModal, setShowModal] = React.useState(false); const [userError, setUserError] = React.useState([]); - const valueListItems: SystemCenter.Types.ValueListItem[] = useAppSelector(ValueListSlice.Data); - const valueListItemStatus: Application.Types.Status = useAppSelector(ValueListSlice.Status); + const [valueListItems, setValueListItems] = React.useState([]); + const [valueListItemStatus, setValueListItemStatus] = React.useState('uninitiated'); - const valueListGroups: SystemCenter.Types.ValueListGroup[] = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus: Application.Types.Status = useAppSelector(ValueListGroupSlice.Status); + const [valueListGroups, setValueListGroups] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); const [act, setAct] = React.useState(newAcct) const [pageStatus, setPageStatus] = React.useState('uninitiated'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + React.useEffect(() => { if (userStatus === 'error' || adlFieldStatus === 'error' || valueListItemStatus === 'error' || valueListGroupStatus === 'error') setPageStatus('error') @@ -105,31 +114,59 @@ const ByUser: Application.Types.iByComponent = (props) => { }, [userStatus, adlFieldStatus, valueListItemStatus, valueListGroupStatus]) React.useEffect(() => { - if (searchStatus === 'uninitiated' || searchStatus === 'changed') - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: currentPage})); - }, [searchStatus, search, sortField, ascending, currentPage]); + setUserStatus('loading') + const h = userAccountController.PagedSearch(filters, sortField, ascending, page); + h.done((d) => { + setUsers(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setRecordsPerPage(d.RecordsPerPage) + setTotalRecords(d.TotalRecords) + if (d.NumberOfPages <= page) + setPage(d.NumberOfPages > 0 ? d.NumberOfPages - 1 : 0) + setUserStatus('idle') + }).fail(() => setUserStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [filters, sortField, ascending, page, userAccountController.PagedSearch, refreshTrigger]); React.useEffect(() => { - if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') - dispatch(UserAdditionalFieldSlice.FetchField()); + if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') { + const h = userAdditionalFieldController.Fetch(); + h.done((d) => { + setAdlFields(d as unknown as Application.Types.iAdditionalUserField[]) + setAdlFieldStatus('idle') + }).fail((d) => { + setAdlFieldStatus('error') + }) + } }, [adlFieldStatus]); - + React.useEffect(() => { - if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') - dispatch(ValueListSlice.Fetch()); + if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') { + const h = valueListController.Fetch(); + h.done((d) => { + setValueListItems(d as unknown as SystemCenter.Types.ValueListItem[]) + setValueListItemStatus('idle') + }).fail((d) => { + setValueListItemStatus('error') + }) + } }, [valueListItemStatus]); + React.useEffect(() => { - if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); + if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') { + const h = valueListGroupController.Fetch(); + h.done((d) => { + setValueListGroups(d as unknown as SystemCenter.Types.ValueListGroup[]) + setValueListGroupStatus('idle') + }).fail((d) => { + setValueListGroupStatus('error') + }) + } }, [valueListGroupStatus]); - - const setPage = React.useCallback((page) => { - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) - - useBoundPaging(currentPage, totalPages, setPage) - + React.useEffect(() => { function ConvertType(type: string) { if (type === 'string' || type === 'integer' || type === 'number' || type === 'datetime' || type === 'boolean') @@ -137,27 +174,23 @@ const ByUser: Application.Types.iByComponent = (props) => { return { type: 'enum', enum: [{ Label: type, Value: type }] } } const ordered = _.orderBy(defaultSearchcols.concat(adlFields.map(item => ( - { label: `[AF] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type) } as Search.IField + { label: `[AF] ${item.FieldName}`, key: item.FieldName, isPivotField: true, ...ConvertType(item.Type) } as Search.IField ))), ['label'], ["asc"]); setFilterableList(ordered) }, [adlFields]); - const setFilters = React.useCallback((filters: Search.IFilter[]) => { - dispatch(UserAccountSlice.PagedSearch({ sortField: sortField ?? "ID", ascending: ascending, filter: filters, page: currentPage })) - }, [sortField, ascending, currentPage]) - - if (pageStatus === 'error') - return
    - -
    ; + // refresh the slice so that users don't have to refresh after selecting a newly created user. + React.useEffect(() => { + dispatch(UserAccountSlice.Fetch()); + }, [refreshTrigger]) return (
    CollumnList={filterableList} SetFilter={setFilters} Direction={'left'} defaultCollumn={{ label: 'Username', key: 'DisplayName', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'} - ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' User Account(s)'} + ShowLoading={userStatus === 'loading'} ResultNote={userStatus === 'error' ? 'Could not complete search.' : 'Displaying User(s) ' + (totalRecords > 0 ? (recordsPerPage * page + 1) : 0) + ' - ' + (recordsPerPage * page + users.length) + ' out of ' + totalRecords} StorageID="UsersFilter" GetEnum={(setOptions, field) => { @@ -185,14 +218,19 @@ const ByUser: Application.Types.iByComponent = (props) => {
    -
    +
    TableClass="table table-hover" - Data={data} + Data={users} SortKey={sortField} Ascending={ascending} OnSort={(d) => { - dispatch(UserAccountSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colField === sortField) + setAscending(!ascending); + else { + setAscending(true); + setSortField(d.colField); + } }} OnClick={(d) => navigate(`${homePath}index.cshtml?name=User&UserAccountID=${d.row.ID}`)} TableStyle={{ @@ -259,16 +297,16 @@ const ByUser: Application.Types.iByComponent = (props) => {
    setPage(page - 1)} Total={totalPages} />
    { - if (confirm) - dispatch(UserAccountSlice.DBAction({ verb: 'POST', record: act })) + if (confirm) + userAccountController.DBAction('POST', act).then(() => { setRefreshTrigger((val) => !val)}) setAct(newAcct); setShowModal(false); }} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx index e87c5a9b62..60f3321430 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx @@ -23,14 +23,11 @@ import * as React from 'react'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen } from '@gpa-gemstone/react-interactive'; +import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen, GenericController } from '@gpa-gemstone/react-interactive'; import { Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; -import { useAppDispatch, useBoundPaging } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { SecurityGroupSlice } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; -import { useSelector } from 'react-redux'; import GroupForm from './GroupForm'; const defaultSearchcols: Search.IField[] = [ @@ -44,63 +41,61 @@ const emptyGroup: ISecurityGroup = { Name: "", CreatedBy: "", CreatedOn: new Dat const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - - const search = useSelector(SecurityGroupSlice.SearchFilters); - const data = useSelector(SecurityGroupSlice.SearchResults); - const currentPage = useSelector(SecurityGroupSlice.CurrentPage); - const totalPages = useSelector(SecurityGroupSlice.TotalPages); - const totalRecords = useSelector(SecurityGroupSlice.TotalRecords); - + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup),[]) + const [filters, setFilters] = React.useState[]>([]); + const [securityGroups, setSecurityGroups] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); const [status, setStatus] = React.useState('uninitiated'); - const searchStatus = useSelector(SecurityGroupSlice.SearchStatus); - - const sortField = useSelector(SecurityGroupSlice.SortField); - const ascending = useSelector(SecurityGroupSlice.Ascending); - + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(true); const [showModal, setShowModal] = React.useState(false); const [groupError, setGroupError] = React.useState([]); - const [newGroup, setNewGroup] = React.useState(emptyGroup); - const [pageStatus, setPageStatus] = React.useState('uninitiated'); - - React.useEffect(() => { - if (status === 'error') - setPageStatus('error') - else if (status === 'loading' ) - setPageStatus('loading') - else - setPageStatus('idle'); - }, [status]) + React.useEffect(() => { + const h = getUserGroups(securityGroupController, filters, sortField, ascending, currentPage) + h.done((d) => { + setSecurityGroups(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setRecordsPerPage(d.RecordsPerPage) + setTotalRecords(d.TotalRecords) + if (d.NumberOfPages <= currentPage) + setCurrentPage(d.NumberOfPages > 0 ? d.NumberOfPages - 1 : 0) + setStatus('idle') + }).fail(() => setStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [sortField, ascending, filters, currentPage, securityGroupController]) React.useEffect(() => { - if (searchStatus == 'uninitiated' || searchStatus == 'changed') - dispatch(SecurityGroupSlice.PagedSearch({ sortField, ascending, filter: search, page: currentPage})) - }, [searchStatus, sortField, ascending, search, currentPage]) - - const setFilters = React.useCallback((filters: Search.IFilter[]) => { - dispatch(SecurityGroupSlice.PagedSearch({ sortField, ascending, filter: filters, page: currentPage})) - }, [sortField, ascending, currentPage]) - - const setPage = React.useCallback((page) => { - dispatch(SecurityGroupSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) - - if (pageStatus === 'error') - return
    - -
    ; - - useBoundPaging(currentPage, totalPages, setPage) + if (status === 'changed') { + const h = getUserGroups(securityGroupController, filters, sortField, ascending, currentPage) + h.done((d) => { + setSecurityGroups(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setRecordsPerPage(d.RecordsPerPage) + setTotalRecords(d.TotalRecords) + if (d.NumberOfPages <= currentPage) + setCurrentPage(d.NumberOfPages > 0 ? d.NumberOfPages - 1 : 0) + setStatus('idle') + }).fail(() => setStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); + } + } + }, [status, sortField, ascending, filters, currentPage, securityGroupController]) return (
    - +
    CollumnList={defaultSearchcols} SetFilter={setFilters} Direction={'left'} defaultCollumn={{ label: 'Name', key: 'DisplayName', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'} - ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' User Group(s)'} + ShowLoading={status === 'loading'} ResultNote={'Displaying User Group(s) ' + (totalRecords > 0 ? (recordsPerPage * currentPage + 1) : 0) + ' - ' + (recordsPerPage * currentPage + securityGroups.length) + ' out of ' + totalRecords} StorageID="UsersGroupFilter" GetEnum={() => { return () => { } @@ -121,11 +116,16 @@ const ByUser: Application.Types.iByComponent = (props) => {
    TableClass="table table-hover" - Data={data} + Data={securityGroups} SortKey={sortField} Ascending={ascending} OnSort={(d) => { - dispatch(SecurityGroupSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colField === sortField) + setAscending(!ascending); + else { + setAscending(true); + setSortField(d.colField); + } }} OnClick={(d) => navigate(`${homePath}index.cshtml?name=Group&GroupID=${d.row.ID}`)} TableStyle={{ @@ -185,19 +185,22 @@ const ByUser: Application.Types.iByComponent = (props) => {
    setCurrentPage(page - 1)} Total={totalPages} />
    { - if (confirm) - dispatch(SecurityGroupSlice.DBAction({ - verb: 'POST', - record: { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0? newGroup.Name : newGroup.DisplayName) } - })) - setShowModal(false); + if (confirm) { + securityGroupController.DBAction( + 'POST', + { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } + ).then(() => { + setStatus('changed'); + setShowModal(false); + }) + } }} ConfirmShowToolTip={groupError.length > 0} ConfirmToolTipContent={<> @@ -213,3 +216,7 @@ const ByUser: Application.Types.iByComponent = (props) => { } export default ByUser; + +function getUserGroups(controller: GenericController, filters: Search.IFilter[], sortField: keyof ISecurityGroup, ascending: boolean, page: number) { + return controller.PagedSearch(filters, sortField, ascending, page) +} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx index 6149a523b5..ed7e1f5f94 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx @@ -26,64 +26,45 @@ import * as _ from 'lodash'; import { LoadingScreen } from '@gpa-gemstone/react-interactive'; import { UserAccountSliceRemote } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; -const GroupUser = (props: {Group: ISecurityGroup}) => { +const GroupUser = (props: { Group: ISecurityGroup }) => { const [showSelect, setShowSelect] = React.useState(false); const [users, setUsers] = React.useState([]); const [asc, setAsc] = React.useState(true); const [sortField, setSortField] = React.useState('AccountName'); const [status, setStatus] = React.useState('uninitiated'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false) React.useEffect(() => { - const handle = getUsers(); - return () => { if (handle != null && handle.abort != null) handle.abort(); } - }, [props.Group.ID, props.Group.Type]) - - React.useEffect(() => { - setUsers((u) => _.orderBy(u, [sortField], asc ? 'asc' : 'desc')); - }, [asc, sortField]) - - function getUsers() { if (props.Group.Type != 'Database') return; - setStatus('loading') - return $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/${props.Group.ID}`, - contentType: "application/json; charset=utf-8", - cache: false, - async: true - }).done((d) => { - setUsers(_.orderBy(d, [sortField], asc ? 'asc' : 'desc')); - setStatus('idle'); - }, () => setStatus('error')); - } - - function saveUser(u) { - if (props.Group.Type != 'Database') - return; - - setStatus('loading'); - - return $.ajax({ + const handle = $.ajax({ type: "POST", - url: `${homePath}api/SystemCenter/FullSecurityGroup/AddUser/${props.Group.ID}`, + url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/PagedList/${props.Group.ID}/${page}`, contentType: "application/json; charset=utf-8", - data: JSON.stringify(u), cache: false, - async: true - }).done((d) => { - setUsers(_.orderBy(d, [sortField], asc ? 'asc' : 'desc')); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: asc }) + }) + setStatus('loading') + handle.done((d) => { + setTotalPages(d.NumberOfPages); + setUsers(JSON.parse(d.Data)); setStatus('idle'); - }, () => setStatus('error')); - } + }).fail(() => setStatus('error')) + + return () => { if (handle != null && handle.abort != null) handle.abort(); } + }, [props.Group.ID, props.Group.Type, asc, sortField, page, refreshTrigger]) if (props.Group == null) return null; + return (
    @@ -98,71 +79,86 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { {props.Group.Type == 'Azure' ?
    Users in an Azure Group cannot be edited in System Center. To add or remove Users, please contact your Azure Administrator.
    : null} - {props.Group.Type == 'AD'?
    + {props.Group.Type == 'AD' ?
    Users in an Active Directory Group cannot be edited in System Center. To add or remove Users, please contact your AD Administrator.
    : null} {props.Group.Type == 'Database' ? - - TableClass="table table-hover" - Data={users} - SortKey={sortField} - Ascending={asc} - OnSort={(d) => { - if (d.colField === sortField) - setAsc(!asc); - else { - setAsc(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, width: '100%' }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'AccountName'} - AllowSort={true} - Field={'AccountName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Username - - - Key={'FirstName'} - AllowSort={true} - Field={'FirstName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > First Name - - - Key={'LastName'} - AllowSort={true} - Field={'LastName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Last Name - - - Key={'Phone'} - AllowSort={true} - Field={'Phone'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Phone - - - Key={'Email'} - AllowSort={true} - Field={'Email'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Email - - + <> +
    +
    + + TableClass="table table-hover" + Data={users} + SortKey={sortField} + Ascending={asc} + OnSort={(d) => { + if (d.colField === sortField) + setAsc(!asc); + else { + setAsc(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, width: '100%' }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'AccountName'} + AllowSort={true} + Field={'AccountName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Username + + + Key={'FirstName'} + AllowSort={true} + Field={'FirstName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > First Name + + + Key={'LastName'} + AllowSort={true} + Field={'LastName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Last Name + + + Key={'Phone'} + AllowSort={true} + Field={'Phone'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Phone + + + Key={'Email'} + AllowSort={true} + Field={'Email'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Email + + +
    +
    +
    +
    + setPage(page - 1)} + Total={totalPages} + /> +
    +
    + : null}
    @@ -180,7 +176,7 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { OnClose={(selected, conf) => { setShowSelect(false); if (!conf) return; - saveUser(selected); + saveUser(selected, props.Group.Type, props.Group.ID).done(() => setRefreshTrigger((val) => !val)); }} Show={showSelect} Type={'multiple'} @@ -206,3 +202,17 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { } export default GroupUser; + +function saveUser(u: Application.Types.iUserAccount[], groupType: string, groupID: string) { + if (groupType != 'Database') + return; + + return $.ajax({ + type: "POST", + url: `${homePath}api/SystemCenter/FullSecurityGroup/AddUser/${groupID}`, + contentType: "application/json; charset=utf-8", + data: JSON.stringify(u), + cache: false, + async: true + }) +} \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx index 461286c6fe..0e528b502d 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx @@ -21,11 +21,10 @@ // ****************************************************************************************************** import * as React from 'react'; -import { LoadingScreen, ServerErrorIcon, TabSelector, Warning } from '@gpa-gemstone/react-interactive'; +import { Application } from '@gpa-gemstone/application-typings' +import { LoadingScreen, ServerErrorIcon, TabSelector, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { SecurityGroupSlice } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; import GroupInfo from './Info'; import GroupUser from './GroupUsers'; @@ -36,12 +35,12 @@ declare type Tab = 'info' | 'users' | 'roles' interface IProps { GroupID: string, Tab: Tab } function UserGroup(props: IProps) { + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup), []) const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const group: ISecurityGroup = useAppSelector((state) => SecurityGroupSlice.Datum(state, props.GroupID) as ISecurityGroup); - const status = useAppSelector(SecurityGroupSlice.Status); + const [group, setGroup] = React.useState(null) const [tab, setTab] = React.useState(getTab()); const [showWarning, setShowWarning] = React.useState(false); + const [status, setStatus] = React.useState('uninitiated') function getTab(): Tab { if (props.Tab != undefined) return props.Tab; @@ -58,10 +57,28 @@ function UserGroup(props: IProps) { }, [tab]); React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed') - dispatch(SecurityGroupSlice.Fetch()); + if (status == 'uninitiated' || status == 'changed') { + const h = securityGroupController.Fetch(); + return () => { + if (h.abort != undefined) h.abort(); + } + } }, [status]) + React.useEffect(() => { + setStatus('loading') + const h = getGroup(props.GroupID); + h.done((d) => { + setGroup(d) + setStatus('idle') + }).fail((d) => { + setStatus('error') + }) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [props.GroupID]) + if (status === 'error') return
    @@ -94,8 +111,7 @@ function UserGroup(props: IProps) { { setShowWarning(false); if (c) { - dispatch(SecurityGroupSlice.DBAction({ verb: 'DELETE', record: group })); - navigate(`${homePath}index.cshtml?name=Groups`); + securityGroupController.DBAction('DELETE', group).then(() => navigate(`${homePath}index.cshtml?name=Groups`) ); } }} />
    @@ -105,3 +121,14 @@ function UserGroup(props: IProps) { } export default UserGroup; + +function getGroup(groupID: string) { + return $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/FullSecurityGroup/One/${groupID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true + }) +} \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx index dbecd776e2..aee2a9180c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx @@ -23,13 +23,11 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { ValueListSlice } from '../Store/Store'; +import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import ValueListForm from './ValueListForm'; -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 { Modal } from '@gpa-gemstone/react-interactive'; +import { Modal, Search, GenericController, ServerErrorIcon, LoadingScreen } from '@gpa-gemstone/react-interactive'; import { ValueListItemDelete, RequiredValueLists } from './ValueListGroupDelete'; import { ToolTip } from '@gpa-gemstone/react-forms'; @@ -37,23 +35,24 @@ interface IProps { Record: SystemCenter.Types.ValueListGroup } -export default function ValueListGroupItems(props: IProps) { - const dispatch = useAppDispatch(); - - const data = useAppSelector(ValueListSlice.Data); - const sortKey = useAppSelector(ValueListSlice.SortField); - const asc = useAppSelector(ValueListSlice.Ascending); - const status = useAppSelector(ValueListSlice.Status); - const parentID= useAppSelector(ValueListSlice.ParentID); - const emptyRecord: SystemCenter.Types.ValueListItem = { ID: 0, GroupID: parentID as number, Value: '', AltValue: null, SortOrder: 0 }; +export default function ValueListGroupItems(props: IProps) { + const [data, setData] = React.useState([]) + const emptyRecord: SystemCenter.Types.ValueListItem = { ID: 0, GroupID: props.Record.ID, Value: '', AltValue: null, SortOrder: 0 }; const [record, setRecord] = React.useState(emptyRecord); const [showWarning, setShowWarning] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); - const [countDictionary, setCountDictionary] = React.useState<{ [key: string]: number }>({}); const [hover, setHover] = React.useState(''); + const [ascending, setAscending] = React.useState(true); + const [sortField, setSortField] = React.useState('SortOrder') + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [status, setStatus] = React.useState('uninitiated') + const [refreshTrigger, setRefreshTrigger] = React.useState(false) + + const controller = React.useMemo(() => new GenericController(`${homePath}api/ValueList`, 'SortOrder'), []); const disallowReason = React.useCallback((ID: string) => { if (!RequiredValueLists.includes(props.Record?.Name)) @@ -67,9 +66,20 @@ export default function ValueListGroupItems(props: IProps) { }, [props.Record?.Name, data.length, countDictionary]); React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed' || parentID != props.Record.ID) - dispatch(ValueListSlice.Fetch(props.Record.ID)); - }, [status, parentID, props.Record.ID]); + setStatus('loading') + const filters = [{ FieldName: "GroupID", SearchText: props.Record.ID.toString(), Operator: "=" as Search.OperatorType, IsPivotColumn: false, Type: "number" as Search.FieldType }] + const h = controller.PagedSearch(filters, sortField, ascending, page) + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setStatus('idle') + }).fail((d) => { + setStatus('error') + }) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [controller, sortField, ascending, page, props.Record, refreshTrigger]); React.useEffect(() => { if (props.Record?.Name == null) return; @@ -88,118 +98,137 @@ export default function ValueListGroupItems(props: IProps) { }, [props.Record?.Name]); return ( -
    -
    -
    -
    -

    List Items:

    +
    + {status === 'error' ? : + status === 'loading' ? : <> +
    +
    +
    +

    List Items:

    +
    +
    +
    +
    +
    +
    + + TableClass="table table-hover" + Data={data} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colField === 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, width: '100%' }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Value'} + AllowSort={true} + Field={'Value'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Value + + + Key={'AltValue'} + AllowSort={true} + Field={'AltValue'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Label + + + Key={'SortOrder'} + AllowSort={true} + Field={'SortOrder'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Sort Order + + + Key={'btns'} + AllowSort={false} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={({ item }) => { + const id = item.ID.toString(); + const isDisallowed = disallowReason(id) != null; + return ( + <> + + + + ); + }} + >

    + + +
    +
    +
    +
    + setPage(page - 1)} + Current={page + 1} + Total={totalPages} + /> +
    +
    -
    -
    -
    -
    - - TableClass="table table-hover" - Data={data} - SortKey={sortKey} - Ascending={asc} - OnSort={(d) => { - if (d.colKey == 'btns') - return; - dispatch(ValueListSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); +
    +
    + +
    +
    + { + if (conf) { + controller.DBAction('DELETE', { ...record }).then(() => setRefreshTrigger((val) => !val)) + } + setShowWarning(false); }} - 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, width: '100%' }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Value'} - AllowSort={true} - Field={'Value'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Value - - - Key={'AltValue'} - AllowSort={true} - Field={'AltValue'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Label - - - Key={'SortOrder'} - AllowSort={true} - Field={'SortOrder'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Sort Order - - - Key={'btns'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => { - const id = item.ID.toString(); - const isDisallowed = disallowReason(id) != null; - return ( - <> - - - - ); - }} - >

    - - -
    -
    -
    -
    - -
    -
    - { - if (conf) - dispatch(ValueListSlice.DBAction({ verb: 'DELETE', record: { ...record } })); - setShowWarning(false); - }} - Record={record} - GroupItemCount={data.length} - AssignedDictionary={countDictionary} - Group={props.Record} - /> + Record={record} + GroupItemCount={data.length} + AssignedDictionary={countDictionary} + Group={props.Record} + /> + + } {disallowReason(hover)} @@ -209,18 +238,16 @@ export default function ValueListGroupItems(props: IProps) { DisableConfirm={errors.length > 0} ShowX={true} CallBack={(conf) => { setShowModal(false); - if (conf && record.ID > 0) - dispatch(ValueListSlice.DBAction({ verb: 'PATCH', record })); - else if (conf && record.ID == 0) - dispatch(ValueListSlice.DBAction({ verb: 'POST', record })); + if (conf && record.ID > 0) { + controller.DBAction('PATCH', record).then(() => setRefreshTrigger((val) => !val)) + } + else if (conf && record.ID == 0) { + controller.DBAction('POST', record).then(() => setRefreshTrigger((val) => !val)) + } }} > -
    - - +
    ); - -} - +} \ No newline at end of file