diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 97c92e7147..6335377d46 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -33,22 +33,21 @@ 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 { [RoutePrefix("api/OpenXDA/AssetGroup")] public class OpenXDAAssetGroupController : ModelController { - private class extendedAssetGroupView: AssetGroupView + private class extendedAssetGroupView : AssetGroupView { public List MeterList { get; set; } public List AssetList { get; set; } @@ -56,7 +55,7 @@ private class extendedAssetGroupView: AssetGroupView public List AssetGroupList { get; set; } } - + [HttpGet, Route("{assetGroupID:int}/Assets")] public IHttpActionResult GetAssets(int assetGroupID) { @@ -93,7 +92,7 @@ GROUP BY AssetAssetGroup.AssetGroupID HAVING AssetAssetGroup.AssetGroupID = {0}"; - return Ok(connection.RetrieveData(sql,assetGroupID)); + return Ok(connection.RetrieveData(sql, assetGroupID)); } catch (Exception ex) { @@ -105,6 +104,74 @@ 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[] sortFields = { "AssetName", "AssetKey", "AssetType" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + { + return BadRequest($"{postData.OrderBy} is not a valid search field."); + } + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string allSql = $@"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}} + "; + + DataTable allRecords = connection.RetrieveData(allSql, assetGroupID); + + string sql = $@"{allSql} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {page * recordsPerPage} ROWS FETCH NEXT {recordsPerPage} ROWS ONLY + "; + + DataTable records = connection.RetrieveData(sql, assetGroupID); + + int totalRecords = allRecords.Rows.Count; + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddAssets")] public IHttpActionResult AddAssets(int assetGroupID, [FromBody] IEnumerable assets) { @@ -119,7 +186,7 @@ public IHttpActionResult AddAssets(int assetGroupID, [FromBody] IEnumerable { int n = connection.ExecuteScalar("Select Count(ID) FROM AssetAssetGroup WHERE AssetID = {0} AND AssetGroupID = {1}", assetID, assetGroupID); if (n == 0) - assetassetGroupTbl.AddNewRecord( new AssetAssetGroup() { AssetGroupID = assetGroupID, AssetID = assetID}); + assetassetGroupTbl.AddNewRecord(new AssetAssetGroup() { AssetGroupID = assetGroupID, AssetID = assetID }); } return Ok(1); } @@ -192,8 +259,8 @@ GROUP BY Location.Name, MeterAssetGroup.AssetGroupID HAVING MeterAssetGroup.AssetGroupID = {0}"; - - return Ok(connection.RetrieveData(sql,assetGroupID)); + + return Ok(connection.RetrieveData(sql, assetGroupID)); } catch (Exception ex) { @@ -205,6 +272,71 @@ 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[] sortFields = { "Name", "Location" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + { + return BadRequest($"{postData.OrderBy} is not a valid search field."); + } + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string allSql = $@"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}} + "; + + DataTable allRecords = connection.RetrieveData(allSql, assetGroupID); + + string orderedPagedSql = $@"{allSql} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + OFFSET {page * recordsPerPage} ROWS FETCH NEXT {recordsPerPage} ROWS ONLY; + "; + + DataTable records = connection.RetrieveData(orderedPagedSql, assetGroupID); + + int totalRecords = allRecords.Rows.Count; + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddMeters")] public IHttpActionResult AddMeters(int assetGroupID, [FromBody] IEnumerable meters) { @@ -293,7 +425,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 +437,34 @@ 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; + + String[] sortFields = { "AssetGroups", "Meters", "Assets", "Name" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + return BadRequest($"{postData.OrderBy} is not a valid search field."); + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecords($"{postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")}", new RecordRestriction("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID)); + + 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) { @@ -368,7 +527,7 @@ public override IHttpActionResult Delete(AssetGroupView record) using (AdoDataConnection connection = new AdoDataConnection(Connection)) { - + int id = record.ID; int result = connection.ExecuteNonQuery($"EXEC UniversalCascadeDelete 'AssetGroup', 'ID = {id}'"); return Ok(result); @@ -396,14 +555,14 @@ public override IHttpActionResult Post([FromBody] JObject record) { extendedAssetGroupView newRecord = record.ToObject(); - AssetGroup newGroup = new AssetGroup() { ID= newRecord.ID, DisplayDashboard = newRecord.DisplayDashboard, Name= newRecord.Name, DisplayEmail=newRecord.DisplayEmail }; + AssetGroup newGroup = new AssetGroup() { ID = newRecord.ID, DisplayDashboard = newRecord.DisplayDashboard, Name = newRecord.Name, DisplayEmail = newRecord.DisplayEmail }; int result = new TableOperations(connection).AddNewRecord(newRecord); return Ok(new TableOperations(connection).QueryRecordWhere("Name = {0}", newRecord.Name)); - - } + + } } else { diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index fabcc4c32f..9aedca1d3d 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; + + string[] sortFields = { "Name", "LocationKey", "Latitude", "Longitude" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy))) + return BadRequest($"{postData.OrderBy} is not a valid search field."); + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecords($"{postData.OrderBy} {(postData.Ascending ? "asc" : "desc")}", new RecordRestriction("ID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = {0})", assetID)); + + 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; + + string[] sortFields = { "AssetKey", "Name", "Make", "Model" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + return BadRequest($"{postData.OrderBy} is not a valid search field."); + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecords($"{postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")}", new RecordRestriction("ID IN (SELECT MeterID FROM MeterAsset WHERE AssetID = {0})", assetID)); + + 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,59 @@ 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[] sortFields = { "AssetName", "AssetKey", "Name" }; + + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + return BadRequest($"{postData.OrderBy} is not a valid search field."); + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = @$" + 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 {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")}"; + + DataTable records = connection.RetrieveData(sql, assetID); + + int totalRecords = records.Rows.Count; + + string pagedSql = $"{sql} OFFSET {page * recordsPerPage} ROWS FETCH NEXT {recordsPerPage} ROWS ONLY;"; + + DataTable pagedRecords = connection.RetrieveData(pagedSql, assetID); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedRecords); + } + + return Ok(results); + } + [HttpGet, Route("{assetID:int}/OtherLocations")] public IHttpActionResult GetOtherLocations(int assetID) { @@ -575,13 +685,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 +706,53 @@ 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; + + PropertyInfo orderByProperty = typeof(ChannelDetail).GetProperty(postData.OrderBy, bindingFlags); + + if (postData.Ascending) + uniqueChannels = uniqueChannels.OrderBy(item => orderByProperty.GetValue(item)); + else + uniqueChannels = uniqueChannels.OrderByDescending(item => orderByProperty.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..b6d77454a2 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, Take ?? 50)); + } + + 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,25 @@ public IHttpActionResult GetImagesForLocation(int locationID) { return InternalServerError(ex); } + } + + +public static PagedResults PageImagePaths(IEnumerable imagePaths, int page, int recordsPerPage) + { + int totalImages = imagePaths.Count(); + + IEnumerable pagedImagePaths = imagePaths + .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..9078adffab 100644 --- a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs +++ b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs @@ -100,6 +100,38 @@ 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[] sortFields = { "Phone", "Email", "FirstName", "LastName", "AccountName" }; + if (!sortFields.Any(f => f.Equals(postData.OrderBy, StringComparison.OrdinalIgnoreCase))) + return BadRequest("Invalid 'OrderBy' field."); + + 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 {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} OFFSET {page * recordsPerPage} ROWS FETCH NEXT {recordsPerPage} ROWS ONLY"; + string countSql = "SELECT COUNT(*) FROM SecurityGroupUserAccount JOIN UserAccount ON UserAccountID = UserAccount.ID WHERE SecurityGroupID = {0}"; + + DataTable results = connection.RetrieveData(sql, groupID.ToString()); + int count = connection.ExecuteScalar(countSql, groupID); + + // 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..8ea5bc97bf 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx @@ -22,15 +22,14 @@ //****************************************************************************************************** import * as React from 'react'; -import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Modal, Search, SearchBar } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { LoadingScreen, GenericController, Modal, Search, SearchBar, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; import AdditionalFieldForm from './AdditionalFieldForm'; -import { useAppDispatch, useAppSelector, useBoundPaging } from '../hooks'; -import { AdditionalFieldsSlice, ValueListGroupSlice } from '../Store/Store'; const AdditionalFieldDefaultSearchField: Search.IField = { label: 'Name', key: 'FieldName', type: 'string', isPivotField: false }; + const emptyRecord: SystemCenter.Types.AdditionalField = { ID: 0, ParentTable: 'Meter', @@ -44,26 +43,33 @@ const emptyRecord: SystemCenter.Types.AdditionalField = { }; const ByAdditionalField: Application.Types.iByComponent = (props) => { - const dispatch = useAppDispatch(); - const data = useAppSelector(AdditionalFieldsSlice.SearchResults); - const status = useAppSelector(AdditionalFieldsSlice.SearchStatus); - 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); - const totalRecords = useAppSelector(AdditionalFieldsSlice.TotalRecords); + // additional field view table + const [data, setData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); + const [search, setSearch] = React.useState[]>([]); + const [sortField, setSortField] = React.useState("FieldName"); + const [ascending, setAscending] = React.useState(false); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + // additional field table pagination + 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 valueListGroupData = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus = useAppSelector(ValueListGroupSlice.Status); + // value list group for 'field type' dropdown in additional field search bar + const [valueListGroupData, setValueListGroupData] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); + // edit modal const [errors, setErrors] = React.useState([]); const [warnings, setWarnings] = React.useState([]); const [mode, setMode] = React.useState<'View' | 'Add' | 'Edit'>('View'); const [record, setRecord] = React.useState(emptyRecord); + const additionalFieldViewController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalFieldView`, "FieldName", true),[]) + const AdditionalFieldSearchField: Array> = [ { label: 'Name', key: 'FieldName', type: 'string', isPivotField: false }, { label: 'External Database', key: 'ExternalDB', type: 'string', isPivotField: false }, @@ -99,45 +105,67 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { } ]; + // fetch data according to filters, paging, and sorting information React.useEffect(() => { - if (parentID !== null) - dispatch(AdditionalFieldsSlice.Fetch()); - }, [parentID]); + setStatus('loading') + const h = additionalFieldViewController.PagedSearch(search, sortField, ascending, page); - React.useEffect(() => { - if (status === 'uninitiated' || status === 'changed') - dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField, ascending, page: currentPage })); - }, [status, search, sortField, ascending, currentPage]); + h.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setRecordsPerPage(d.RecordsPerPage); + setTotalRecords(d.TotalRecords); + if (d.NumberOfPages == 0) + setPage(0); + else if (page >= d.NumberOfPages) + setPage(d.NumberOfPages - 1); + setStatus('idle'); + }) + + h.fail(() => setStatus('error')) + return () => { + if (h != null && h.abort != null) + h.abort() + } + + }, [search, sortField, ascending, page, additionalFieldViewController, refreshTrigger]); + + // fetch value list groups React.useEffect(() => { - if (valueListGroupStatus == 'uninitiated' || valueListGroupStatus == 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); + setValueListGroupStatus('loading') - const setFilters = React.useCallback((filters: Search.IFilter[]) => { - dispatch(AdditionalFieldsSlice.PagedSearch({ sortField, ascending, filter: filters, page: currentPage })) - }, [sortField, ascending, currentPage]) + const h = new GenericController(`${homePath}api/ValueListGroup`, 'Name').Fetch(); - const setPage = React.useCallback((page: number) => { - dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField, ascending, page: page - 1})) - }, [search, sortField, ascending]) + h.done((d) => { + setValueListGroupData(d); + setValueListGroupStatus('idle'); + }) - useBoundPaging(currentPage, totalPages, setPage) + h.fail(() => setValueListGroupStatus('error')) + + return () => { + if (h != null && h.abort != null) + h.abort() + } + }, []); return (
+ +
CollumnList={AdditionalFieldSearchField} - SetFilter={setFilters} + SetFilter={setSearch} Direction={'left'} defaultCollumn={AdditionalFieldDefaultSearchField} Width={'50%'} StorageID="AdditionalFieldsFilter" Label={'Search'} ShowLoading={status == 'loading'} - ResultNote={status == 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' Additional Field(s)'} + ResultNote={status == 'error' ? 'Could not complete Search' : `Displaying Additional Field(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} >
  • @@ -163,8 +191,12 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { SortKey={sortField as string} Ascending={ascending} OnSort={(d) => { - if (d.colKey === null) return; - dispatch(AdditionalFieldsSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortField) + setAscending(a => !a) + else { + setAscending(true); + setSortField(d.colField); + } }} OnClick={(item) => { setRecord(item.row); setMode('Edit'); }} TableStyle={{ @@ -260,18 +292,18 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => {
    setPage(p - 1)} />
    { if (conf) - dispatch(AdditionalFieldsSlice.DBAction({ verb: mode === 'Add' ? 'POST' : 'PATCH', record })); + additionalFieldViewController.DBAction(mode === 'Add' ? 'POST' : 'PATCH', record).done(() => setRefreshTrigger(val => !val)); else if (isBtn) - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'DELETE', record })); + additionalFieldViewController.DBAction('DELETE', record).done(() => setRefreshTrigger(val => !val)); setMode('View'); }} ShowX={true} @@ -287,7 +319,8 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { DisableConfirm={errors.length > 0} ShowCancel={mode === 'Edit'} CancelText={'Delete'} - Show={mode === 'Add' || mode === 'Edit'} > + Show={mode === 'Add' || mode === 'Edit'} + >
  • 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..f7695a58bd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx @@ -21,14 +21,12 @@ // //****************************************************************************************************** -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 { useAppSelector } from '../hooks'; -import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import * as React from 'react'; +import { Application } from '@gpa-gemstone/application-typings'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; declare var homePath: string; @@ -66,50 +64,56 @@ interface ChannelDetail { //TODO: Move to Gemstone const AssetChannelWindow = (props: IProps) => { - const [assetChannels, setAssetChannels] = React.useState([]); - const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; - const [status, setStatus] = React.useState('idle'); + // asset channels table + const [assetChannels, setAssetChannels] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + // asset channels pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + + // fetch refresh assetChannels based on sort and page React.useEffect(() => { - let channelHandle = getChannels(); + setStatus('loading') + const channelHandle = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending, Searches: [] }) + }) - Promise.all([channelHandle]); + channelHandle.done( + (d) => { + setAssetChannels(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (d.NumberOfPages == 0) + setPage(0); + else if (page >= d.NumberOfPages) + setPage(d.NumberOfPages - 1); + setStatus('idle'); + }) + + channelHandle.fail(() => setStatus('error')); return () => { if (channelHandle != null && channelHandle.abort != null) channelHandle.abort(); } - }, [props.ID]); - - function getChannels(): JQuery.jqXHR { - setStatus('loading'); - return $.ajax( - { - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels`, - contentType: "application/json; charset=utf-A", - dataType: 'json', - cache: true, - async: true - } - ).done( - (d: Array) => { - const sortedChannels = sortData(sortField, ascending, d); - setAssetChannels(sortedChannels) - setStatus('idle'); - } - ).fail(() => setStatus('error')); - } - function sortData(key: keyof ChannelDetail, ascending: boolean, data: ChannelDetail[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }, [props.ID, ascending, page, sortField]); - if (status == 'error' || pStatus == 'error' || mtStatus == 'error') + if (status == 'error') return
    @@ -117,6 +121,14 @@ const AssetChannelWindow = (props: IProps) => {

    Channels:

    + +
    +
    +

    + {'Could not complete Search'} +

    +
    +
    @@ -127,7 +139,7 @@ const AssetChannelWindow = (props: IProps) => {
    - if (status == 'loading' || pStatus == 'loading' || mtStatus == 'loading') + if (status == 'loading') return
    @@ -135,6 +147,13 @@ const AssetChannelWindow = (props: IProps) => {

    Channels:

    +
    +
    +

    + {'Loading...'} +

    +
    +
    @@ -153,100 +172,115 @@ const AssetChannelWindow = (props: IProps) => {

    Channels:

    +
    +
    +

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

    +
    +
    - - TableClass="table table-hover" - Data={assetChannels} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(assetChannels, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetChannels(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(assetChannels, [d.colKey], ["asc"]); - setAssetChannels(ordered); - } - }} - TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Label - - - Key={'MeterName'} - AllowSort={true} - Field={'MeterName'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - Content={(row) => {row.item.MeterName}} - > Meter Name - - - Key={'AssetName'} - AllowSort={true} - Field={'AssetName'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - Content={(row) => (row.item.AssetID !== props.ID ? - {row.item.AssetName} : - row.item.AssetName - )} - > Asset Name - - - Key={'MeasurementType'} - AllowSort={true} - Field={'MeasurementType'} - HeaderStyle={{ width: '8%' }} - RowStyle={{ width: '8%' }} - > Type - - - Key={'Phase'} - AllowSort={true} - Field={'Phase'} - HeaderStyle={{ width: '8%' }} - RowStyle={{ width: '8%' }} - > Phase - - - Key={'AssetID'} - AllowSort={true} - Field={'AssetID'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={row => row.item.AssetID !== props.ID ? : <>} - > Shared Via Asset Connection - - - Key={'Description'} - AllowSort={true} - Field={'Description'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Description - - +
    +
    + + TableClass="table table-hover" + Data={assetChannels} + SortKey={sortField} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == sortField) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortField(d.colField); + } + }} + TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1 }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + > Label + + + Key={'MeterName'} + AllowSort={true} + Field={'MeterName'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + Content={(row) => {row.item.MeterName}} + > Meter Name + + + Key={'AssetName'} + AllowSort={true} + Field={'AssetName'} + HeaderStyle={{ width: '15%' }} + RowStyle={{ width: '15%' }} + Content={(row) => (row.item.AssetID !== props.ID ? + {row.item.AssetName} : + row.item.AssetName + )} + > Asset Name + + + Key={'MeasurementType'} + AllowSort={true} + Field={'MeasurementType'} + HeaderStyle={{ width: '8%' }} + RowStyle={{ width: '8%' }} + > Type + + + Key={'Phase'} + AllowSort={true} + Field={'Phase'} + HeaderStyle={{ width: '8%' }} + RowStyle={{ width: '8%' }} + > Phase + + + Key={'AssetID'} + AllowSort={true} + Field={'AssetID'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={row => row.item.AssetID !== props.ID ? : <>} + > Shared Via Asset Connection + + + Key={'Description'} + AllowSort={true} + Field={'Description'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Description + + +
    +
    +
    +
    + { setPage(p - 1) }} + Total={totalPages} + /> +
    +
    ); } -export default AssetChannelWindow -; \ No newline at end of file +export default AssetChannelWindow; \ No newline at end of file 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..b0f4b8fec3 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx @@ -21,16 +21,15 @@ // //****************************************************************************************************** -import * as React from 'react'; import _ from 'lodash'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import * as React from 'react'; import { useNavigate } from "react-router-dom"; -import { LoadingIcon, Modal, Search, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; +import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' -import { OpenXDA } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { AssetConnectionTypeSlice } from '../Store/Store'; +import { GenericController, LoadingIcon, Modal, Search, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { ToolTip } from '@gpa-gemstone/react-forms'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; +import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; interface AssetConnection { @@ -41,115 +40,170 @@ interface AssetConnection { AssetName: string } -function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number}): JSX.Element{ +interface IProps { + Name: string + ID: number + TypeID: number +} + +function AssetConnectionWindow(props: IProps): JSX.Element { let navigate = useNavigate(); - let dispatch = useAppDispatch(); + // asset connections table const [assetConnections, setAssetConnections] = React.useState>([]); + const [status, setStatus] = React.useState('idle'); + const [sortKey, setSortKey] = React.useState('AssetName'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); - const assetConnectionTypes = useAppSelector(AssetConnectionTypeSlice.SearchResults); - const [selectedAssetID, setSelectedAssetID] = React.useState(0); + // connection type selection + const [assetConnectionTypes, setAssetConnectionTypes] = React.useState([]); + const [assetConnectionTypeStatus, setAssetConnectionTypeStatus] = React.useState('uninitiated'); const [selectedTypeID, setSelectedtypeID] = React.useState(0); - const [localAssets, setLocalAssets] = React.useState>([]); - const [sortKey, setSortKey] = React.useState('AssetName'); - const [ascending, setAscending] = React.useState(true); + // connected asset selection + const [localAssets, setLocalAssets] = React.useState([]); + const [localAssetStatus, setLocalAssetStatus] = React.useState('uninitiated'); + const [selectedAssetID, setSelectedAssetID] = React.useState(0); + + // pagination info + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // new connection modal const [showModal, setShowModal] = React.useState(false); - const [status, setStatus] = React.useState<'idle' | 'loading' | 'error'>('idle'); - const actStatus = useAppSelector(AssetConnectionTypeSlice.SearchStatus); - const [trigger, setTrigger] = React.useState(0); + //db action status + const [actionStatus, setActionStatus] = React.useState('uninitiated'); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); + // fetch AssetConnections according to page and sort React.useEffect(() => { - let handle = getAssetConnections(); - return () => { if (handle != null || handle.abort != null) handle.abort();} - }, [props.ID, trigger]) + setStatus('loading'); - React.useEffect(() => { - if (props.ID > 0) { - let sqlString = `(SELECT AssetRelationshipTypeID FROM AssetRelationshipTypeAssetType LEFT JOIN Asset ON ` - sqlString = sqlString + `Asset.AssetTypeID <> ${props.TypeID} AND Asset.AssetTypeID = AssetRelationshipTypeAssetType.assetTypeID AND ` - sqlString = sqlString + `Asset.ID IN (SELECT AssetID FROM AssetLocation WHERE LocationID IN (Select LocationID FROM AssetLocation WHERE AssetID = ${props.ID})) ` - sqlString = sqlString + `GROUP BY AssetRelationshipTypeAssetType.AssetTypeID, AssetRelationshipTypeAssetType.AssetRelationshipTypeID ` - sqlString = sqlString + `HAVING COUNT(Asset.ID) > 0)` - const filter: Search.IFilter[] = [ - { FieldName: 'ID', SearchText: `(SELECT AssetRelationshipTypeID FROM AssetRelationshipTypeAssetType WHERE AssetTypeID = ${props.TypeID})`, Operator: 'IN', Type: 'query', IsPivotColumn: false }, - { - FieldName: 'ID', SearchText: sqlString, Operator: 'IN', Type: 'query', IsPivotColumn: false - } - ] - dispatch(AssetConnectionTypeSlice.DBSearch({ filter: filter })) + const handle = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) + }) + + handle.done((d) => { + setAssetConnections(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (d.NumberOfPages == 0) + setPage(0); + else if (page >= d.NumberOfPages) + setPage(d.NumberOfPages - 1); + setStatus('idle') + }) + + handle.fail(() => setStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); } - }, [props.TypeID]) + }, [props.ID, refreshTrigger, page, sortKey, ascending]) + // fetch AssetRelationshipTypes for Asset Connection Type select + React.useEffect(() => { + if (props.ID <= 0) return; + + setAssetConnectionTypeStatus('loading'); + + const sqlString = `(SELECT AssetRelationshipTypeID FROM AssetRelationshipTypeAssetType LEFT JOIN Asset ON + Asset.AssetTypeID <> ${props.TypeID} AND Asset.AssetTypeID = AssetRelationshipTypeAssetType.assetTypeID AND + Asset.ID IN (SELECT AssetID FROM AssetLocation WHERE LocationID IN (Select LocationID FROM AssetLocation WHERE AssetID = ${props.ID})) + GROUP BY AssetRelationshipTypeAssetType.AssetTypeID, AssetRelationshipTypeAssetType.AssetRelationshipTypeID + HAVING COUNT(Asset.ID) > 0)` + + const filter: Search.IFilter[] = [ + { + FieldName: 'ID', + SearchText: `(SELECT AssetRelationshipTypeID FROM AssetRelationshipTypeAssetType WHERE AssetTypeID = ${props.TypeID})`, + Operator: 'IN', + Type: 'query', + IsPivotColumn: false + }, + { + FieldName: 'ID', + SearchText: sqlString, + Operator: 'IN', + Type: 'query', + IsPivotColumn: false + } + ] + + const controller = new GenericController(`${homePath}api/OpenXDA/AssetConnectionType`, 'Name'); + + const handle = controller.DBSearch(filter); + + handle.done((d) => { + setAssetConnectionTypes(d); + setAssetConnectionTypeStatus('idle'); + }) + }, [props.TypeID]) + + // fetch local assets for Connecting Asset select React.useEffect(() => { if (selectedTypeID == 0) { setLocalAssets([]); return; } - let handle = getAssets(); + setLocalAssetStatus('loading'); + + 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 }, + { FieldName: 'AssetTypeID', SearchText: `(SELECT AssetTypeID FROM AssetRelationshipTypeAssetType WHERE AssetRelationshipTypeID = ${selectedTypeID} AND AssetTypeID <> ${props.TypeID})`, Operator: 'IN', Type: 'query', IsPivotColumn: false } + ] + + let handle = $.ajax({ + type: 'POST', + url: `${homePath}api/OpenXDA/Asset/SearchableList`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify({ Searches: filter, OrderBy: 'AssetName', Ascending: false }), + cache: false, + async: true + }) + + handle.done((d) => { + setLocalAssetStatus('idle') + setLocalAssets(JSON.parse(d) as OpenXDA.Types.Asset[]); + }) + + handle.fail(() => setLocalAssetStatus('error')); return () => { if (handle != null && handle.abort != null) handle.abort(); } }, [selectedTypeID]) + // reset selected type when assetConnectionTypes changes and selectedTypeID no longer exists. React.useEffect(() => { let index = assetConnectionTypes.findIndex(t => t.ID == selectedTypeID); - if (index == -1 && assetConnectionTypes.length> 0) + if (index == -1 && assetConnectionTypes.length > 0) setSelectedtypeID(assetConnectionTypes[0].ID) }, [assetConnectionTypes]) + // reset selected asset when localAssets changes and selectedAssetID no longer exists. React.useEffect(() => { let index = localAssets.findIndex(t => t.ID == selectedAssetID); if (index == -1 && localAssets.length > 0) setSelectedAssetID(localAssets[0].ID) }, [localAssets]) - - function getAssetConnections(): JQuery.jqXHR { - setStatus('loading'); - return $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }).done((d) => { - setStatus('idle') - const sortedConnections = sortData(sortKey, ascending, d); - setAssetConnections(sortedConnections) - }).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 }, - { FieldName: 'AssetTypeID', SearchText: `(SELECT AssetTypeID FROM AssetRelationshipTypeAssetType WHERE AssetRelationshipTypeID = ${selectedTypeID} AND AssetTypeID <> ${props.TypeID})`, Operator: 'IN', Type: 'query', IsPivotColumn: false } - ] - setStatus('loading'); - return $.ajax({ - type: 'POST', - url: `${homePath}api/OpenXDA/Asset/SearchableList`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - data: JSON.stringify({ Searches: filter, OrderBy: 'AssetName', Ascending: false }), - cache: false, - async: true - }).done((d) => { - setStatus('idle') - setLocalAssets(JSON.parse(d) as OpenXDA.Types.Asset[]); - }).fail(() => setStatus('error')); - } async function deleteAssetConnection(connection: AssetConnection) { - setStatus('loading') + setActionStatus('loading') return $.ajax({ type: "DELETE", url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnection/${connection.AssetID}`, @@ -158,31 +212,31 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number cache: true, async: true }).done(() => { - setTrigger(x => x + 1); - + setActionStatus('idle'); + setRefreshTrigger(val => !val); }).fail(() => { - setStatus('error') + setActionStatus('error'); }); } - function addConnection() { - setStatus('loading') + async function addConnection(selectedTypeID: number, selectedAssetID: number) { + setActionStatus('loading'); $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetConnection/Add`, contentType: "application/json; charset=utf-8", dataType: 'json', - data: JSON.stringify({ ID: 0, AssetRelationshipTypeID: selectedTypeID, ParentID: props.ID, ChildID: selectedAssetID}), + data: JSON.stringify({ ID: 0, AssetRelationshipTypeID: selectedTypeID, ParentID: props.ID, ChildID: selectedAssetID }), cache: false, async: true }).done(() => { - setTrigger(x => x + 1); + setActionStatus('idle'); + setRefreshTrigger(val => !val); }).fail(() => { - setStatus('error') + setActionStatus('error'); }); } - function handleSelect(item) { navigate(`${homePath}index.cshtml?name=Asset&AssetID=${item.row.AssetID}`); } @@ -193,7 +247,7 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number return true; } - if (status == 'error' || actStatus == 'error') + if (status === 'error' || actionStatus === "error") return
    @@ -201,6 +255,13 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number

    Assets:

    +
    +
    +

    + {'Could not complete Search'} +

    +
    +
    @@ -211,7 +272,7 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number
    - if (status == 'loading' || actStatus == 'loading' ) + if (status === 'loading' || actionStatus === 'loading') return
    @@ -219,6 +280,13 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number

    Assets:

    +
    +
    +

    + {'Loading...'} +

    +
    +
    @@ -229,7 +297,7 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number
    - const connectionsAvailable = !(assetConnectionTypes == undefined) && (assetConnectionTypes.length > 0); + const connectionsAvailable = (assetConnectionTypes != null && assetConnectionTypes.length > 0) && (localAssets != null && localAssets.length > 0); return (
    @@ -238,92 +306,124 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number

    Connections:

    +
    +
    +

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

    +
    +
    -
    - - TableClass="table table-hover" - Data={assetConnections} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey === "DeleteButton") - return; - - 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%' }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - OnClick={handleSelect} - Selected={(item) => false} - KeySelector={(item) => item.AssetID} - > - - Key={'AssetName'} - AllowSort={true} - Field={'AssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Asset Name - - - Key={'AssetKey'} - AllowSort={true} - Field={'AssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Asset Key - - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Relationship - - - Key={'DeleteButton'} - AllowSort={false} - HeaderStyle={{ width: '6%' }} - RowStyle={{ width: '6%' }} - Content={({ item }) => <> - - } - >

    - - +
    +
    + + TableClass="table table-hover" + Data={assetConnections} + SortKey={sortKey} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey === "DeleteButton") + return; + + if (d.colKey === sortKey) { + setAscending(!ascending); + } + else { + setAscending(true); + setSortKey(d.colKey as keyof AssetConnection); + } + }} + TableStyle={{ height: '100%' }} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + OnClick={handleSelect} + Selected={(item) => false} + KeySelector={(item) => item.AssetID} + > + + Key={'AssetName'} + AllowSort={true} + Field={'AssetName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Asset Name + + + Key={'AssetKey'} + AllowSort={true} + Field={'AssetKey'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Asset Key + + + Key={'Name'} + AllowSort={true} + Field={'Name'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Relationship + + + Key={'DeleteButton'} + AllowSort={false} + HeaderStyle={{ width: '6%' }} + RowStyle={{ width: '6%' }} + Content={({ item }) => <> + + } + >

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

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

    - { if (conf) - addConnection(); + addConnection(selectedTypeID, selectedAssetID); setShowModal(false); - }} ConfirmText={'Save'} DisableConfirm={!connectionsAvailable}> + }} + ConfirmText={'Save'} + DisableConfirm={!connectionsAvailable}> {connectionsAvailable ? <>
    @@ -335,26 +435,42 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number
    - + { + assetConnectionTypeStatus === 'idle' ? + : + <> + + + + } - + { + localAssetStatus === "idle" ? + : + <> + + + + } +
    :

    There are no Assets at this Substation that can be connected to {props.Name}.

    -
    } +
    + } - + ); } 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..f6a3f31392 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx @@ -21,80 +21,118 @@ // //****************************************************************************************************** -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 * as React from 'react'; import { useNavigate } from "react-router-dom"; +import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' +import { ToolTip } from '@gpa-gemstone/react-forms'; +import { LoadingIcon, LoadingScreen, Modal, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; -import { Modal } from '@gpa-gemstone/react-interactive'; -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(); + + // substation table const [locations, setLocations] = React.useState>([]); + const [locationStatus, setLocationStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); - const [allLocations, setAllLocations] = React.useState>([]); + const [hover, setHover] = React.useState(undefined); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); // also used by other substation select + + // substation pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // other substation select + const [allOtherLocations, setAllOtherLocations] = React.useState>([]); + const [otherLocationStatus, setOtherLocationStatus] = React.useState('uninitiated'); const [newLocation, setNewLocation] = React.useState(); - const [hover, setHover] = React.useState(undefined); const [showModal, setShowModal] = React.useState(false); const roles = useAppSelector(SelectRoles); + // database action + const [actionStatus, setActionStatus] = React.useState('uninitiated'); + function hasPermissions(): boolean { if (roles.indexOf('Administrator') < 0 && roles.indexOf('Engineer') < 0) return false; return true; } + // keep fresh data in the locaction table, sorted and paged React.useEffect(() => { - getData(); - }, [props.Asset]); + setLocationStatus('loading'); - function getData() { - getLocations(); - getAllOtherLocations(); - } - - function getLocations(): void { - $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations`, + const h = $.ajax({ + 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 }) + }) + + h.done(d => { + setLocations(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (d.NumberOfPages == 0) + setPage(0); + else if (page >= d.NumberOfPages) + setPage(d.NumberOfPages - 1); + setLocationStatus('idle'); }); - } - function sortData(key: keyof OpenXDA.Types.Location, ascending: boolean, data: OpenXDA.Types.Location[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + h.fail(() => setLocationStatus('error')) + + return () => { + if (h != null && h.abort != null) + h.abort(); + } + + }, [ascending, sortField, page, props.Asset.ID, refreshTrigger]) - function getAllOtherLocations(): void { - $.ajax({ + // keep fresh options in the Other Location Select + React.useEffect(() => { + setOtherLocationStatus('loading'); + const h = $.ajax({ type: "GET", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/OtherLocations`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, async: true - }).done(data => { + }) + + h.done(data => { let records = _.orderBy(data, ['Name'], ['asc']); - setAllLocations(records); + setAllOtherLocations(records); setNewLocation(records[0]); + setOtherLocationStatus('idle'); }); - } + + h.fail(() => setOtherLocationStatus('error')) + + return () => { + if (h != null && h.abort != null) + h.abort(); + } + }, [props.Asset.ID, refreshTrigger]); async function deleteLocation(location: OpenXDA.Types.Location) { + setActionStatus('loading'); return $.ajax({ type: "DELETE", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Location/${location.ID}`, @@ -103,14 +141,17 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element cache: true, async: true }).done((assets: Array) => { - getData(); + setRefreshTrigger(val => !val); + setActionStatus('idle'); }).fail((msg) => { + setActionStatus('error'); if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); } async function addLocation() { + setActionStatus('loading'); return $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Location/${newLocation.ID}`, @@ -119,8 +160,10 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element cache: true, async: true }).done(record => { - getData(); + setRefreshTrigger(val => !val); + setActionStatus('idle'); }).fail((msg) => { + setActionStatus('error'); if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); @@ -138,122 +181,160 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element

    Substations:

    +
    +
    +

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

    +
    +
    -
    - - 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 }) => <> - - } - >

    - - -
    + {locationStatus === 'idle' && (actionStatus === 'idle' || actionStatus === 'uninitiated') ? +
    +
    + + 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.

    - { if (conf) { addLocation(); } setShowModal(false) }} - ConfirmText={'Save'} DisableConfirm={allLocations.length === 0}> - -
    - - -
    + ConfirmText={'Save'} + DisableConfirm={allOtherLocations.length === 0}> + +
    + + {otherLocationStatus === 'idle' ? + : + <> + + + + } +
    - + ); } 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..5c6917d691 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx @@ -21,15 +21,15 @@ // //****************************************************************************************************** -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 * as React from 'react'; import { useNavigate } from "react-router-dom"; +import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; -import { ByMeterSlice } from '../Store/Store'; -import { Search } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; +import { Search } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; +import { ByMeterSlice } from '../Store/Store'; import { useAppDispatch, useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; @@ -38,14 +38,30 @@ declare var homePath: string; function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ let navigate = useNavigate(); const dispatch = useAppDispatch(); + + // meter table const [meters, setMeters] = React.useState>([]); + const [status, setStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + + // meter pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // db actions + const [actionStatus, setActionStatus] = React.useState('idle') + + // selects const [showAdd, setShowAdd] = React.useState(false); const allMeters = useAppSelector(ByMeterSlice.Data); const mStatus = useAppSelector(ByMeterSlice.Status); const mParentID = useAppSelector(ByMeterSlice.ParentID); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -54,29 +70,40 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ }, [mStatus, mParentID]); + // fetch asset meters paged and sorted React.useEffect(() => { - getMeters(); - }, [props.Asset]); + setStatus('loading'); - function getMeters(): void { - $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters`, + const h = $.ajax({ + 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 }) + }) + + h.done(d => { + setMeters(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + + if (d.NumberOfPages == 0) + setPage(0); + else if (page >= d.NumberOfPages) + setPage(d.NumberOfPages - 1); + + setStatus('idle'); }); - } - function sortData(key: keyof OpenXDA.Types.Meter, ascending: boolean, data: OpenXDA.Types.Meter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + h.fail(() => setStatus('error')) + }, [ascending, sortField, page, refreshTrigger, props.Asset.ID]) + + async function addMeter(meterID: number) { + setActionStatus('loading'); - function addMeter(meterID: number) { return $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meter/${meterID}`, @@ -85,8 +112,10 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ cache: true, async: true }).done(record => { - getMeters(); + setRefreshTrigger(val => !val); + setActionStatus('idle'); }).fail((msg) => { + setActionStatus('error'); if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); @@ -112,7 +141,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,11 +170,12 @@ 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(); }; } async function deleteMeter(meter: OpenXDA.Types.Meter) { + setActionStatus('loading'); return $.ajax({ type: "DELETE", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meter/${meter.ID}`, @@ -154,8 +184,10 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ cache: true, async: true }).done(() => { - getMeters(); + setRefreshTrigger(val => !val); + setActionStatus('idle'); }).fail((msg) => { + setActionStatus('error'); if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); @@ -175,8 +207,18 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{

    Meters:

    +
    +
    +

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

    +
    +
    -
    +
    +
    TableClass="table table-hover" Data={meters} @@ -185,14 +227,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 +272,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..54240fe1db 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx @@ -23,77 +23,89 @@ import * as React from 'react'; -import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; -import { AssetGroupSlice, AssetTypeSlice } from '../Store/Store'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { Warning } from '@gpa-gemstone/react-interactive'; +import * as _ from 'lodash'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { Warning } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; +import { useAppSelector } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; -function AssetAssetGroupWindow(props: { AssetGroupID: number}) { +interface IProps { + AssetGroupID: number +} + +function AssetAssetGroupWindow(props: IProps) { let navigate = useNavigate(); - const [assetList, setAssetList] = React.useState>([]); + + // asset table + const [assets, setAssets] = React.useState([]); + const [assetStatus, setAssetStatus] = React.useState('uninitiated'); const [sortKey, setSortKey] = React.useState('AssetName'); const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + + // paging + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // add/remove asset modal const [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeAsset, setRemoveAsset] = React.useState(-1); - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - const dispatch = useAppDispatch(); + // db actions + const [addStatus, setAddStatus] = React.useState('idle'); + const [removeStatus, setRemoveStatus] = React.useState('idle'); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]); - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); + if (props.AssetGroupID == null) return - function getData() { - if (props.AssetGroupID == null) - return () => { }; + setAssetStatus('loading'); let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) }) - handle.done((data: Array) => { - const sortedData = sortData(sortKey, ascending, data); - setAssetList(sortedData); + handle.done((d) => { + setAssets(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setAssetStatus('idle') }); - - return function cleanup() { - if (handle.abort != null) + + handle.fail(() => setAssetStatus('error')) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); } - } + }, [ascending, sortKey, page, props.AssetGroupID, refreshTrigger]) - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedAsset[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function saveItems(items: SystemCenter.Types.DetailedAsset[]) { + async function saveItems(items: SystemCenter.Types.DetailedAsset[]) { + setAddStatus('loading'); - let handle = $.ajax({ + const handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddAssets`, contentType: "application/json; charset=utf-8", @@ -103,13 +115,23 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(d => { + setAddStatus('idle') + setRefreshTrigger(val => !val); + }) + handle.fail(() => setAddStatus('error')); + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } } - function removeItem(id: number) { - let handle = $.ajax({ + async function removeItem(id: number) { + setRemoveStatus('loading'); + + const handle = $.ajax({ type: "GET", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/RemoveAsset/${id}`, contentType: "application/json; charset=utf-8", @@ -118,7 +140,17 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(d => { + setRemoveStatus('idle') + setRefreshTrigger(val => !val); + }) + + handle.fail(() => setRemoveStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } } function hasPermissions(): boolean { @@ -133,108 +165,129 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
    -
    -
    -
    -

    Transmission Assets:

    +
    +
    +
    +
    +

    Transmission Assets:

    +
    +
    +
    +
    +

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

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

    - -
    -
    -
    -
    - +
    +
    + + TableClass="table table-hover" + Data={assets} + 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.

    +
    -
    - { setShowAdd(false); if (!conf) return - saveItems(selected.filter(items => assetList.findIndex(g => g.ID == items.ID) < 0)) + saveItems(selected.filter(items => assets.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..d5146285c0 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx @@ -24,9 +24,9 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA } from '@gpa-gemstone/application-typings'; +import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { AssetGroupSlice } from '../Store/Store'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { Search, Warning } from '@gpa-gemstone/react-interactive'; @@ -38,17 +38,30 @@ 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>([]); + + // asset group table + const [groups, setGroups] = React.useState([]); + const [groupStatus, setGroupStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + + // asset group pagination + 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 [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeGroup, setRemoveGroup] = React.useState(-1); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + // db actions + const [addStatus, setAddStatus] = React.useState('idle'); + const [removeStatus, setRemoveStatus] = React.useState('idle'); + const roles = useAppSelector(SelectRoles); const noSameFilter: Search.IFilter = @@ -61,37 +74,35 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { }; React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]); - - function getData() { - if (props.AssetGroupID == null) - return () => { }; + if (props.AssetGroupID == null) return - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups`, + const handle = $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) }); - handle.done((data: Array) => { - const sortedData = sortData(sortField, ascending, data); - setGroupList(sortedData); + handle.done((d) => { + setGroups(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setGroupStatus('idle') }); - + + handle.fail(() => setGroupStatus('error')); + return function cleanup() { if (handle.abort != null) handle.abort(); } - } - - function sortData(key: string, ascending: boolean, data: OpenXDA.Types.AssetGroup[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }, [ascending, sortField, page, props.AssetGroupID, refreshTrigger]) function getEnum(setOptions, field) { let handle = null; @@ -109,12 +120,15 @@ 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(); } } - function removeItem(id: number) { - let handle = $.ajax({ + async function removeItem(id: number) { + + setRemoveStatus('loading'); + + const handle = $.ajax({ type: "GET", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/RemoveGroup/${id}`, contentType: "application/json; charset=utf-8", @@ -123,12 +137,24 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(d => { + setRefreshTrigger(val => !val) + setRemoveStatus('idle'); + }) + + handle.fail(() => setRemoveStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } } - function saveItems(items: OpenXDA.Types.AssetGroup[]) { - - let handle = $.ajax({ + async function saveItems(items: OpenXDA.Types.AssetGroup[]) { + + setAddStatus('loading'); + + const handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddAssetGroups`, contentType: "application/json; charset=utf-8", @@ -138,9 +164,17 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(d => { + setRefreshTrigger(val => !val) + setAddStatus('idle'); + }) + handle.fail(() => setAddStatus('error')); + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } } function hasPermissions(): boolean { @@ -151,92 +185,104 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
    -
    -
    -
    -

    Asset Groups in Asset Group:

    +
    +
    +
    +
    +

    Asset Groups in Asset Group:

    +
    +
    +
    +
    +

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

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

    - - +
    +
    + + TableClass="table table-hover" + Data={groups} + 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} + /> +
    +
    - -
    -
    +
    @@ -246,11 +292,11 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { { setShowAdd(false) if (!conf) return - saveItems(selected.filter(items => groupList.findIndex(g => g.ID == items.ID) < 0)) + saveItems(selected.filter(items => groups.findIndex(g => g.ID == items.ID) < 0)) }} Show={showAdd} Type={'multiple'} @@ -270,7 +316,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..83d6120567 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx @@ -22,21 +22,20 @@ //****************************************************************************************************** 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'; declare var homePath: string; - interface extendedAssetGroup extends OpenXDA.Types.AssetGroup { MeterList: Array, AssetList: Array, UserList: Array, AssetGroupList: Array } const emptyAssetGroup: extendedAssetGroup = { ID: -1, Name: '', DisplayDashboard: true, AssetGroups: 0, Meters: 0, Assets: 0, Users: 0, MeterList: [], AssetList: [], UserList: [], AssetGroupList: [], DisplayEmail: false }; @@ -47,37 +46,84 @@ 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 allAssetGroups = useAppSelector(AssetGroupSlice.Data); - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); + const [data, setData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); + const [searchFilters, setSearchFilters] = React.useState[]>([]); + const [sortKey, setSortKey] = React.useState('Name'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const [showFilter, setFilter] = React.useState<('None' | 'Meter' | 'Asset' | 'Asset Group' | 'Station')>('None'); + 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 [allAssetGroups, setAllAssetGroups] = React.useState([]); + const [allAssetGroupStatus, setAllAssetGroupStatus] = React.useState('uninitiated'); + const [assetTypes, setAssetTypes] = React.useState([]); + const [assetTypeStatus, setAssetTypeStatus] = React.useState('uninitiated'); + 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 assetGroupController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/AssetGroup`, "Name", true),[]) + + // fetch asset groups for table, paged, filtered, and sorted React.useEffect(() => { - if (status == 'changed' || status == 'uninitiated') - dispatch(AssetGroupSlice.Fetch()); - }, [status]); + 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); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setStatus('idle'); + }) + h.fail(() => setStatus('error')) + + return () => { + if (h != null && h.abort != null) h.abort(); + } + }, [sortKey, ascending, page, searchFilters, refreshTrigger]) + // fetch all asset groups for validation React.useEffect(() => { - if (searchStatus == 'changed' || searchStatus == 'uninitiated') - dispatch(AssetGroupSlice.DBSearch({ filter: searchFields })); - }, [searchStatus]); + setAllAssetGroupStatus('loading'); + const handle = assetGroupController.Fetch(); + handle.done((d) => { + setAllAssetGroups(d); + setAllAssetGroupStatus('idle'); + }) + handle.fail(() => setAllAssetGroupStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, []) + // fetch all asset types React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); + setAssetTypeStatus('loading'); + + const handle = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name').Fetch(); + + handle.done((d) => { + setAssetTypes(d); + setAssetTypeStatus('idle'); + }) + handle.fail(() => setAssetTypeStatus('error')) + + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, []); React.useEffect(() => { let e = []; @@ -113,36 +159,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(); }; } @@ -188,6 +205,7 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { Promise.all([handle1,handle2, handle3]).then((x) => { sessionStorage.clear(); + setRefreshTrigger(val => !val); dispatch(AssetGroupSlice.SetChanged()) navigate(`${homePath}index.cshtml?name=AssetGroup&AssetGroupID=${d.ID}`); }, (msg) => { @@ -198,7 +216,6 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) }); - } function handleSelect(item) { @@ -215,7 +232,7 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { function getEnum(setOptions, field) { if (field.key == 'AssetType' && field.type == 'enum') { - setOptions(assetType.map((t) => ({ Value: t.Name, Label: t.Name }))) + setOptions(assetTypes.map((t) => ({ Value: t.Name, Label: t.Name }))) return () => { } } @@ -233,26 +250,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 +295,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 +355,15 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => {
    +
    +
    + {setPage(page - 1)} } + /> +
    +
    >([]); + + // meter table + const [meters, setMeters] = React.useState([]); + const [meterStatus, setMeterStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + // meter pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // db actions + const [removeStatus, setRemoveStatus] = React.useState('idle'); + const [addStatus, setAddStatus] = React.useState('idle'); + const [showAdd, setShowAdd] = React.useState(false); - const [counter, setCounter] = React.useState(0); const [removeMeter, setRemoveMeter] = React.useState(-1); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); React.useEffect(() => { - dispatch(AssetGroupSlice.SetChanged()); - return getData(); - }, [props.AssetGroupID, counter]) + if (props.AssetGroupID == null) return - function getData() { - if (props.AssetGroupID == null) - return () => { }; + setMeterStatus('loading'); - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Meters`, + const handle = $.ajax({ + 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) => { + setMeters(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setMeterStatus('idle'); }); - + + handle.fail(() => setMeterStatus('error')) + return function cleanup() { if (handle.abort != null) handle.abort(); } - } - - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedMeter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + }, [ascending, sortField, page, props.AssetGroupID, refreshTrigger]) function getEnum(setOptions, field) { let handle = null; @@ -101,7 +114,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,13 +143,15 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { }); return () => { - if (handle != null && handle.abort == null) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; } function saveItems(items: SystemCenter.Types.DetailedMeter[]) { - let handle = $.ajax({ + setAddStatus('loading'); + + const handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddMeters`, contentType: "application/json; charset=utf-8", @@ -146,11 +161,24 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { data: JSON.stringify(items.map(e => e.ID)) }); - handle.done(d => setCounter(x => x + 1)) + handle.done(() => { + setRefreshTrigger(val => !val); + setAddStatus('idle'); + }) + + handle.fail(() => setAddStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } } function removeItem(id: number) { - let handle = $.ajax({ + + setRemoveStatus('loading'); + + const handle = $.ajax({ type: "GET", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/RemoveMeter/${id}`, contentType: "application/json; charset=utf-8", @@ -159,7 +187,17 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { async: true }); - handle.done(d => setCounter(x => x + 1)) + handle.done(d => { + setRefreshTrigger(val => !val); + setRemoveStatus('idle'); + }) + + handle.fail(() => setRemoveStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } } function hasPermissions(): boolean { @@ -174,98 +212,115 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
    -
    -
    -
    -

    Meters in Asset Group:

    +
    +
    +
    +
    +

    Meters in Asset Group:

    +
    +
    +
    +
    +

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

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

    - - +
    +
    + + TableClass="table table-hover" + Data={meters} + 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.

    -
    +
    { setShowAdd(false) if (!conf) return - saveItems(selected.filter(items => meterList.findIndex(g => g.ID == items.ID) < 0)) + saveItems(selected.filter(items => meters.findIndex(g => g.ID == items.ID) < 0)) }} Show={showAdd} Type={'multiple'} @@ -286,7 +341,7 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { 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 (
    +
    +
    +

    + {'Could not complete Search'} +

    +
    +
    @@ -104,6 +133,13 @@ const CustomerAssetWindow = (props: IProps) => {

    Assigned Assets:

    +
    +
    +

    + {'Loading...'} +

    +
    +
    @@ -115,90 +151,115 @@ const CustomerAssetWindow = (props: IProps) => {
    return <> -
    -
    -
    -
    -

    Assigned Assets:

    +
    +
    +
    +
    +

    Assigned Assets:

    +
    +
    +
    +
    +

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

    +
    -
    -
    -
    - - 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..54c7aa6862 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'; @@ -37,39 +37,39 @@ 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 [showAdd, setShowAdd] = React.useState(false); - const sortField = useAppSelector(CustomerMeterSlice.SortField); - const ascending = useAppSelector(CustomerMeterSlice.Ascending); + const [data, setData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated') + const [sortField, setSortField] = React.useState('MeterName') + const [ascending, setAscending] = React.useState(true) + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + + 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 [showAdd, setShowAdd] = React.useState(false); const [removeRecord, setRemoveRecord] = React.useState(null); - 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) + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + 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 +87,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 +116,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 +133,8 @@ const CustomerMeterWindow = (props: IProps) => { MeterName: meter.Name, MeterID: meter.ID } - })) - }) + ) + })).then(() => setRefreshTrigger((val) => !val)) } function hasPermissions(): boolean { @@ -151,6 +151,13 @@ const CustomerMeterWindow = (props: IProps) => {

    Assigned Meters:

    +
    +
    +

    + {'Could not complete Search'} +

    +
    +
    @@ -169,6 +176,13 @@ const CustomerMeterWindow = (props: IProps) => {

    Assigned Meters:

    +
    +
    +

    + {'Loading...'} +

    +
    +
    @@ -180,84 +194,107 @@ const CustomerMeterWindow = (props: IProps) => {
    return <> -
    -
    -
    -
    -

    Assigned Meters:

    +
    +
    +
    +
    +

    Assigned Meters:

    +
    +
    +
    +
    +

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

    +
    -
    -
    -
    - - 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..e916f53170 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx @@ -25,11 +25,11 @@ import * as React from 'react'; import * as _ from 'lodash'; import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import { useAppSelector, useAppDispatch } from '../hooks'; -import { AdditionalFieldsSlice, ValueListGroupSlice } from '../Store/Store'; +import { AdditionalFieldsSlice } 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,19 +50,25 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const dispatch = useAppDispatch(); - const data = useAppSelector(AdditionalFieldsSlice.Data); - const status = useAppSelector(AdditionalFieldsSlice.Status); - const searchData = useAppSelector(AdditionalFieldsSlice.SearchResults); - const searchStatus = useAppSelector(AdditionalFieldsSlice.SearchStatus); - - const valueListGroupData = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus = useAppSelector(ValueListGroupSlice.Status); - + // External DB Table Fields Table const [fieldsInTable, setFieldsInTable] = React.useState([]); - const parentID = React.useRef(-1); const [tableStatus, setTableStatus] = React.useState('uninitiated'); + const [sortKey, setSortKey] = React.useState('FieldName'); const [asc, setAsc] = React.useState(true); - const [sortKey, setSortKey] = React.useState('FieldName'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + // External DB Table Fields Pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // silce needed for the selection popup + const searchData = useAppSelector(AdditionalFieldsSlice.SearchResults); + const searchStatus = useAppSelector(AdditionalFieldsSlice.SearchStatus); + + const [valueListGroups, setValueListGroups] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); const [record, setRecord] = React.useState(emptyRecord); const [showRemove, setShowRemove] = React.useState(false); @@ -73,71 +79,67 @@ 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 additionalFieldsController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalFieldView`, "FieldName", true),[]) + // fetch AdditionalFields associated with ExternalDBTable, sorted and paged, for table React.useEffect(() => { - sortData(fieldsInTable); - }, [sortKey, asc]); + setTableStatus('loading'); + 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); + setTotalRecords(results.TotalRecords); + setRecordsPerPage(results.RecordsPerPage); + if (page >= results.NumberOfPages) + setPage(Math.max(results.NumberOfPages - 1, 0)); + setTableStatus('idle'); + }); + handle.fail(() => { + setTableStatus('error'); + }); + return () => { + if (handle != null && handle.abort != null) handle.abort(); + }; + }, [sortKey, asc, props.ID, page, refreshTrigger]); + // fetch ValueListGroups for Additional Field selector React.useEffect(() => { - if (status === 'uninitiated' || status === 'changed') - dispatch(AdditionalFieldsSlice.Fetch()); - }, [status]); + setValueListGroupStatus('loading'); + const handle = new GenericController(`${homePath}api/ValueListGroup`, 'Name').Fetch(); - React.useEffect(() => { - if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); + handle.done((d) => { + setValueListGroups(d); + setValueListGroupStatus('idle'); + }) - /* 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]); - */ + handle.fail(() => setValueListGroupStatus('error')); - const sortData = React.useCallback((sortData: SystemCenter.Types.AdditionalFieldView[]) => { - setFieldsInTable(_.orderBy(sortData, [sortKey], [(asc ? "asc" : "desc")])); - }, [setFieldsInTable, sortKey, asc]); + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, []); 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 ( @@ -150,6 +152,16 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu

    Fields:

  • +
    +
    +

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

    +
    +
    +
    @@ -166,7 +178,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 +216,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 +225,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 +234,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 +243,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 +268,15 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu }
    +
    +
    + { setPage(page - 1) }} + Total={totalPages} + /> +
    +
    @@ -280,14 +301,13 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu ShowCancel={false} ConfirmText={'Close'} > - - + + { if (c) { overWriteFields.forEach((f) => AssociateField(f)); - setTableStatus('changed'); } setOverWriteFields([]); }} Message={`Associating the selected ${overWriteFields.length} field(s) with ${props.TableName ?? 'this table'} will overwrite an existing connection in ${overWriteFields.filter(f => f.ExternalDBTableID != null).length} field(s). This cannot be undone.`} /> @@ -310,8 +330,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) + }) + } } }} > @@ -345,7 +373,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu { Value: 'string', Label: 'string' }, { Value: 'integer', Label: 'integer' }, { Value: 'number', Label: 'number' } - ].concat(valueListGroupData.map(x => { return { Value: x.Name, Label: x.Name } })) + ].concat(valueListGroups.map(x => { return { Value: x.Name, Label: x.Name } })) } ]} SetFilter={(flds) => dispatch(AdditionalFieldsSlice.DBSearch({ filter: flds }))} @@ -358,31 +386,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..77c650111e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx @@ -22,36 +22,72 @@ //****************************************************************************************************** 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 { ExternalDBTablesSlice } from '../Store/Store'; -import ExternalDBTableForm from './ExternalDBTableForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import * as _ from 'lodash'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { Modal, Warning } from '@gpa-gemstone/react-interactive'; +import { GenericController, Modal, Search, Warning } from '@gpa-gemstone/react-interactive'; +import { Column, Paging, Table } from '@gpa-gemstone/react-table'; +import ExternalDBTableForm from './ExternalDBTableForm'; + +const emptyRecord: SystemCenter.Types.extDBTables = { ID: 0, TableName: '', ExtDBID: 0, Query: '' }; + +interface IProps { + ID: number +} -export default function ExternalDBTables(props: { ID: number }) { +const ExternalDBTables = (props: IProps) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const data = useAppSelector(ExternalDBTablesSlice.Data); - const sortKey = useAppSelector(ExternalDBTablesSlice.SortField); - const asc = useAppSelector(ExternalDBTablesSlice.Ascending); - const status = useAppSelector(ExternalDBTablesSlice.Status); - const parentID = useAppSelector(ExternalDBTablesSlice.ParentID); + // extDBTable table + const [extDBTables, setExtDBTables] = React.useState([]); + const [extDBTablesStatus, setExtDBTablesStatus] = React.useState('uninitiated'); + const [sortField, setSortField] = React.useState('ID'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + // extDBTable pagination + 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 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([]); + // db action statuses + const [deleteStatus, setDeleteStatus] = React.useState('idle'); + const [createStatus, setCreateStatus] = React.useState('idle'); + const [editStatus, setEditStatus] = React.useState('idle'); + + const filters: Search.IFilter[] = React.useMemo(() => [{ SearchText: props.ID.toString(), FieldName: 'ExtDBID', Operator: "=", IsPivotColumn: false, Type: 'string' }], [props.ID]) + const externalDBTableController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/extDBTables`, "TableName", true),[]) + + + // fetch data for External DB Tables table, filtered, sorted, and paged. React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed' || parentID != props.ID) - dispatch(ExternalDBTablesSlice.Fetch(props.ID)); - }, [status, parentID, props.ID]); + setExtDBTablesStatus('loading'); + const handle = externalDBTableController.PagedSearch(filters, sortField, ascending, page); + + handle.done((d) => { + setExtDBTables(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setExtDBTablesStatus('idle'); + }) + + handle.fail(() => setExtDBTablesStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, [filters, sortField, ascending, page, refreshTrigger]); React.useEffect(() => { let e = []; @@ -65,8 +101,12 @@ export default function ExternalDBTables(props: { ID: number }) { setErrors(e); }, [record]); - function Delete() { - dispatch(ExternalDBTablesSlice.DBAction({ verb: 'DELETE', record: { ...record } })); + async function Delete() { + setDeleteStatus('loading'); + externalDBTableController.DBAction('DELETE', record).done(() => { + setDeleteStatus('idle'); + setRefreshTrigger(val => !val); + }).fail(() => setDeleteStatus('error')); setShowWarning(false); setRecord(emptyRecord); } @@ -85,6 +125,15 @@ export default function ExternalDBTables(props: { ID: number }) {

    Tables:

    +
    +
    +

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

    +
    +
    @@ -92,12 +141,16 @@ export default function ExternalDBTables(props: { ID: number }) {
    TableClass="table table-hover" - Data={data} - SortKey={sortKey.toString()} - Ascending={asc} + Data={extDBTables} + SortKey={sortField} + Ascending={ascending} OnSort={(d) => { - if (d.colKey == 'btns') return; - dispatch(ExternalDBTablesSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey == sortField) { + setAscending(a => !a); + } + else { + setSortField(d.colKey as keyof SystemCenter.Types.DetailedExtDBTables); + } }} OnClick={handleSelect} TableStyle={{ padding: 0, width: '100%', height: '100%', tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' }} @@ -141,6 +194,15 @@ export default function ExternalDBTables(props: { ID: number }) {
    +
    +
    + setPage(page - 1)} + Current={page + 1} + Total={totalPages} + /> +
    +
    @@ -161,9 +223,9 @@ export default function ExternalDBTables(props: { ID: number }) { ShowX={true} CallBack={(conf) => { setShowModal(false); if (conf && record.ID > 0) - dispatch(ExternalDBTablesSlice.DBAction({ verb: 'PATCH', record })); + externalDBTableController.DBAction('PATCH', record).done(() => { setEditStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setEditStatus('error')); else if (conf && record.ID == 0) - dispatch(ExternalDBTablesSlice.DBAction({ verb: 'POST', record })); + externalDBTableController.DBAction('POST', record).done(() => { setCreateStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setCreateStatus('error')); }} > @@ -175,4 +237,6 @@ export default function ExternalDBTables(props: { ID: number }) { ); -} \ No newline at end of file +} + +export default ExternalDBTables; \ No newline at end of file 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..b5897a0a5f 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,37 @@ 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); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + 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 +140,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 +161,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 +185,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 +214,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 +259,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 +308,7 @@ const ByLocation: Application.Types.iByComponent = (props) => {
    - setPage(p - 1)} /> + setPage(p - 1)} />
    @@ -277,7 +316,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..46e68e2a4b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx @@ -24,73 +24,109 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA } from '@gpa-gemstone/application-typings'; +import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { 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 [imageStatus, setImageStatus] = React.useState('uninitiated'); + 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(() => { - let handle = getImages(); - handle.done(i => {setImages(i)}); + setImageStatus('loading'); + let handle = getImages(props.Location.ID, page); + handle.done(d => { + setImages(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setImageStatus('idle'); + }); + handle.fail(() => setImageStatus('error')); return () => { - if (handle.abort != undefined) handle.abort(); + if (handle != null && handle.abort != null) handle.abort(); }; - }, [props.Location.ID]); - - function getImages(): JQuery.jqXHR { - return $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }) - } + }, [props.Location.ID, page]); return (
    -
    -

    Substation Images:

    +
    +
    +

    Substation Images:

    +
    +
    +
    +
    +

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

    +
    {images.length > 0 - ? (images.map((img, i) => ( -
    -
    -
    - {img} setImage(img)} - key={i} - style={{cursor: 'pointer', objectFit: 'contain'}} /> -
    -
    -
    -
    -
    {img}
    -
    -
    -
    ))) - :
    No images to display.
    + ? (images.map((img, i) => ( +
    +
    +
    + {img} setImage(img)} + key={i} + style={{ cursor: 'pointer', objectFit: 'contain' }} /> +
    +
    +
    +
    +
    {img}
    +
    +
    +
    ))) + :
    No images to display.
    }
    +
    + { setPage(page - 1) }} + Current={page + 1} + Total={totalPages} + > + +
    0} ShowCancel={false} ShowX={true} ShowConfirm={false} Title={image} - CallBack={() => setImage('') }> + CallBack={() => setImage('')}>
    ); } -export default LocationImagesWindow; \ No newline at end of file +export default LocationImagesWindow; + + +function getImages(locationID: number, page: number): JQuery.jqXHR { + return $.ajax({ + type: "GET", + url: `${homePath}api/OpenXDA/Location/${locationID}/Images/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) +} 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..1018b192d0 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,41 @@ 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); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + 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 +77,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 +86,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 +116,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 +164,7 @@ const ByMeter: Application.Types.iByComponent = (props) => {
  • -
    +
    @@ -218,7 +239,7 @@ const ByMeter: Application.Types.iByComponent = (props) => {
    - setPage(p - 1)} /> + setPage(p - 1)} />
    @@ -233,4 +254,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/Node/Node.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Node/Node.tsx index 88d109a165..7b3200c7f0 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Node/Node.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Node/Node.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import NodeForm from './NodeForm'; import NodeSettings from './NodeSettings' -import { TabSelector, Warning, GenericController } from '@gpa-gemstone/react-interactive'; +import { TabSelector, Warning, GenericController, LoadingScreen, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { Application } from '@gpa-gemstone/application-typings' import { SystemCenter as SC } from '../global' import { useNavigate } from 'react-router-dom'; @@ -58,9 +58,9 @@ export default function Node(props: IProps) { } function deleteNode() { - const controller = new GenericController(`${homePath}api/openXDA/Node`, 'ID') - controller.DBAction('DELETE', convertToXDANode(node, nodeTypes, appHosts)) - navigate(`${homePath}index.cshtml?name=TaskRunners`) + setStatus('loading'); + const controller = new GenericController(`${homePath}api/openXDA/Node`, 'ID'); + controller.DBAction('DELETE', convertToXDANode(node, nodeTypes, appHosts)).then(() => { navigate(`${homePath}index.cshtml?name=TaskRunners`); setStatus('idle'); }) } React.useEffect(() => { @@ -121,6 +121,8 @@ export default function Node(props: IProps) { return (
    + +

    {node != null ? node.Name : ''}

    @@ -134,7 +136,7 @@ export default function Node(props: IProps) { setTab(t)} Tabs={Tabs} /> {tab === 'info' ? setRefreshTrigger((val) => !val)} NodeTypes={nodeTypes} AppHosts={appHosts} /> : null} {tab === 'settings' ? : null} - { if (c) deleteNode(); setShowWarning(false) }} /> + { if (c) deleteNode(); setShowWarning(false) }} />
    ) } 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..5b687f0c9b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx @@ -24,10 +24,10 @@ 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'; +import { GenericController, LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAAsset, RemoteAssetForm } from './RemoteAssetForm'; @@ -36,62 +36,97 @@ import { SelectRoles } from '../Store/UserSettings'; interface IProps { ID: number } - const RemoteAssetTab = (props: IProps) => { - // Display Remote Assets Consts - const [sortKey, setSortKey] = React.useState('LocalAssetName'); - const [ascending, setAscending] = React.useState(true); + const dispatch = useAppDispatch(); - const remoteAssetStatus = useAppSelector(RemoteXDAAssetSlice.Status); - const searchResults = useAppSelector(RemoteXDAAssetSlice.SearchResults); - const searchState = useAppSelector(RemoteXDAAssetSlice.SearchStatus); - const searchFilters: Search.IFilter[] = - [{ - FieldName: 'RemoteXDAInstanceID', - SearchText: props.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }] + // Remote Asset Table + const [remoteAssets, setRemoteAssets] = React.useState([]); + const [remoteAssetStatus, setRemoteAssetStatus] = React.useState('uninitiated'); + const [ascending, setAscending] = React.useState(true); + const [sortKey, setSortKey] = React.useState('LocalAssetName'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - // Edit and Delete Form Consts + // Remote Asset Pagination + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // Edit and Delete Form const [newInstErrors, setNewInstErrors] = React.useState([]); const [remoteAsset, setRemoteAsset] = React.useState(BlankRemoteXDAAsset); const [selectedAsset, setSelectedAsset] = React.useState(BlankRemoteXDAAsset); const [showEdit, setShowEdit] = React.useState<(boolean)>(false); const [showDelete, setShowDelete] = React.useState<(boolean)>(false); - // Add New Asset Consts - const assetStatus = useAppSelector(ByAssetSlice.Status) as Application.Types.Status; - const [assetList, setAssetList] = React.useState>([]); + // Add New Asset + const [assetStatus, setAssetStatus] = React.useState('uninitiated'); + const [assets, setAssets] = React.useState>([]); const [showAddAssets, setShowAddAssets] = React.useState<(boolean)>(false); + // DBAction statues + const [patchStatus, setPatchStatus] = React.useState('idle'); + const [deleteStatus, setDeleteStatus] = React.useState('idle'); + const [addStatus, setAddStatus] = React.useState('idle'); + const roles = useAppSelector(SelectRoles); - const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - React.useEffect(() => { - if (remoteAssetStatus === 'uninitiated' || remoteAssetStatus === 'changed') - dispatch(RemoteXDAAssetSlice.Fetch()); - }, [dispatch, remoteAssetStatus]); + const remoteAssetController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAAsset`, "LocalAssetName", false), []) + const byAssetController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/ByAsset`, "AssetName", true), []) + const filters: Search.IFilter[] = React.useMemo(() => [{ + FieldName: 'RemoteXDAInstanceID', + SearchText: props.ID.toString(), + Operator: '=', + Type: 'number', + IsPivotColumn: false + }], [props.ID]) + // fetch remote assets React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAAssetSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); + setRemoteAssetStatus('loading'); - React.useEffect(() => { - dispatch(RemoteXDAAssetSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + const handle = remoteAssetController.PagedSearch(filters, sortKey, ascending, page); + + handle.done((d) => { + setRemoteAssets(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setRemoteAssetStatus('idle'); + }) + + handle.fail(() => setRemoteAssetStatus('error')); + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + + }, [filters, ascending, sortKey, page, remoteAssetController, refreshTrigger]); + + // fetch all assets React.useEffect(() => { - if (assetStatus === 'uninitiated' || assetStatus === 'changed') - dispatch(ByAssetSlice.Fetch()); - }, [dispatch, assetStatus]); + setAssetStatus('loading'); + + const handle = byAssetController.Fetch(); + + handle.done((d) => { + setAssetStatus('idle'); + setAssets(d.filter(asset => remoteAssets.find(rmt => rmt.LocalAssetKey === asset.AssetKey) != null ? true : false)); + }) + + handle.fail(() => setAssetStatus('error')) + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, [byAssetController, remoteAssets]); - function isEditable(item: OpenXDA.Types.RemoteXDAAsset): boolean { - return item.RemoteXDAAssetID <= 0; - } function hasPermissions(): boolean { if (roles.indexOf('Administrator') < 0) @@ -106,120 +141,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={remoteAssets} + 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 ( @@ -230,6 +278,15 @@ const RemoteAssetTab = (props: IProps) => {

    Remote openXDA Assets:

    +
    +
    +

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

    +
    +
    {cardBody} @@ -254,13 +311,14 @@ const RemoteAssetTab = (props: IProps) => {
    { - if (conf) dispatch(RemoteXDAAssetSlice.DBAction({ verb: 'DELETE', record: selectedAsset })); + setDeleteStatus('loading'); + if (conf) remoteAssetController.DBAction('DELETE', selectedAsset).done(() => { setDeleteStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setDeleteStatus('error')); setShowDelete(false); }} /> { - if (conf) dispatch(RemoteXDAAssetSlice.DBAction({ verb: 'PATCH', record: remoteAsset })); + if (conf) remoteAssetController.DBAction('PATCH', remoteAsset).done(() => { setPatchStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setPatchStatus('error')); setShowEdit(false); }} DisableConfirm={newInstErrors.length > 0} @@ -272,13 +330,17 @@ const RemoteAssetTab = (props: IProps) => { }> - { setShowAddAssets(false); - setAssetList([]); + setAssets([]); if (!conf) return; - selected.forEach((asset) => { + const handle = Promise.allSettled(selected.map((asset) => { let newRemote: OpenXDA.Types.RemoteXDAAsset = { ID: -1, RemoteXDAInstanceID: props.ID, @@ -293,13 +355,18 @@ const RemoteAssetTab = (props: IProps) => { RemoteAssetName: "", RemoteAssetKey: "" } - dispatch(RemoteXDAAssetSlice.DBAction({ verb: "POST", record: newRemote })); - }); + return remoteAssetController.DBAction("POST", newRemote); + })).then(() => { setAddStatus('idle'); setRefreshTrigger(val => !val)}) // some of these will fail, assuming that the selected asset was already attached to the remote XDA instance. + }} />
    ); - } -export default RemoteAssetTab; \ No newline at end of file +export default RemoteAssetTab; + + +function isEditable(item: OpenXDA.Types.RemoteXDAAsset): boolean { + return item.RemoteXDAAssetID <= 0; +} \ No newline at end of file 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..6044d647e2 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx @@ -24,10 +24,10 @@ 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'; +import { GenericController, LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAMeter, RemoteMeterForm } from './RemoteMeterForm'; @@ -38,41 +38,34 @@ interface IProps { ID: number } const RemoteMeterTab = (props: IProps) => { - // Display Remote Meters Consts + + const dispatch = useAppDispatch(); + + // Meters Table + const [meters, setMeters] = React.useState([]); + const [searchStatus, setSearchStatus] = React.useState('uninitiated'); const [sortKey, setSortKey] = React.useState('LocalMeterName'); const [ascending, setAscending] = React.useState(true); - const dispatch = useAppDispatch(); - const remoteMeterStatus = useAppSelector(RemoteXDAMeterSlice.Status) as Application.Types.Status; - const searchResults = useAppSelector(RemoteXDAMeterSlice.SearchResults); - const searchState = useAppSelector(RemoteXDAMeterSlice.SearchStatus); - - const searchFilters: Search.IFilter[] = - [{ - FieldName: 'RemoteXDAInstanceID', - SearchText: props.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }] + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - const noSameFilter: Search.IFilter = { - FieldName: 'ID', - SearchText: searchResults.map((r) => r.LocalXDAMeterID).join(','), - Operator: 'NOT IN', - Type: 'number', - IsPivotColumn: false - }; - // Shared Consts + // Remote Meters pagination + const [page, setPage] = React.useState(0) + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + // Shared const [selectedMeter, setSelectedMeter] = React.useState(BlankRemoteXDAMeter); - // Edit and Delete Form Consts + // Edit and Delete Form const [newInstErrors, setNewInstErrors] = React.useState([]); const [remoteMeter, setRemoteMeter] = React.useState(BlankRemoteXDAMeter); const [showEdit, setShowEdit] = React.useState<(boolean)>(false); const [showDelete, setShowDelete] = React.useState<(boolean)>(false); - // Add New Meter Consts - const meterStatus = useAppSelector(ByMeterSlice.Status) as Application.Types.Status; + // Add New Meter + const [meterStatus, setMeterStatus] = React.useState('uninitiated'); const [meterList, setMeterList] = React.useState>([]); const [showAddMeters, setShowAddMeters] = React.useState<(boolean)>(false); @@ -80,27 +73,76 @@ const RemoteMeterTab = (props: IProps) => { const [showLoading, setShowLoading] = React.useState<(boolean)>(false); const [assetCount, setAssetCount] = React.useState(0); + // DB Actions + const [remoteStatus, setRemoveStatus] = React.useState('idle'); + const [addMeterStatus, setAddMeterStatus] = React.useState('idle'); + const [addAssetStatus, setAddAssetStatus] = React.useState('idle'); + const [editStatus, setEditStatus] = React.useState('idle'); + const roles = useAppSelector(SelectRoles); - const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - React.useEffect(() => { - if (remoteMeterStatus === 'uninitiated' || remoteMeterStatus === 'changed') - dispatch(RemoteXDAMeterSlice.Fetch()); - }, [dispatch, remoteMeterStatus]); + const remoteMeterController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAMeter`, "LocalMeterName", false), []) - React.useEffect(() => { - if (meterStatus === 'uninitiated' || meterStatus === 'changed') - dispatch(ByMeterSlice.Fetch()); - }, [dispatch, meterStatus]); + const noSameFilter: Search.IFilter = { + FieldName: 'ID', + SearchText: meters.map((r) => r.LocalXDAMeterID).join(','), + Operator: 'NOT IN', + Type: 'number', + IsPivotColumn: false + }; + // fetch remote meters for the XDA node, paged and sorted React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAMeterSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); + setSearchStatus('loading'); + + const searchFilters: Search.IFilter[] = + [{ + FieldName: 'RemoteXDAInstanceID', + SearchText: props.ID.toString(), + Operator: '=', + Type: 'number', + IsPivotColumn: false + }] + + const handle = remoteMeterController.PagedSearch(searchFilters, sortKey, ascending, page); + + handle.done((d) => { + setMeters(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setSearchStatus('idle') + }) + + handle.fail(() => setSearchStatus('error')); + + return () => { + if (handle != null && handle.abort) + handle.abort() + } + + }, [props.ID, ascending, sortKey, page, remoteMeterController, refreshTrigger]); React.useEffect(() => { - dispatch(RemoteXDAMeterSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + setMeterStatus('loading'); + + const handle = new GenericController(`${homePath}api/OpenXDA/ByMeter`, "Name", true).Fetch(); + + handle.done((d) => { + setMeterList(d); + setMeterStatus('idle') + }) + + handle.fail(() => setMeterStatus('error')); + + return () => { + if (handle != null && handle.abort) + handle.abort() + } + }, []); + function isEditable(item: OpenXDA.Types.RemoteXDAMeter): boolean { return item.RemoteXDAMeterID <= 0; @@ -135,143 +177,156 @@ const RemoteMeterTab = (props: IProps) => { } let cardBody; - if (remoteMeterStatus === 'error') { + if (searchStatus === 'error') { cardBody = - } else if (remoteMeterStatus === 'loading' || showLoading) { + } else if (searchStatus === 'loading' || showLoading) { 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={meters} + 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 ( @@ -282,6 +337,16 @@ const RemoteMeterTab = (props: IProps) => {

    Remote openXDA Meters:

    +
    +
    +

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

    +
    +
    {cardBody} @@ -306,13 +371,20 @@ const RemoteMeterTab = (props: IProps) => {
    { - if (conf) dispatch(RemoteXDAMeterSlice.DBAction({ verb: 'DELETE', record: selectedMeter })); + if (conf) { + setRemoveStatus('loading'); + remoteMeterController.DBAction('DELETE', selectedMeter).done(() => { setRemoveStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setRemoveStatus('error')); + }; setShowDelete(false); - }}/> + }} /> + { - if (conf) dispatch(RemoteXDAMeterSlice.DBAction({ verb: 'PATCH', record: remoteMeter })); + if (conf) { + setEditStatus('loading'); + remoteMeterController.DBAction('PATCH', remoteMeter).done(() => { setEditStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setEditStatus('error')); + } setShowEdit(false); }} DisableConfirm={newInstErrors.length > 0} @@ -324,15 +396,17 @@ const RemoteMeterTab = (props: IProps) => { }> + 0} Title={'Add Associated Remote Assets?'} ShowCancel={true} CallBack={(conf) => { setAssetCount(0); if (conf) { + setAddAssetStatus('loading'); let addAssetHandle = addAssociatedAssets(selectedMeter); - addAssetHandle.then((data: number) => { - dispatch(RemoteXDAAssetSlice.Fetch()); // TODO: This doesn't properly reload the asset slice when switching tabs - }); + addAssetHandle.done((data: number) => { + setAddAssetStatus('idle'); + }).fail(() => setAddAssetStatus('error')); return () => { if (addAssetHandle != null && addAssetHandle.abort != null) { addAssetHandle.abort(); @@ -344,45 +418,45 @@ const RemoteMeterTab = (props: IProps) => { ShowX={true} Size={"sm"} ConfirmText={"Yes"} CancelText={"No"}> -

    Add { assetCount } Associated Assets?

    +

    Add {assetCount} Associated Assets?

    + { setShowAddMeters(false); setMeterList([]); - if (!conf) return; - selected.forEach((meter) => { - setShowLoading(true); - let newRemote: OpenXDA.Types.RemoteXDAMeter = { - ID: -1, - RemoteXDAInstanceID: props.ID, - LocalXDAMeterID: meter.ID, - RemoteXDAMeterID: -1, - RemoteXDAName: "", - RemoteXDAAssetKey: meter.AssetKey, - Obsfucate: false, - Synced: false, - LocalAlias: "", - LocalMeterName: "", - LocalAssetKey: "", - RemoteAlias: "" - } - dispatch(RemoteXDAMeterSlice.DBAction({ verb: "POST", record: newRemote })); - setSelectedMeter(newRemote); // Technically, this is a race condition with setAssetCount - let fetchAssetHandle = getAssociatedAssetCount(newRemote); - fetchAssetHandle.then((data: number) => { - setAssetCount(data); - setShowLoading(false); - }); - return () => { - if (fetchAssetHandle != null && fetchAssetHandle.abort != null) { - fetchAssetHandle.abort(); - setShowLoading(false); - } - }; + if (!conf || selected.length == 0) return; + const meter = selected[0] + setAddMeterStatus('loading'); // this should become the dbaction status for add + const newRemote: OpenXDA.Types.RemoteXDAMeter = { + ID: -1, + RemoteXDAInstanceID: props.ID, + LocalXDAMeterID: meter.ID, + RemoteXDAMeterID: -1, + RemoteXDAName: "", + RemoteXDAAssetKey: meter.AssetKey, + Obsfucate: false, + Synced: false, + LocalAlias: "", + LocalMeterName: "", + LocalAssetKey: "", + RemoteAlias: "" + } + setSelectedMeter(newRemote); + remoteMeterController.DBAction('POST', newRemote).done(() => { setAddMeterStatus('idle'); setRefreshTrigger(val => !val); }).fail(() => setAddMeterStatus('error')); + let fetchAssetHandle = getAssociatedAssetCount(newRemote); + fetchAssetHandle.then((data: number) => { + setAssetCount(data); + setShowLoading(false); }); + return () => { + if (fetchAssetHandle != null && fetchAssetHandle.abort != null) { + fetchAssetHandle.abort(); + setShowLoading(false); + } + }; }} Show={showAddMeters} Type={'single'} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx index 4f179b1371..03c79c2d0d 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx @@ -146,7 +146,7 @@ export default function RemoteXDAInstanceForm(props: IProps) { }); return () => { - 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 +61,48 @@ const newAcct: IUserAccount = { } const ByUser: Application.Types.iByComponent = (props) => { + let navigate = useNavigate(); const dispatch = useAppDispatch(); - const search = useAppSelector(UserAccountSlice.SearchFilters); - - 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 [filters, setFilters] = React.useState[]>([]); + const [sortField, setSortField] = React.useState("DisplayName"); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); - 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 [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: Application.Types.iAdditionalUserField[] = useAppSelector(UserAdditionalFieldSlice.Fields) - const adlFieldStatus: Application.Types.Status = useAppSelector(UserAdditionalFieldSlice.FieldStatus) + 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 [newUserStatus, setNewUserStatus] = React.useState('idle'); + + 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'), []) + + React.useEffect(() => { if (userStatus === 'error' || adlFieldStatus === 'error' || valueListItemStatus === 'error' || valueListGroupStatus === 'error') setPageStatus('error') @@ -105,30 +113,49 @@ 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]); - - React.useEffect(() => { - if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') - dispatch(UserAdditionalFieldSlice.FetchField()); - }, [adlFieldStatus]); + 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(Math.max(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 (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') - dispatch(ValueListSlice.Fetch()); - }, [valueListItemStatus]); + setAdlFieldStatus('loading') + const h = userAdditionalFieldController.Fetch(); + h.done((d) => { + setAdlFields(d as unknown as Application.Types.iAdditionalUserField[]) + setAdlFieldStatus('idle') + }).fail((d) => { + setAdlFieldStatus('error') + }) + if (h.abort != undefined) h.abort(); + }, []); React.useEffect(() => { - if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); - - const setPage = React.useCallback((page) => { - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) + setValueListItemStatus('loading') + const h = valueListController.Fetch(); + h.done((d) => { + setValueListItems(d as unknown as SystemCenter.Types.ValueListItem[]) + setValueListItemStatus('idle') + }).fail((d) => { + setValueListItemStatus('error') + }) + + return () => { + if (h.abort != undefined) h.abort(); + } + }, []); - useBoundPaging(currentPage, totalPages, setPage) React.useEffect(() => { function ConvertType(type: string) { @@ -137,27 +164,18 @@ 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
    - -
    ; - 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 +203,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 +282,18 @@ const ByUser: Application.Types.iByComponent = (props) => {
    setPage(page - 1)} Total={totalPages} />
    { - if (confirm) - dispatch(UserAccountSlice.DBAction({ verb: 'POST', record: act })) + if (confirm) { + setNewUserStatus('loading'); + userAccountController.DBAction('POST', act).done(() => { setRefreshTrigger((val) => !val); setNewUserStatus('idle'); }).fail(() => setNewUserStatus('error')); + } 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..fd8311cd12 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,50 @@ 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 [securityGroups, setSecurityGroups] = React.useState([]); const [status, setStatus] = React.useState('uninitiated'); - const searchStatus = useSelector(SecurityGroupSlice.SearchStatus); + const [filters, setFilters] = React.useState[]>([]); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const sortField = useSelector(SecurityGroupSlice.SortField); - const ascending = useSelector(SecurityGroupSlice.Ascending); + const [currentPage, setCurrentPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); 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(() => { - 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) + const [newGroupStatus, setNewGroupStatus] = React.useState('idle'); + + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup), []) + + 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(Math.max(d.NumberOfPages - 1, 0)); + setStatus('idle') + }).fail(() => setStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [sortField, ascending, filters, currentPage, securityGroupController, refreshTrigger]) 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 +105,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 +174,24 @@ 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) { + setNewGroupStatus('loading'); + securityGroupController.DBAction( + 'POST', + { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } + ).done(() => { + setRefreshTrigger(val => !val); + setNewGroupStatus('idle'); + setShowModal(false); + }).fail(() => setNewGroupStatus('error')); + } }} ConfirmShowToolTip={groupError.length > 0} ConfirmToolTipContent={<> @@ -213,3 +207,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..03bdaa089e 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 @@ -21,69 +21,62 @@ // ****************************************************************************************************** import * as React from 'react'; -import { Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; +import { Application } from '@gpa-gemstone/application-typings'; +import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { LoadingScreen } from '@gpa-gemstone/react-interactive'; -import { UserAccountSliceRemote } from '../../Store/Store'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ISecurityGroup } from '../Types'; -import { Table, Column } from '@gpa-gemstone/react-table'; -import { DefaultSelects } from '@gpa-gemstone/common-pages'; +import { UserAccountSliceRemote } from '../../Store/Store'; -const GroupUser = (props: {Group: ISecurityGroup}) => { +const GroupUser = (props: { Group: ISecurityGroup }) => { - const [showSelect, setShowSelect] = React.useState(false); + // user table const [users, setUsers] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); const [asc, setAsc] = React.useState(true); const [sortField, setSortField] = React.useState('AccountName'); - const [status, setStatus] = React.useState('uninitiated'); + 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]) + // user pagination + 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(() => { - setUsers((u) => _.orderBy(u, [sortField], asc ? 'asc' : 'desc')); - }, [asc, sortField]) + // user selector + const [showSelect, setShowSelect] = React.useState(false); - function getUsers() { - if (props.Group.Type != 'Database') - return; + // db actions + const [addUserStatus, setAddUserStatus] = React.useState('idle'); - 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) { + // fetch group users, paged and sorted + React.useEffect(() => { 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)); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); 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 (
    @@ -92,77 +85,101 @@ const GroupUser = (props: {Group: ISecurityGroup}) => {

    Users:

    +
    +
    +

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

    +
    +
    {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 +197,8 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { OnClose={(selected, conf) => { setShowSelect(false); if (!conf) return; - saveUser(selected); + setAddUserStatus('loading'); + saveUser(selected, props.Group.Type, props.Group.ID).done(() => { setRefreshTrigger(val => !val); setAddUserStatus('idle'); }).fail(() => setAddUserStatus('error')); }} Show={showSelect} Type={'multiple'} @@ -206,3 +224,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..5edf329e5d 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 * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { SecurityGroupSlice } from '../../Store/Store'; +import * as _ from 'lodash'; +import { Application } from '@gpa-gemstone/application-typings' +import { LoadingScreen, ServerErrorIcon, TabSelector, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { ISecurityGroup } from '../Types'; import GroupInfo from './Info'; import GroupUser from './GroupUsers'; @@ -36,12 +35,14 @@ declare type Tab = 'info' | 'users' | 'roles' interface IProps { GroupID: string, Tab: Tab } function UserGroup(props: IProps) { + 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') + + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup), []) function getTab(): Tab { if (props.Tab != undefined) return props.Tab; @@ -58,10 +59,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 +113,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).done(() => navigate(`${homePath}index.cshtml?name=Groups`) ).fail(() => setStatus('error')); } }} />
    @@ -105,3 +123,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..a5d0c762ee 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx @@ -23,37 +23,39 @@ 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 ValueListForm from './ValueListForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { Modal } from '@gpa-gemstone/react-interactive'; -import { ValueListItemDelete, RequiredValueLists } from './ValueListGroupDelete'; import { ToolTip } from '@gpa-gemstone/react-forms'; - +import { Modal, Search, GenericController } from '@gpa-gemstone/react-interactive'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; +import { ValueListItemDelete, RequiredValueLists } from './ValueListGroupDelete'; +import ValueListForm from './ValueListForm'; interface IProps { Record: SystemCenter.Types.ValueListGroup } export default function ValueListGroupItems(props: IProps) { - const dispatch = useAppDispatch(); + const emptyRecord: SystemCenter.Types.ValueListItem = { ID: 0, GroupID: props.Record.ID, Value: '', AltValue: null, SortOrder: 0 }; + + const [data, setData] = React.useState([]) + const [status, setStatus] = React.useState('uninitiated') + const [sortField, setSortField] = React.useState('SortOrder') + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false) + const [hover, setHover] = React.useState(''); - 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 [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); - const emptyRecord: SystemCenter.Types.ValueListItem = { ID: 0, GroupID: parentID as number, 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 controller = React.useMemo(() => new GenericController(`${homePath}api/ValueList`, 'SortOrder'), []); const disallowReason = React.useCallback((ID: string) => { if (!RequiredValueLists.includes(props.Record?.Name)) @@ -67,9 +69,24 @@ 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) + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + 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,97 +105,120 @@ export default function ValueListGroupItems(props: IProps) { }, [props.Record?.Name]); return ( -
    +

    List Items:

    +
    +
    +

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

    +
    +
    -
    -
    - - 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 })); - }} - 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 ( - <> - - - - ); +
    +
    +
    + + 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} + /> +
    @@ -191,8 +231,9 @@ export default function ValueListGroupItems(props: IProps) { { - if (conf) - dispatch(ValueListSlice.DBAction({ verb: 'DELETE', record: { ...record } })); + if (conf) { + controller.DBAction('DELETE', { ...record }).then(() => setRefreshTrigger((val) => !val)) + } setShowWarning(false); }} Record={record} @@ -209,18 +250,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