From 7880c88718907fce2b395459ae9bb5691cf3f17d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 14:52:50 -0400 Subject: [PATCH 01/11] paginate Asset Location --- .../OpenXDA/Assets/OpenXDAAssetController.cs | 34 ++++- .../TSX/SystemCenter/Asset/AssetLocation.tsx | 123 +++++++++++------- 2 files changed, 111 insertions(+), 46 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index 019ae00e56..1c0332d0c6 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -24,7 +24,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Diagnostics; using System.Linq; using System.Transactions; using System.Web.Http; @@ -33,6 +32,7 @@ using GSF.Data.Model; using GSF.Reflection; using GSF.Web.Model; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using openXDA.Model; using SystemCenter.Model; @@ -65,6 +65,38 @@ 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)) + { + RecordRestriction assetLocationRestriction = new RecordRestriction("ID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = {0})", assetID); + + int count = new TableOperations(connection).QueryRecordCount(assetLocationRestriction); + + IEnumerable records = new TableOperations(connection).QueryRecords(postData.OrderBy, postData.Ascending, page + 1, recordsPerPage, assetLocationRestriction); + + results.TotalRecords = count; + results.NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records); + } + return Ok(results); + } + [HttpGet, Route("{assetID:int}/AssetLocations")] public IHttpActionResult GetAssetLocationModels(int assetID) { 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..2630756e8d 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx @@ -23,8 +23,8 @@ 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 { Application, OpenXDA } from '@gpa-gemstone/application-typings'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { useAppSelector } from '../hooks'; @@ -37,9 +37,16 @@ declare var homePath: string; function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ let navigate = useNavigate(); const [locations, setLocations] = React.useState>([]); + const [locationStatus, setLocationStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [allLocations, setAllLocations] = React.useState>([]); + const [allLocationStatus, setAllLocationsStatus] = React.useState('uninitiated'); const [newLocation, setNewLocation] = React.useState(); const [hover, setHover] = React.useState(undefined); const [showModal, setShowModal] = React.useState(false); @@ -52,47 +59,62 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element } React.useEffect(() => { - getData(); - }, [props.Asset]); + setAllLocationsStatus('loading'); - function getData() { - getLocations(); - getAllOtherLocations(); - } - - function getLocations(): void { - $.ajax({ + const handle = $.ajax({ type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations`, + url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/OtherLocations`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, async: true - }).done(data => { - const sortedLocations = sortData(sortField, ascending, data); - setLocations(sortedLocations); + }) + + handle.done(data => { + let records = _.orderBy(data, ['Name'], ['asc']); + setAllLocations(records); + setNewLocation(records[0]); + setAllLocationsStatus('idle'); }); - } - function sortData(key: keyof OpenXDA.Types.Location, ascending: boolean, data: OpenXDA.Types.Location[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } + handle.fail(() => setAllLocationsStatus('error')); - function getAllOtherLocations(): void { - $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/OtherLocations`, + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + }, [props.Asset.ID, refreshTrigger]); + + + React.useEffect(() => { + setLocationStatus('loading'); + + const handle = $.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 => { - let records = _.orderBy(data, ['Name'], ['asc']); - setAllLocations(records); - setNewLocation(records[0]); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) + }) + + handle.done((d) => { + setLocations(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setLocationStatus('idle') }); - } + handle.fail(() => setLocationStatus('error')); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, [props.Asset.ID, sortField, ascending, page, refreshTrigger]) async function deleteLocation(location: OpenXDA.Types.Location) { return $.ajax({ @@ -102,8 +124,8 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element dataType: 'json', cache: true, async: true - }).done((assets: Array) => { - getData(); + }).done(() => { + setRefreshTrigger(val => !val); }).fail((msg) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) @@ -118,8 +140,8 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element dataType: 'json', cache: true, async: true - }).done(record => { - getData(); + }).done(() => { + setRefreshTrigger(val => !val); }).fail((msg) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) @@ -138,25 +160,26 @@ 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); - } + if (d.colKey === sortField) setAscending(a => !a); + else setSortField(d.colField); }} TableStyle={{ height: '100%' }} TheadStyle={{ fontSize: 'smaller' }} @@ -223,6 +246,16 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element
+
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
+
+
+

+ {`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); - } + if (d.colKey === sortKey) setAscending(a => !a); + else setSortKey(d.colField); }} TableStyle={{ height: '100%' }} TheadStyle={{ fontSize: 'smaller' }} @@ -307,6 +311,16 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number >

+
+
+
+ { }} + Total={1} + /> +
+
From 8d348304c77917b8a5835fd292cea6866cc0b556 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:06:13 -0400 Subject: [PATCH 03/11] paginate asset Meters --- .../TSX/SystemCenter/Asset/AssetMeter.tsx | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) 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..87b5aaf580 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { ByMeterSlice } from '../Store/Store'; @@ -39,8 +39,13 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ let navigate = useNavigate(); const dispatch = useAppDispatch(); const [meters, setMeters] = React.useState>([]); - const [sortField, setSortField] = React.useState('Name'); + const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [showAdd, setShowAdd] = React.useState(false); const allMeters = useAppSelector(ByMeterSlice.Data); const mStatus = useAppSelector(ByMeterSlice.Status); @@ -55,26 +60,28 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ React.useEffect(() => { - getMeters(); - }, [props.Asset]); - - function getMeters(): void { - $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters`, + const handle = $.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 }) + }).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)); }); - } - function sortData(key: keyof OpenXDA.Types.Meter, ascending: boolean, data: OpenXDA.Types.Meter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); + return () => { + if (handle != null && handle.abort != null) + handle.abort(); } + }, [props.Asset, refreshTrigger, sortField, ascending, page]); function addMeter(meterID: number) { return $.ajax({ @@ -85,7 +92,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ cache: true, async: true }).done(record => { - getMeters(); + setRefreshTrigger(val => !val); }).fail((msg) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) @@ -154,7 +161,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ cache: true, async: true }).done(() => { - getMeters(); + setRefreshTrigger(val => !val); }).fail((msg) => { if (msg.status == 500) alert(msg.responseJSON.ExceptionMessage) @@ -175,25 +182,24 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{

Meters:

+
+
+

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

-
+
+
+
+
TableClass="table table-hover" Data={meters} SortKey={sortField} Ascending={ascending} 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); - } + if (d.colKey === sortField) setAscending(a => !a); + else setSortField(d.colField); }} TableStyle={{ height: '100%' }} TheadStyle={{ fontSize: 'smaller' }} @@ -235,6 +241,16 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ > Model +
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
From 519ad489b26d40d9002624d1c6f7f0439ccd4ec1 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:06:36 -0400 Subject: [PATCH 04/11] paginate substation images --- .../OpenXDA/OpenXDALocationController.cs | 34 +++++++++-- .../SystemCenter/Location/LocationImages.tsx | 56 ++++++++++++++----- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs index 2a15dbcd27..9038b85caf 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,18 @@ 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 ?? 50 + }); } else return Unauthorized(); @@ -335,6 +344,23 @@ public IHttpActionResult GetImagesForLocation(int locationID) } + 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}")] public HttpResponseMessage GetImageForLocation(int locationID, string file) { 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..c7fd6607ef 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx @@ -24,40 +24,63 @@ 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)}); - - return () => { - if (handle.abort != undefined) handle.abort(); - }; - }, [props.Location.ID]); - - function getImages(): JQuery.jqXHR { - return $.ajax({ + setImageStatus('loading') + let handle = $.ajax({ type: "GET", - url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images`, + url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, async: true }) - } + handle.done(d => { + setImages(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)); + setImageStatus('idle') + }); + handle.fail(() => setImageStatus('error')); + + return () => { + if (handle.abort != undefined) handle.abort(); + }; + }, [props.Location.ID, page]); return (
+

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}`} +

+
+
@@ -84,6 +107,13 @@ const LocationImagesWindow = (props: { Location: OpenXDA.Types.Location }) => { }
+
+ { setPage(page - 1) }} + Current={page + 1} + Total={totalPages} + /> +
0} ShowCancel={false} ShowX={true} ShowConfirm={false} Title={image} CallBack={() => setImage('') }> From bda57200860fe2b5f2ffce9eca3e360ff91c0b66 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:06:48 -0400 Subject: [PATCH 05/11] paginate Customer asset tab with controller --- .../SystemCenter/Customer/CustomerAsset.tsx | 117 +++++++++++++----- 1 file changed, 84 insertions(+), 33 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx index bb0c3bceb4..c4f2d106d8 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx @@ -23,42 +23,64 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA as LocalXDA } from '../global'; -import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings' -import { useAppDispatch, useAppSelector } from '../hooks'; -import { CustomerAssetSlice } from '../Store/Store' -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings' import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { LoadingIcon, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; +import { GenericController, LoadingIcon, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import AssetSelect from '../Asset/AssetSelect'; +import { OpenXDA as LocalXDA } from '../global'; +import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; interface IProps { Customer: OpenXDA.Types.Customer } const CustomerAssetWindow = (props: IProps) => { - const dispatch = useAppDispatch(); - const data = useAppSelector(CustomerAssetSlice.Data); - const status = useAppSelector(CustomerAssetSlice.Status); + + const [customerAssets, setCustomerAssets] = React.useState([]); + const [customerAssetStatus, setCustomerAssetStatus] = React.useState('uninitiated'); + const [sortField, setSortField] = 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 [showAdd, setShowAdd] = React.useState(false); - const sortField = useAppSelector(CustomerAssetSlice.SortField); - const ascending = useAppSelector(CustomerAssetSlice.Ascending); + const [page, setPage] = React.useState(0); + const [pageInfo, setPageInfo] = React.useState<{ TotalPages: number, TotalRecords: number, RecordsPerPage: number }>({ TotalPages: 0, TotalRecords: 0, RecordsPerPage: 0 }) const [removeRecord, setRemoveRecord] = React.useState(null); - const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles) + const customerAssetController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/CustomerAsset`, 'AssetName', true), []) + + // fetch paged and sorted CustomerAssets React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed') - dispatch(CustomerAssetSlice.Fetch()); - }, [status]); + + setCustomerAssetStatus('loading') + const handle = customerAssetController.PagedSearch([], sortField, ascending, page, props.Customer.ID); + + handle.done((d) => { + setCustomerAssets(JSON.parse(d.Data as unknown as string)) + setPageInfo({ TotalPages: d.NumberOfPages, TotalRecords: d.TotalRecords, RecordsPerPage: d.RecordsPerPage }); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setCustomerAssetStatus('idle'); + }) + + handle.fail(() => { setCustomerAssetStatus('error') }); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, [customerAssetController, sortField, ascending, page, props.Customer.ID, refreshTrigger]) function saveCustomerAssets(m: SystemCenter.Types.DetailedAsset[]) { - m.forEach((asset) => { - dispatch(CustomerAssetSlice.DBAction({ - verb: 'POST', record: { + Promise.all(m.map((asset) => { + customerAssetController.DBAction( + 'POST', { ID: 0, CustomerKey: props.Customer.CustomerKey, CustomerName: props.Customer.Name, @@ -67,9 +89,8 @@ const CustomerAssetWindow = (props: IProps) => { AssetName: asset.AssetName, AssetType: asset.AssetType, AssetID: asset.ID - } - })) }) + })).then(() => setRefreshTrigger(val => !val)); } function hasPermissions(): boolean { @@ -78,7 +99,7 @@ const CustomerAssetWindow = (props: IProps) => { return true; } - if (status == 'error') + if (customerAssetStatus == 'error') return
@@ -96,7 +117,7 @@ const CustomerAssetWindow = (props: IProps) => {
- if (status == 'loading') + if (customerAssetStatus == 'loading') return
@@ -122,18 +143,25 @@ const CustomerAssetWindow = (props: IProps) => {

Assigned Assets:

+
+
+

+ {`Displaying Asset${customerAssets.length > 1 ? 's' : ''} ${pageInfo.TotalRecords > 0 ? (pageInfo.RecordsPerPage * page + 1) : 0} - ${pageInfo.RecordsPerPage * page + customerAssets.length} out of ${pageInfo.TotalRecords}`} +

-
-
+
+ +
+
+
TableClass="table table-hover" - Data={data} + Data={customerAssets} SortKey={sortField} Ascending={ascending} OnSort={(d) => { - if (d.colKey == 'Remove') - return; - dispatch(CustomerAssetSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortField) setAscending(a => !a); + else setSortField(d.colField); }} TheadStyle={{ fontSize: 'smaller' }} RowStyle={{ fontSize: 'smaller' }} @@ -179,11 +207,25 @@ const CustomerAssetWindow = (props: IProps) => {
+
+
+ { setPage(p - 1) }} + Total={pageInfo.TotalPages} + Current={page + 1} + />
+
+
-
@@ -192,13 +234,22 @@ const CustomerAssetWindow = (props: IProps) => {
- { if (c) dispatch(CustomerAssetSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> - { if (c) customerAssetController.DBAction('DELETE', removeRecord).done(() => { setRemoveRecord(null); setRefreshTrigger(val => !val) }); }} + /> + { setShowAdd(false) if (!conf) return - saveCustomerAssets(selected.filter(items => data.findIndex(g => g.AssetID == items.ID) < 0)); + saveCustomerAssets(selected.filter(items => customerAssets.findIndex(g => g.AssetID == items.ID) < 0)); }} /> } From 7c01dd33709fa7fd6b302888f1952238ebec298b Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:07:01 -0400 Subject: [PATCH 06/11] paginate Customer meter tab with controller --- .../SystemCenter/Customer/CustomerMeter.tsx | 113 +++++++++++------- 1 file changed, 71 insertions(+), 42 deletions(-) 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..eae201eb86 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx @@ -23,13 +23,13 @@ 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 as LocalXDA } from '../global'; +import { Application, OpenXDA, SystemCenter } 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 { GenericController, LoadingIcon, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { SelectRoles } from '../Store/UserSettings'; @@ -38,38 +38,43 @@ 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 [customerMeters, setCustomerMeters] = React.useState([]); + const [customerMeterStatus, setCustomerMeterStatus] = React.useState('uninitiated'); + const [sortField, setSortField] = React.useState('MeterName'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + const [showAdd, setShowAdd] = React.useState(false); - const sortField = useAppSelector(CustomerMeterSlice.SortField); - const ascending = useAppSelector(CustomerMeterSlice.Ascending); + const [page, setPage] = React.useState(0); + const [pageInfo, setPageInfo] = React.useState<{ TotalPages: number, TotalRecords: number, RecordsPerPage: number }>({ TotalPages: 0, TotalRecords: 0, RecordsPerPage: 0 }) const [removeRecord, setRemoveRecord] = React.useState(null); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); - React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed') - getData(); - }, [status]) + const customerMeterController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/CustomerMeter`, 'MeterName', true), []); React.useEffect(() => { - getData(); - }, [props.Customer.ID]) + setCustomerMeterStatus('loading') + const handle = customerMeterController.PagedSearch([], sortField, ascending, page, props.Customer.ID); + + handle.done((d) => { + setCustomerMeters(JSON.parse(d.Data as unknown as string)) + setPageInfo({ TotalPages: d.NumberOfPages, TotalRecords: d.TotalRecords, RecordsPerPage: d.RecordsPerPage }); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setCustomerMeterStatus('idle'); + }) - function getData() { - dispatch(CustomerMeterSlice.DBSearch({ - filter: [{ - FieldName: 'CustomerID', - SearchText: props.Customer.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }], sortField, ascending - })); + handle.fail(() => { setCustomerMeterStatus('error') }); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); } + }, [customerMeterController, sortField, ascending, page, props.Customer.ID, refreshTrigger]) function getEnum(setOptions, field) { let handle = null; @@ -121,9 +126,9 @@ const CustomerMeterWindow = (props: IProps) => { } 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 +138,8 @@ const CustomerMeterWindow = (props: IProps) => { MeterName: meter.Name, MeterID: meter.ID } - })) - }) + ) + })).then(() => setRefreshTrigger(val => !val)); } function hasPermissions(): boolean { @@ -143,7 +148,7 @@ const CustomerMeterWindow = (props: IProps) => { return true; } - if (status == 'error') + if (customerMeterStatus === 'error') return
@@ -161,7 +166,7 @@ const CustomerMeterWindow = (props: IProps) => {
- if (status == 'loading') + if (customerMeterStatus === 'loading') return
@@ -187,18 +192,24 @@ const CustomerMeterWindow = (props: IProps) => {

Assigned Meters:

+
+
+

+ {`Displaying Meter${customerMeters.length > 1 ? 's' : ''} ${pageInfo.TotalRecords > 0 ? (pageInfo.RecordsPerPage * page + 1) : 0} - ${pageInfo.RecordsPerPage * page + customerMeters.length} out of ${pageInfo.TotalRecords}`} +

-
-
+
+
+
+
TableClass="table table-hover" - Data={data} + Data={customerMeters} SortKey={sortField} Ascending={ascending} OnSort={(d) => { - if (d.colKey == 'Remove') - return; - dispatch(CustomerMeterSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortField) setAscending(a => !a); + else setSortField(d.colField); }} TheadStyle={{ fontSize: 'smaller' }} RowStyle={{ fontSize: 'smaller' }} @@ -244,11 +255,25 @@ const CustomerMeterWindow = (props: IProps) => {
+
+
+ setPage(p - 1)} + Total={pageInfo.TotalPages} + Current={page + 1} + />
+
+
-
@@ -257,14 +282,18 @@ const CustomerMeterWindow = (props: IProps) => {
- { if (c) dispatch(CustomerMeterSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> + { if (c) customerMeterController.DBAction('DELETE', removeRecord).done(() => { setRemoveRecord(null); setRefreshTrigger(val => !val) }) }} /> { setShowAdd(false) if (!conf) return - saveCustomerMeters(selected.filter(items => data.findIndex(g => g.MeterID == items.ID) < 0)) + saveCustomerMeters(selected.filter(items => customerMeters.findIndex(g => g.MeterID == items.ID) < 0)) }} Show={showAdd} Type={'multiple'} From efb920a1e63ce084fec0ec2bd42b5e63f4f441ef Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:07:25 -0400 Subject: [PATCH 07/11] paginate external table fields, fixing selection popup --- .../ExternalDB/ExternalDBTableFields.tsx | 90 ++++++++++++------- 1 file changed, 57 insertions(+), 33 deletions(-) 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..38490b5983 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx @@ -27,9 +27,9 @@ import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import { useAppSelector, useAppDispatch } from '../hooks'; import { AdditionalFieldsSlice, ValueListGroupSlice } from '../Store/Store'; import AdditionalFieldForm from '../AdditionalFields/AdditionalFieldForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { LoadingScreen, Modal, SearchBar, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingScreen, Modal, SearchBar, ServerErrorIcon, Warning, GenericController, Search } from '@gpa-gemstone/react-interactive'; import { SelectPopup } from '@gpa-gemstone/common-pages'; const emptyRecord: SystemCenter.Types.AdditionalFieldView = { @@ -62,7 +62,14 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const parentID = React.useRef(-1); const [tableStatus, setTableStatus] = React.useState('uninitiated'); const [asc, setAsc] = React.useState(true); - const [sortKey, setSortKey] = React.useState('FieldName'); + const [sortKey, setSortKey] = React.useState('FieldName'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [record, setRecord] = React.useState(emptyRecord); const [showRemove, setShowRemove] = React.useState(false); @@ -73,32 +80,28 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const [overWriteFields, setOverWriteFields] = React.useState([]); + const additionalFieldsController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalFieldView`, "FieldName", true), []) + React.useEffect(() => { if (status !== 'idle') return; - if (tableStatus === 'uninitiated' || tableStatus === 'changed' || parentID.current !== props.ID) { + + const filters: Search.IFilter[] = [{ SearchText: props.TableName, Operator: "=", IsPivotColumn: false, FieldName: "ExternalTable", Type: 'string' }] 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())); + const handle = additionalFieldsController.PagedSearch(filters, sortKey, asc, page) + handle.done((d) => { + setFieldsInTable(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)); setTableStatus('idle'); }); handle.fail(() => { setTableStatus('error'); }); - } - }, [tableStatus, props.ID, status]); - - React.useEffect(() => { - sortData(fieldsInTable); - }, [sortKey, asc]); + }, [props.ID, status, sortKey, asc, page, additionalFieldsController, refreshTrigger]); React.useEffect(() => { if (status === 'uninitiated' || status === 'changed') @@ -122,22 +125,24 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu }, [props.ID]); */ - const sortData = React.useCallback((sortData: SystemCenter.Types.AdditionalFieldView[]) => { - setFieldsInTable(_.orderBy(sortData, [sortKey], [(asc ? "asc" : "desc")])); - }, [setFieldsInTable, sortKey, asc]); - function Delete() { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'DELETE', record: { ...record } })); + additionalFieldsController.DBAction('DELETE', { ...record }).then(() => { + setRefreshTrigger(val => !val) setRecord(emptyRecord); + }); } function AssociateField(fld) { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record: { ...fld, ExternalDBTableID: props.ID } })); + additionalFieldsController.DBAction('PATCH', { ...fld, ExternalDBTableID: props.ID }).then(() => { + setRefreshTrigger(val => !val) + }); } function DisassociateField(fld) { - dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record: { ...fld, ExternalDBTableID: null } })); + additionalFieldsController.DBAction('PATCH', { ...fld, ExternalDBTableID: null }).then(() => { + setRefreshTrigger(val => !val) setRecord(emptyRecord); + }); } return ( @@ -150,6 +155,15 @@ 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 +180,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' }} @@ -256,6 +270,15 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu }
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
@@ -310,8 +333,8 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu setShowNew(false); if (conf) { setTableStatus('changed'); - if (record.ID > 0) dispatch(AdditionalFieldsSlice.DBAction({ verb: 'PATCH', record })); - else if (record.ID == 0) dispatch(AdditionalFieldsSlice.DBAction({ verb: 'POST', record })); + if (record.ID > 0) additionalFieldsController.DBAction('PATCH', record).then(() => setRefreshTrigger(val => !val)); + else if (record.ID == 0) additionalFieldsController.DBAction('POST', record).then(() => setRefreshTrigger(val => !val)); } }} > @@ -358,6 +381,9 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu ResultNote={searchStatus == 'error' ? 'Could not complete Search' : 'Found ' + searchData.length + ' Additional Field(s)'} > {children} + + } + > Name row.item ? : } >Key - - } - /> +
From 12dcf82c6bdf320c131a2d01fd469f14daf28677 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:31:22 -0400 Subject: [PATCH 08/11] paginate externalDBTables with controller --- .../ExternalDB/ExternalDBTables.tsx | 86 ++++++++++++++----- 1 file changed, 64 insertions(+), 22 deletions(-) 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..7349569715 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx @@ -24,23 +24,26 @@ 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 { Application, SystemCenter } from '@gpa-gemstone/application-typings'; +import { useAppDispatch } from '../hooks'; import ExternalDBTableForm from './ExternalDBTableForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { Modal, Warning } from '@gpa-gemstone/react-interactive'; +import { GenericController, Modal, Warning, Search } from '@gpa-gemstone/react-interactive'; export default function ExternalDBTables(props: { ID: number }) { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const data = useAppSelector(ExternalDBTablesSlice.Data); - const sortKey = useAppSelector(ExternalDBTablesSlice.SortField); - const asc = useAppSelector(ExternalDBTablesSlice.Ascending); - const status = useAppSelector(ExternalDBTablesSlice.Status); - const parentID = useAppSelector(ExternalDBTablesSlice.ParentID); + const [data, setData] = React.useState([]); + const [sortKey, setSortKey] = React.useState('TableName'); + const [asc, setAsc] = React.useState(true); + const [status, setStatus] = 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); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const emptyRecord: SystemCenter.Types.extDBTables = { ID: 0, TableName: '', ExtDBID: 0, Query: '' }; const [record, setRecord] = React.useState(emptyRecord); @@ -48,10 +51,8 @@ export default function ExternalDBTables(props: { ID: number }) { const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); - React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed' || parentID != props.ID) - dispatch(ExternalDBTablesSlice.Fetch(props.ID)); - }, [status, parentID, props.ID]); + const externalDBTableController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/extDBTables`, "TableName", true), []) + const filters: Search.IFilter[] = React.useMemo(() => [{ SearchText: props.ID.toString(), FieldName: 'ExtDBID', Operator: "=", IsPivotColumn: false, Type: 'string' }], [props.ID]) React.useEffect(() => { let e = []; @@ -65,10 +66,33 @@ export default function ExternalDBTables(props: { ID: number }) { setErrors(e); }, [record]); + React.useEffect(() => { + setStatus('loading'); + const handle = externalDBTableController.PagedSearch(filters, sortKey, asc, page) + + handle.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setStatus('idle') + }) + handle.fail(() => { setStatus('error') }); + + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, [externalDBTableController, filters, sortKey, asc, refreshTrigger, page]) + function Delete() { - dispatch(ExternalDBTablesSlice.DBAction({ verb: 'DELETE', record: { ...record } })); - setShowWarning(false); - setRecord(emptyRecord); + externalDBTableController.DBAction('DELETE', { ...record }).then(() => { + setShowWarning(false); + setRecord(emptyRecord); + setRefreshTrigger(val => !val); + }) } function handleSelect(item) { @@ -85,6 +109,15 @@ export default function ExternalDBTables(props: { ID: number }) {

Tables:

+
+
+

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

+
+
@@ -96,8 +129,8 @@ export default function ExternalDBTables(props: { ID: number }) { SortKey={sortKey.toString()} Ascending={asc} OnSort={(d) => { - if (d.colKey == 'btns') return; - dispatch(ExternalDBTablesSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortKey) setAsc(a => !a); + else setSortKey(d.colField); }} OnClick={handleSelect} TableStyle={{ padding: 0, width: '100%', height: '100%', tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' }} @@ -141,6 +174,15 @@ export default function ExternalDBTables(props: { ID: number }) {
+
+
+ setPage(p - 1)} + /> +
+
@@ -161,9 +203,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).then(() => setRefreshTrigger(val => !val)); else if (conf && record.ID == 0) - dispatch(ExternalDBTablesSlice.DBAction({ verb: 'POST', record })); + externalDBTableController.DBAction('POST', record).then(() => setRefreshTrigger(val => !val)); }} > From 5bfa63f3b058329f4769e8675826cfe853003626 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:31:27 -0400 Subject: [PATCH 09/11] paginate remote assets with controller --- .../SystemCenter/RemoteXDA/RemoteAssetTab.tsx | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) 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..99be7ec966 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 { LoadingScreen, Modal, Search, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAAsset, RemoteAssetForm } from './RemoteAssetForm'; @@ -43,17 +43,14 @@ const RemoteAssetTab = (props: IProps) => { 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 [searchResults, setSearchResults] = React.useState([]); + const [searchState, setSearchState] = React.useState('uninitiated'); - const searchFilters: Search.IFilter[] = - [{ - FieldName: 'RemoteXDAInstanceID', - SearchText: props.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }] + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); // Edit and Delete Form Consts const [newInstErrors, setNewInstErrors] = React.useState([]); @@ -70,19 +67,37 @@ const RemoteAssetTab = (props: IProps) => { const roles = useAppSelector(SelectRoles); const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); + const remoteAssetController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAAsset`, "LocalAssetName", false), []) + const filters: Search.IFilter[] = React.useMemo(() => [{ + FieldName: 'RemoteXDAInstanceID', + SearchText: props.ID.toString(), + Operator: '=', + Type: 'number', + IsPivotColumn: false + }], [props.ID]) + React.useEffect(() => { if (remoteAssetStatus === 'uninitiated' || remoteAssetStatus === 'changed') dispatch(RemoteXDAAssetSlice.Fetch()); }, [dispatch, remoteAssetStatus]); React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAAssetSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); - - React.useEffect(() => { - dispatch(RemoteXDAAssetSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + setSearchState('loading'); + const handle = remoteAssetController.PagedSearch(filters, sortKey, ascending, page); + handle.done((d) => { + setSearchResults(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)); + setSearchState('idle') + }) + handle.fail(() => setSearchState("error")); + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + }, [filters, sortKey, ascending, page, remoteAssetController, refreshTrigger]) React.useEffect(() => { if (assetStatus === 'uninitiated' || assetStatus === 'changed') @@ -106,6 +121,8 @@ const RemoteAssetTab = (props: IProps) => { cardBody = } else { cardBody = + <> +
TableClass="table table-hover" Data={searchResults} @@ -220,6 +237,17 @@ const RemoteAssetTab = (props: IProps) => { >

+
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
+ } return ( @@ -230,6 +258,15 @@ const RemoteAssetTab = (props: IProps) => {

Remote openXDA Assets:

+
+
+

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

+
+
{cardBody} @@ -254,13 +291,13 @@ const RemoteAssetTab = (props: IProps) => {
{ - if (conf) dispatch(RemoteXDAAssetSlice.DBAction({ verb: 'DELETE', record: selectedAsset })); + if (conf) remoteAssetController.DBAction('DELETE',selectedAsset).then(() => setRefreshTrigger(val => !val)); setShowDelete(false); }} /> { - if (conf) dispatch(RemoteXDAAssetSlice.DBAction({ verb: 'PATCH', record: remoteAsset })); + if (conf) remoteAssetController.DBAction('PATCH', remoteAsset).then(() => setRefreshTrigger(val => !val)); setShowEdit(false); }} DisableConfirm={newInstErrors.length > 0} @@ -293,7 +330,7 @@ const RemoteAssetTab = (props: IProps) => { RemoteAssetName: "", RemoteAssetKey: "" } - dispatch(RemoteXDAAssetSlice.DBAction({ verb: "POST", record: newRemote })); + remoteAssetController.DBAction("POST", newRemote).then(() => setRefreshTrigger(val => !val)); }); }} /> From acf105786873f12f0e28640cfcfe7ee4acc158a8 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 24 Jul 2026 16:31:34 -0400 Subject: [PATCH 10/11] paginate remote Meters tab with controller --- .../SystemCenter/RemoteXDA/RemoteMeterTab.tsx | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) 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..3f8ff23b3f 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 { RemoteXDAMeterSlice, ByMeterSlice } from '../Store/Store'; +import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAMeter, RemoteMeterForm } from './RemoteMeterForm'; @@ -43,17 +43,8 @@ const RemoteMeterTab = (props: IProps) => { 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 [searchResults, setSearchResults] = React.useState([]); + const [searchState, setSearchState] = React.useState('uninitiated'); const noSameFilter: Search.IFilter = { FieldName: 'ID', @@ -62,6 +53,13 @@ const RemoteMeterTab = (props: IProps) => { Type: 'number', IsPivotColumn: false }; + + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + // Shared Consts const [selectedMeter, setSelectedMeter] = React.useState(BlankRemoteXDAMeter); @@ -83,6 +81,9 @@ const RemoteMeterTab = (props: IProps) => { const roles = useAppSelector(SelectRoles); const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); + const remoteMeterController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAMeter`, "LocalMeterName", false), []) + const searchFilters: Search.IFilter[] = React.useMemo(() => [{ FieldName: 'RemoteXDAInstanceID', SearchText: props.ID.toString(), Operator: '=', Type: 'number', IsPivotColumn: false }],[props.ID]) + React.useEffect(() => { if (remoteMeterStatus === 'uninitiated' || remoteMeterStatus === 'changed') dispatch(RemoteXDAMeterSlice.Fetch()); @@ -94,13 +95,26 @@ const RemoteMeterTab = (props: IProps) => { }, [dispatch, meterStatus]); React.useEffect(() => { - if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAMeterSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); - }, [dispatch, searchState]); + setSearchState('loading') + const handle = remoteMeterController.PagedSearch(searchFilters, sortKey, ascending, page); - React.useEffect(() => { - dispatch(RemoteXDAMeterSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + handle.done((d) => { + setSearchResults(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)); + setSearchState('idle') + }) + + handle.fail(() => setSearchState('error')) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + + }, [ascending, sortKey, remoteMeterController, searchFilters, refreshTrigger]) function isEditable(item: OpenXDA.Types.RemoteXDAMeter): boolean { return item.RemoteXDAMeterID <= 0; @@ -141,6 +155,8 @@ const RemoteMeterTab = (props: IProps) => { cardBody = } else { cardBody = + <> +
TableClass="table table-hover" Data={searchResults} @@ -272,6 +288,17 @@ const RemoteMeterTab = (props: IProps) => { >

+
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
+ } return ( @@ -282,6 +309,16 @@ const RemoteMeterTab = (props: IProps) => {

Remote openXDA Meters:

+
+
+

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

+
+
+
{cardBody} @@ -306,13 +343,13 @@ const RemoteMeterTab = (props: IProps) => {
{ - if (conf) dispatch(RemoteXDAMeterSlice.DBAction({ verb: 'DELETE', record: selectedMeter })); + if (conf) remoteMeterController.DBAction('DELETE',selectedMeter).then(() => setRefreshTrigger(val => !val)); setShowDelete(false); }}/> { - if (conf) dispatch(RemoteXDAMeterSlice.DBAction({ verb: 'PATCH', record: remoteMeter })); + if (conf) remoteMeterController.DBAction('PATCH', remoteMeter).then(() => setRefreshTrigger(val => !val)); setShowEdit(false); }} DisableConfirm={newInstErrors.length > 0} @@ -330,9 +367,7 @@ const RemoteMeterTab = (props: IProps) => { setAssetCount(0); if (conf) { let addAssetHandle = addAssociatedAssets(selectedMeter); - addAssetHandle.then((data: number) => { - dispatch(RemoteXDAAssetSlice.Fetch()); // TODO: This doesn't properly reload the asset slice when switching tabs - }); + addAssetHandle.then(() => setRefreshTrigger(val => !val)); return () => { if (addAssetHandle != null && addAssetHandle.abort != null) { addAssetHandle.abort(); @@ -369,10 +404,11 @@ const RemoteMeterTab = (props: IProps) => { LocalAssetKey: "", RemoteAlias: "" } - dispatch(RemoteXDAMeterSlice.DBAction({ verb: "POST", record: newRemote })); + remoteMeterController.DBAction("POST", newRemote); setSelectedMeter(newRemote); // Technically, this is a race condition with setAssetCount let fetchAssetHandle = getAssociatedAssetCount(newRemote); fetchAssetHandle.then((data: number) => { + setRefreshTrigger(val => !val); setAssetCount(data); setShowLoading(false); }); From fb819960da851726751fe3811374a45b259970c4 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 4 Aug 2026 12:01:15 -0400 Subject: [PATCH 11/11] swap error alerts for error states to be exposed later --- .../TSX/SystemCenter/Asset/AssetLocation.tsx | 15 +++++++++++---- .../TSX/SystemCenter/Asset/AssetMeter.tsx | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) 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 2630756e8d..ffb92b8f8c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx @@ -50,6 +50,9 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element const [newLocation, setNewLocation] = React.useState(); const [hover, setHover] = React.useState(undefined); const [showModal, setShowModal] = React.useState(false); + const [deleteStatus, setDeleteStatus] = React.useState('idle'); + const [createStatus, setCreateStatus] = React.useState('idle'); + const [errorObject, setErrorObject] = React.useState<{ Message: string | null, Code: number | null }>({Message: null, Code: null}) const roles = useAppSelector(SelectRoles); function hasPermissions(): boolean { @@ -117,6 +120,7 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element }, [props.Asset.ID, sortField, ascending, page, refreshTrigger]) async function deleteLocation(location: OpenXDA.Types.Location) { + setDeleteStatus('loading'); return $.ajax({ type: "DELETE", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Location/${location.ID}`, @@ -126,13 +130,15 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element async: true }).done(() => { setRefreshTrigger(val => !val); + setDeleteStatus('idle'); }).fail((msg) => { - if (msg.status == 500) - alert(msg.responseJSON.ExceptionMessage) + setDeleteStatus('error'); + setErrorObject({Message: msg.responseJSON.ExceptionMessage, Code: msg.status}) }); } async function addLocation() { + setCreateStatus('loading'); return $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Location/${newLocation.ID}`, @@ -141,10 +147,11 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element cache: true, async: true }).done(() => { + setCreateStatus('idle'); setRefreshTrigger(val => !val); }).fail((msg) => { - if (msg.status == 500) - alert(msg.responseJSON.ExceptionMessage) + setCreateStatus('error'); + setErrorObject({ Message: msg.responseJSON.ExceptionMessage, Code: msg.status }) }); } 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 87b5aaf580..a070caa40a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; +import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { DefaultSelects } from '@gpa-gemstone/common-pages'; @@ -51,6 +51,9 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ const mStatus = useAppSelector(ByMeterSlice.Status); const mParentID = useAppSelector(ByMeterSlice.ParentID); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [deleteStatus, setDeleteStatus] = React.useState('idle'); + const [createStatus, setCreateStatus] = React.useState('idle'); + const [errorObject, setErrorObject] = React.useState<{ Message: string | null, Code: number | null }>({ Message: null, Code: null }) const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -84,6 +87,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ }, [props.Asset, refreshTrigger, sortField, ascending, page]); function addMeter(meterID: number) { + setCreateStatus('loading'); return $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meter/${meterID}`, @@ -92,10 +96,11 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ cache: true, async: true }).done(record => { + setCreateStatus('idle') setRefreshTrigger(val => !val); }).fail((msg) => { - if (msg.status == 500) - alert(msg.responseJSON.ExceptionMessage) + setCreateStatus('error') + setErrorObject({ Message: msg.responseJSON.ExceptionMessage, Code: msg.status }) }); } @@ -153,6 +158,7 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ } async function deleteMeter(meter: OpenXDA.Types.Meter) { + setDeleteStatus('loading'); return $.ajax({ type: "DELETE", url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meter/${meter.ID}`, @@ -162,9 +168,10 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ async: true }).done(() => { setRefreshTrigger(val => !val); + setDeleteStatus('idle'); }).fail((msg) => { - if (msg.status == 500) - alert(msg.responseJSON.ExceptionMessage) + setDeleteStatus('error'); + setErrorObject({ Message: msg.responseJSON.ExceptionMessage, Code: msg.status }) }); }