From 5d7ad00d33099d9d51b0d48340059f3eed2d4045 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 11 May 2026 15:38:07 -0400 Subject: [PATCH 01/42] paginate substation images --- .../OpenXDA/OpenXDALocationController.cs | 31 +++++++++++++++++-- .../SystemCenter/Location/LocationImages.tsx | 21 +++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs index 5dbb1164a..1f42556c7 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/OpenXDALocationController.cs @@ -301,8 +301,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 { @@ -315,7 +315,12 @@ public IHttpActionResult GetImagesForLocation(int locationID) if (path == null) return BadRequest("ImageDirectory.Path not set in settings table."); if (Directory.Exists(Path.Combine(path, key))) - return Ok(Directory.GetFiles(Path.Combine(path, key)).Select(fp => new FileInfo(fp).Name)); + { + IEnumerable imagePaths = Directory.GetFiles(Path.Combine(path, key)).Select(fp => new FileInfo(fp).Name); + return Ok(PageImagePaths(imagePaths, page)); + } + + else return Ok(new string[] { }); } @@ -326,7 +331,27 @@ public IHttpActionResult GetImagesForLocation(int locationID) { return InternalServerError(ex); } + } + + +public static PagedResults PageImagePaths(IEnumerable imagePaths, int page) + { + int recordsPerPage = 48; + int totalImages = imagePaths.Count(); + + IEnumerable pagedImagePaths = imagePaths + .OrderBy(fp => fp) + .Skip((page) * recordsPerPage) + .Take(recordsPerPage); + + return new PagedResults() + { + Data = JsonConvert.SerializeObject(pagedImagePaths), + TotalRecords = totalImages, + NumberOfPages = (totalImages + recordsPerPage - 1) / recordsPerPage, + RecordsPerPage = recordsPerPage + }; } [HttpGet, Route("{locationID:int}/Images/{file}")] diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx index c1b6ceae9..f4cf00f0b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationImages.tsx @@ -26,25 +26,32 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA } from '@gpa-gemstone/application-typings'; import { Modal, LayoutGrid } from '@gpa-gemstone/react-interactive'; +import { Paging } from '@gpa-gemstone/react-table' declare var homePath: string; const LocationImagesWindow = (props: { Location: OpenXDA.Types.Location }) => { const [images, setImages] = React.useState([]); const [image, setImage] = React.useState(''); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(1) React.useEffect(() => { let handle = getImages(); - handle.done(i => {setImages(i)}); + handle.done(i => { + setImages(JSON.parse(i.Data)) + setTotalPages(i.NumberOfPages) + } + ); return () => { if (handle.abort != undefined) handle.abort(); }; - }, [props.Location.ID]); + }, [props.Location.ID, page]); function getImages(): JQuery.jqXHR { return $.ajax({ type: "GET", - url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images`, + url: `${homePath}api/OpenXDA/Location/${props.Location.ID}/Images/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, @@ -84,6 +91,14 @@ const LocationImagesWindow = (props: { Location: OpenXDA.Types.Location }) => { } +
+ { setPage(page - 1) }} + Current={page + 1} + Total={totalPages} + > + +
0} ShowCancel={false} ShowX={true} ShowConfirm={false} Title={image} CallBack={() => setImage('') }> From 5f053a0678e30c5a816bb0b6c44e1b77c6a11547 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 19 May 2026 12:29:27 -0400 Subject: [PATCH 02/42] reset to first page on sort --- .../Scripts/TSX/SystemCenter/Settings/Setting.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx index da7c5750f..94c66314a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx @@ -147,14 +147,7 @@ function Setting(props: IProps) { Data={data} SortKey={sortField as string} Ascending={ascending} - OnSort={(d) => { - if (d.colField === sortField) - setAscending(!ascending); - else { - setAscending(true); - setSortField(d.colField); - } - }} + OnSort={sort} OnClick={(item) => { setEditNewSetting(item.row); setShowModal(true); setEditNew('Edit'); }} TheadStyle={{ fontSize: 'smaller' }} TbodyStyle={{ display: 'block', overflowY: 'scroll', maxHeight: window.innerHeight - 300, width: '100%' }} From 48548ef10c31d4b93d7f28b1f3854c5963394425 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 19 May 2026 14:00:07 -0400 Subject: [PATCH 03/42] reset to first page on sort --- .../AdditionalFields/ByAdditionalField.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 b0dffaacf..b30c90945 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AdditionalFields/ByAdditionalField.tsx @@ -47,10 +47,8 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { const dispatch = useAppDispatch(); const data = useAppSelector(AdditionalFieldsSlice.SearchResults); - const status = useAppSelector(AdditionalFieldsSlice.SearchStatus); + const status = useAppSelector(AdditionalFieldsSlice.PagedStatus); const search = useAppSelector(AdditionalFieldsSlice.SearchFilters); - const sortField = useAppSelector(AdditionalFieldsSlice.SortField); - const ascending = useAppSelector(AdditionalFieldsSlice.Ascending); const parentID = useAppSelector(AdditionalFieldsSlice.ParentID); const currentPage = useAppSelector(AdditionalFieldsSlice.CurrentPage); const totalPages = useAppSelector(AdditionalFieldsSlice.TotalPages); @@ -59,6 +57,9 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { const valueListGroupData = useAppSelector(ValueListGroupSlice.Data); const valueListGroupStatus = useAppSelector(ValueListGroupSlice.Status); + + const [sortField, setSortField] = React.useState("FieldName") + const [ascending, setAscending] = React.useState(false) const [errors, setErrors] = React.useState([]); const [warnings, setWarnings] = React.useState([]); const [mode, setMode] = React.useState<'View' | 'Add' | 'Edit'>('View'); @@ -122,6 +123,21 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField, ascending, page: page - 1})) }, [search, sortField, ascending]) + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(AdditionalFieldsSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) + }, [search, ascending, sortField]) + useBoundPaging(currentPage, totalPages, setPage) return ( @@ -162,10 +178,7 @@ const ByAdditionalField: Application.Types.iByComponent = (props) => { Data={data} SortKey={sortField as string} Ascending={ascending} - OnSort={(d) => { - if (d.colKey === null) return; - dispatch(AdditionalFieldsSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + OnSort={sort} OnClick={(item) => { setRecord(item.row); setMode('Edit'); }} TableStyle={{ padding: 0, width: 'calc(100%)', height: 'calc(100% - 16px)', From 2e0f3b613534f0f54439ec54f14f807eb8e76015 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 19 May 2026 14:05:20 -0400 Subject: [PATCH 04/42] reset to first page on sort --- .../TSX/SystemCenter/User/User/ByUser.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index 71e17eb97..04ffd0b8b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -69,10 +69,10 @@ const ByUser: Application.Types.iByComponent = (props) => { const data = useAppSelector(UserAccountSlice.SearchResults); const userStatus: Application.Types.Status = useAppSelector(UserAccountSlice.Status); - const searchStatus: Application.Types.Status = useAppSelector(UserAccountSlice.SearchStatus); + const searchStatus: Application.Types.Status = useAppSelector(UserAccountSlice.PagedStatus); - const sortField = useAppSelector(UserAccountSlice.SortField) - const ascending = useAppSelector(UserAccountSlice.Ascending) + const [sortField, setSortField] = React.useState("DisplayName"); + const [ascending, setAscending] = React.useState(false); const currentPage = useAppSelector(UserAccountSlice.CurrentPage); const totalPages = useAppSelector(UserAccountSlice.TotalPages); const totalRecords = useAppSelector(UserAccountSlice.TotalRecords); @@ -130,6 +130,21 @@ const ByUser: Application.Types.iByComponent = (props) => { useBoundPaging(currentPage, totalPages, setPage) + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) + }, [search, ascending, sortField]) + React.useEffect(() => { function ConvertType(type: string) { if (type === 'string' || type === 'integer' || type === 'number' || type === 'datetime' || type === 'boolean') @@ -191,9 +206,7 @@ const ByUser: Application.Types.iByComponent = (props) => { Data={data} SortKey={sortField} Ascending={ascending} - OnSort={(d) => { - dispatch(UserAccountSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + OnSort={sort} OnClick={(d) => navigate(`${homePath}index.cshtml?name=User&UserAccountID=${d.row.ID}`)} TableStyle={{ padding: 0, width: '100%', height: '100%', From 7ffabc42e9721798e3be6b6307d55579080a8840 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 19 May 2026 14:15:28 -0400 Subject: [PATCH 05/42] reset to first page on sort --- .../User/UserGroup/ByUserGroup.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) 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 e87c5a9b6..0bf745fe7 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 @@ -55,8 +55,8 @@ const ByUser: Application.Types.iByComponent = (props) => { const [status, setStatus] = React.useState('uninitiated'); const searchStatus = useSelector(SecurityGroupSlice.SearchStatus); - const sortField = useSelector(SecurityGroupSlice.SortField); - const ascending = useSelector(SecurityGroupSlice.Ascending); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [groupError, setGroupError] = React.useState([]); @@ -87,6 +87,21 @@ const ByUser: Application.Types.iByComponent = (props) => { dispatch(SecurityGroupSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) }, [sortField, ascending, search]) + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(SecurityGroupSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) + }, [search, ascending, sortField]) + if (pageStatus === 'error') return
@@ -124,9 +139,7 @@ const ByUser: Application.Types.iByComponent = (props) => { Data={data} SortKey={sortField} Ascending={ascending} - OnSort={(d) => { - dispatch(SecurityGroupSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + OnSort={sort} OnClick={(d) => navigate(`${homePath}index.cshtml?name=Group&GroupID=${d.row.ID}`)} TableStyle={{ padding: 0, width: '100%', height: '100%', From 547f6bb8aea5a3e2449e0dac76220b9a6b4fef4e Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 19 May 2026 14:49:03 -0400 Subject: [PATCH 06/42] reset to first page on sort --- .../ChannelGroup/ChannelGroupItem.tsx | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) 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 318cddded..161c6cf2f 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%' }} From be7fa982d2c18e65f0dd99b0286d696b86724f69 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 22 May 2026 15:51:40 -0400 Subject: [PATCH 07/42] fix styling for paging --- .../TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 dbecd776e..aeb1b2f0f 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx @@ -88,7 +88,7 @@ export default function ValueListGroupItems(props: IProps) { }, [props.Record?.Name]); return ( -
+
@@ -96,8 +96,9 @@ export default function ValueListGroupItems(props: IProps) {
-
-
+
+
+
TableClass="table table-hover" Data={data} From b3e9e372738bc5ed3da807d62bc7b6d18b49b730 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 26 May 2026 11:23:40 -0400 Subject: [PATCH 08/42] paginate value list group item --- .../ValueListGroup/ValueListGroupItem.tsx | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) 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 aeb1b2f0f..344d9898c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.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 { ValueListSlice } from '../Store/Store'; import ValueListForm from './ValueListForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { Modal } from '@gpa-gemstone/react-interactive'; import { ValueListItemDelete, RequiredValueLists } from './ValueListGroupDelete'; @@ -40,11 +40,11 @@ interface IProps { export default function ValueListGroupItems(props: IProps) { const dispatch = useAppDispatch(); - const data = useAppSelector(ValueListSlice.Data); - const sortKey = useAppSelector(ValueListSlice.SortField); - const asc = useAppSelector(ValueListSlice.Ascending); + const data = useAppSelector(ValueListSlice.SearchResults); const status = useAppSelector(ValueListSlice.Status); - const parentID= useAppSelector(ValueListSlice.ParentID); + const parentID = useAppSelector(ValueListSlice.ParentID); + const currentPage = useAppSelector(ValueListSlice.CurrentPage); + const totalPages = useAppSelector(ValueListSlice.TotalPages); const emptyRecord: SystemCenter.Types.ValueListItem = { ID: 0, GroupID: parentID as number, Value: '', AltValue: null, SortOrder: 0 }; const [record, setRecord] = React.useState(emptyRecord); @@ -54,6 +54,8 @@ export default function ValueListGroupItems(props: IProps) { const [countDictionary, setCountDictionary] = React.useState<{ [key: string]: number }>({}); const [hover, setHover] = React.useState(''); + const [ascending, setAscending] = React.useState(true); + const [sortField, setSortField] = React.useState('SortOrder') const disallowReason = React.useCallback((ID: string) => { if (!RequiredValueLists.includes(props.Record?.Name)) @@ -67,8 +69,8 @@ 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)); + if (status == 'uninitiated' || status == 'changed') + dispatch(ValueListSlice.PagedSearch({ filter: [], sortField: 'SortOrder', ascending: true, page: 0 })); }, [status, parentID, props.Record.ID]); React.useEffect(() => { @@ -87,6 +89,29 @@ export default function ValueListGroupItems(props: IProps) { return () => { if (h?.abort != null) h.abort(); } }, [props.Record?.Name]); + const setPage = React.useCallback((page) => { + dispatch(ValueListSlice.PagedSearch({ filter: [], sortField: sortField, 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(!asc) + asc = !asc + } + else { + setSortField(d.colField) + setAscending(true) + asc = true + } + dispatch(ValueListSlice.PagedSearch({ filter: [], sortField: sort, ascending: asc, page: 0 })) + }, [ascending, sortField]) + return (
@@ -102,13 +127,9 @@ export default function ValueListGroupItems(props: IProps) { 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 })); - }} + 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%' }} @@ -181,6 +202,16 @@ export default function ValueListGroupItems(props: IProps) {
+
+
+
+ +
+
From ce2a90b03d4fa36c238b30369c4295c3ce2916e2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 27 May 2026 14:24:30 -0400 Subject: [PATCH 09/42] page asset ConnectedChannels endpoint --- .../OpenXDA/Assets/OpenXDAAssetController.cs | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index 5e9edd262..d920dabc7 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -575,13 +575,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 +596,51 @@ public IHttpActionResult GetAssetChannels(int assetID) .QueryRecordsWhere($"ID in ({string.Join(", ", connectedChannels.Select(channels => channels.ID))})") .DistinctBy(c => c.ID); - return Ok(uniqueChannels); + return uniqueChannels; } else { - return Ok(new List()); + return new List(); } + } - } catch (Exception ex) + } + + [HttpPost, Route("{assetID:int}/ConnectedChannels/{page:int}")] + public IHttpActionResult GetPagedList([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) { - return InternalServerError(ex); - } - } - else + if (!GetAuthCheck()) return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + IEnumerable uniqueChannels = GetAssetChannels(assetID); + + int totalRecords = uniqueChannels.Count(); + + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + + if (postData.OrderBy == "Phase" || postData.OrderBy == "MeasurementType") + bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + if (postData.Ascending) + uniqueChannels = uniqueChannels.OrderBy(item => item.GetType().GetProperty(postData.OrderBy, bindingFlags).GetValue(item)); + else + uniqueChannels = uniqueChannels.OrderByDescending(item => item.GetType().GetProperty(postData.OrderBy, bindingFlags).GetValue(item)); + + uniqueChannels = uniqueChannels + .Skip(recordsPerPage * page) + .Take(recordsPerPage); + + PagedResults pagedResults = new PagedResults() + { + Data = JsonConvert.SerializeObject(uniqueChannels), + RecordsPerPage = recordsPerPage, + TotalRecords = totalRecords, + NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage + }; + + return Ok(pagedResults); } From d0738132ab2df7b9171f7178e3929a9c74ef81c5 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 27 May 2026 14:26:15 -0400 Subject: [PATCH 10/42] page Asset Channels table --- .../TSX/SystemCenter/Asset/AssetChannel.tsx | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) 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 61ae1afcf..2842e50fa 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { PhaseSlice, MeasurmentTypeSlice } from '../Store/Store' -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useAppSelector } from '../hooks'; import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; @@ -73,6 +73,8 @@ const AssetChannelWindow = (props: IProps) => { const [status, setStatus] = React.useState('idle'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); React.useEffect(() => { let channelHandle = getChannels(); @@ -83,32 +85,29 @@ const AssetChannelWindow = (props: IProps) => { if (channelHandle != null && channelHandle.abort != null) channelHandle.abort(); } - }, [props.ID]); + }, [props.ID, ascending, page, sortField]); function getChannels(): JQuery.jqXHR { setStatus('loading'); return $.ajax( { - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/ConnectedChannels/${page}`, contentType: "application/json; charset=utf-A", dataType: 'json', cache: true, - async: true + async: true, + data: JSON.stringify({OrderBy: sortField, Ascending: ascending, Searches: []}) } ).done( - (d: Array) => { - const sortedChannels = sortData(sortField, ascending, d); - setAssetChannels(sortedChannels) + (d) => { + setAssetChannels(JSON.parse(d.Data)) + setTotalPages(d.NumberOfPages) setStatus('idle'); } ).fail(() => setStatus('error')); } - function sortData(key: keyof ChannelDetail, ascending: boolean, data: ChannelDetail[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - if (status == 'error' || pStatus == 'error' || mtStatus == 'error') return
@@ -155,22 +154,21 @@ const AssetChannelWindow = (props: IProps) => {
+
+
TableClass="table table-hover" Data={assetChannels} SortKey={sortField} Ascending={ascending} OnSort={(d) => { + setPage(0) if (d.colKey == sortField) { setAscending(!ascending); - const ordered = _.orderBy(assetChannels, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetChannels(ordered); } else { setAscending(true); setSortField(d.colField); - const ordered = _.orderBy(assetChannels, [d.colKey], ["asc"]); - setAssetChannels(ordered); } }} TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} @@ -244,6 +242,17 @@ const AssetChannelWindow = (props: IProps) => {
+
+
+
+ { setPage(p - 1) }} + Total={totalPages} + /> +
+
+
); } From 5ea261c906cf3579f7eefa19413104dbf51a027e Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 28 May 2026 12:02:59 -0400 Subject: [PATCH 11/42] paginate tables tab of external db --- .../ExternalDB/ExternalDBTables.tsx | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 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 06441b402..df53e565c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTables.tsx @@ -25,10 +25,10 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from "react-router-dom"; import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useAppDispatch, useBoundPaging } from '../hooks'; import { ExternalDBTablesSlice } from '../Store/Store'; import ExternalDBTableForm from './ExternalDBTableForm'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { Modal, Warning } from '@gpa-gemstone/react-interactive'; @@ -36,17 +36,20 @@ export default function ExternalDBTables(props: { ID: number }) { let navigate = useNavigate(); const dispatch = useAppDispatch(); - const data = useAppSelector(ExternalDBTablesSlice.Data); - const sortKey = useAppSelector(ExternalDBTablesSlice.SortField); - const asc = useAppSelector(ExternalDBTablesSlice.Ascending); + const data = useAppSelector(ExternalDBTablesSlice.SearchResults); const status = useAppSelector(ExternalDBTablesSlice.Status); const parentID = useAppSelector(ExternalDBTablesSlice.ParentID); + const currentPage = useAppSelector(ExternalDBTablesSlice.CurrentPage); + const totalPages = useAppSelector(ExternalDBTablesSlice.TotalPages); const emptyRecord: SystemCenter.Types.extDBTables = { ID: 0, TableName: '', ExtDBID: 0, Query: '' }; const [record, setRecord] = React.useState(emptyRecord); const [showWarning, setShowWarning] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); + const [sortField, setSortField] = React.useState('ID'); + const [ascending, setAscending] = React.useState(true); + React.useEffect(() => { if (status == 'uninitiated' || status == 'changed' || parentID != props.ID) @@ -75,6 +78,27 @@ export default function ExternalDBTables(props: { ID: number }) { navigate(`${homePath}index.cshtml?name=ExternalTable&ID=${item.row.ID}`); } + const setPage = React.useCallback((page) => { + dispatch(ExternalDBTablesSlice.PagedSearch({ filter: [], sortField, ascending, page: page - 1 })) + }, [sortField, ascending]) + + const sort = React.useCallback((d) => { + let asc = ascending + let sort = d.colField + if (sortField === d.colField) { + setAscending(!ascending) + asc = !ascending + } + else { + setSortField(d.colField) + setAscending(false) + asc = false + } + dispatch(ExternalDBTablesSlice.PagedSearch({ filter: [], sortField: sort, ascending: asc, page: 0 })) + }, [ascending, sortField]) + + useBoundPaging(currentPage, totalPages, setPage) + return (
@@ -93,12 +117,9 @@ export default function ExternalDBTables(props: { ID: number }) { TableClass="table table-hover" Data={data} - SortKey={sortKey.toString()} - Ascending={asc} - OnSort={(d) => { - if (d.colKey == 'btns') return; - dispatch(ExternalDBTablesSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} + SortKey={sortField} + Ascending={ascending} + OnSort={sort} OnClick={handleSelect} TableStyle={{ padding: 0, width: '100%', height: '100%', tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' }} TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} @@ -141,6 +162,15 @@ export default function ExternalDBTables(props: { ID: number }) {
+
+
+ +
+
From 49b25b4af8b8cafc4c4bc0fc88448adcc64fbdac Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 5 Jun 2026 13:16:38 -0400 Subject: [PATCH 12/42] Fix error filtering users by additional fields --- .../wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index 04ffd0b8b..465df6ef8 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -152,7 +152,7 @@ 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) From 9c8d49ec3b643b66d6d0be99514440d0ed0c1350 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 5 Jun 2026 13:23:50 -0400 Subject: [PATCH 13/42] Refactor byUser page from slices to local state with Generic Controller --- .../TSX/SystemCenter/User/User/ByUser.tsx | 139 ++++++++++-------- 1 file changed, 80 insertions(+), 59 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index 465df6ef8..c88465c28 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -23,13 +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 { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; import UserForm from './UserForm'; -import { useAppDispatch, useAppSelector, useBoundPaging } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { ValueListSlice, ValueListGroupSlice, UserAdditionalFieldSlice, UserAccountSlice } from '../../Store/Store'; import { IUserAccount } from '../Types'; import moment from 'moment'; @@ -62,34 +60,40 @@ const newAcct: IUserAccount = { } const ByUser: Application.Types.iByComponent = (props) => { + + const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true), []) + const userAdditionalFieldController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/AdditionalUserField`, "User"), []) + const valueListController = React.useMemo(() => new GenericController(`${homePath}api/ValueList`, 'SortOrder'), []) + const valueListGroupController = React.useMemo(() => new GenericController(`${homePath}api/ValueListGroup`, 'Name'), []) + let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const search = useAppSelector(UserAccountSlice.SearchFilters); + const [filters, setFilters] = React.useState[]>([]); - const data = useAppSelector(UserAccountSlice.SearchResults); - const userStatus: Application.Types.Status = useAppSelector(UserAccountSlice.Status); - const searchStatus: Application.Types.Status = useAppSelector(UserAccountSlice.PagedStatus); + const [users, setUsers] = React.useState([]); + const [userStatus, setUserStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState("DisplayName"); const [ascending, setAscending] = React.useState(false); - const currentPage = useAppSelector(UserAccountSlice.CurrentPage); - const totalPages = useAppSelector(UserAccountSlice.TotalPages); - const totalRecords = useAppSelector(UserAccountSlice.TotalRecords); - const adlFields: Application.Types.iAdditionalUserField[] = useAppSelector(UserAdditionalFieldSlice.Fields) - const adlFieldStatus: Application.Types.Status = useAppSelector(UserAdditionalFieldSlice.FieldStatus) + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + + const [adlFields, setAdlFields] = React.useState([]) + const [adlFieldStatus, setAdlFieldStatus] = React.useState('uninitiated'); const [filterableList, setFilterableList] = React.useState[]>(defaultSearchcols); const [showModal, setShowModal] = React.useState(false); const [userError, setUserError] = React.useState([]); - const valueListItems: SystemCenter.Types.ValueListItem[] = useAppSelector(ValueListSlice.Data); - const valueListItemStatus: Application.Types.Status = useAppSelector(ValueListSlice.Status); + const [valueListItems, setValueListItems] = React.useState([]); + const [valueListItemStatus, setValueListItemStatus] = React.useState('uninitiated'); - const valueListGroups: SystemCenter.Types.ValueListGroup[] = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus: Application.Types.Status = useAppSelector(ValueListGroupSlice.Status); + const [valueListGroups, setValueListGroups] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); const [act, setAct] = React.useState(newAcct) @@ -105,46 +109,60 @@ const ByUser: Application.Types.iByComponent = (props) => { }, [userStatus, adlFieldStatus, valueListItemStatus, valueListGroupStatus]) React.useEffect(() => { - if (searchStatus === 'uninitiated' || searchStatus === 'changed') - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: currentPage})); - }, [searchStatus, search, sortField, ascending, currentPage]); + setUserStatus('loading') + const h = userAccountController.PagedSearch(filters, "DisplayName", ascending, page); // TODO remove hard code displayname -- what's with the type error? + h.done((d) => { + setUsers(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setRecordsPerPage(d.RecordsPerPage) + setTotalRecords(d.TotalRecords) + if (d.NumberOfPages <= page) + setPage(d.NumberOfPages > 0 ? d.NumberOfPages - 1 : 0) + setUserStatus('idle') + }).fail(() => setUserStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); + } + }, [filters, sortField, ascending, page]); + React.useEffect(() => { - if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') - dispatch(UserAdditionalFieldSlice.FetchField()); + if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') { + const h = userAdditionalFieldController.Fetch(); + h.done((d) => { + setAdlFields(d as unknown as Application.Types.iAdditionalUserField[]) + setAdlFieldStatus('idle') + }).fail((d) => { + setAdlFieldStatus('error') + }) + } }, [adlFieldStatus]); - + React.useEffect(() => { - if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') - dispatch(ValueListSlice.Fetch()); + if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') { + const h = valueListController.Fetch(); + h.done((d) => { + setValueListItems(d as unknown as SystemCenter.Types.ValueListItem[]) + setValueListItemStatus('idle') + }).fail((d) => { + setValueListItemStatus('error') + }) + } }, [valueListItemStatus]); + React.useEffect(() => { - if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); - - const setPage = React.useCallback((page) => { - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) - - useBoundPaging(currentPage, totalPages, setPage) - - const sort = React.useCallback((d) => { - let asc = ascending - let sort = d.colField - if (sortField === d.colField) { - setAscending(!ascending) - asc = !ascending + if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') { + const h = valueListGroupController.Fetch(); + h.done((d) => { + setValueListGroups(d as unknown as SystemCenter.Types.ValueListGroup[]) + setValueListGroupStatus('idle') + }).fail((d) => { + setValueListGroupStatus('error') + }) } - else { - setSortField(d.colField) - setAscending(false) - asc = false - } - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) - }, [search, ascending, sortField]) - + }, [valueListGroupStatus]); + React.useEffect(() => { function ConvertType(type: string) { if (type === 'string' || type === 'integer' || type === 'number' || type === 'datetime' || type === 'boolean') @@ -158,10 +176,6 @@ const ByUser: Application.Types.iByComponent = (props) => { 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
@@ -172,7 +186,7 @@ const ByUser: Application.Types.iByComponent = (props) => { 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' : 'Found ' + totalRecords + ' User Account(s)'} StorageID="UsersFilter" GetEnum={(setOptions, field) => { @@ -203,10 +217,17 @@ const ByUser: Application.Types.iByComponent = (props) => {
TableClass="table table-hover" - Data={data} + Data={users} SortKey={sortField} Ascending={ascending} - OnSort={sort} + OnSort={(d) => { + 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={{ padding: 0, width: '100%', height: '100%', @@ -272,8 +293,8 @@ const ByUser: Application.Types.iByComponent = (props) => {
setPage(page - 1)} Total={totalPages} />
@@ -281,7 +302,7 @@ const ByUser: Application.Types.iByComponent = (props) => { { if (confirm) - dispatch(UserAccountSlice.DBAction({ verb: 'POST', record: act })) + userAccountController.DBAction('POST', act) setAct(newAcct); setShowModal(false); }} From 7824d309037cbc7a9fa0d764165d6439aa3cd055 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 5 Jun 2026 15:58:16 -0400 Subject: [PATCH 14/42] clean up byUser --- .../TSX/SystemCenter/User/User/ByUser.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index c88465c28..df1afcf20 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -108,9 +108,9 @@ const ByUser: Application.Types.iByComponent = (props) => { setPageStatus('idle'); }, [userStatus, adlFieldStatus, valueListItemStatus, valueListGroupStatus]) - React.useEffect(() => { + const pagedSearch = React.useCallback(() => { setUserStatus('loading') - const h = userAccountController.PagedSearch(filters, "DisplayName", ascending, page); // TODO remove hard code displayname -- what's with the type error? + const h = userAccountController.PagedSearch(filters, sortField as "DisplayName", ascending, page); h.done((d) => { setUsers(JSON.parse(d.Data as unknown as string)) setTotalPages(d.NumberOfPages) @@ -123,8 +123,16 @@ const ByUser: Application.Types.iByComponent = (props) => { return () => { if (h.abort != undefined) h.abort(); } - }, [filters, sortField, ascending, page]); + }, [filters, sortField, ascending, page, userAccountController.PagedSearch]) + + React.useEffect(() => { + pagedSearch() + }, [filters, sortField, ascending, page, pagedSearch]); + React.useEffect(() => { + if (userStatus === 'uninitiated' || userStatus === 'changed') + pagedSearch() + }, [userStatus, pagedSearch]) React.useEffect(() => { if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') { @@ -186,7 +194,7 @@ const ByUser: Application.Types.iByComponent = (props) => { CollumnList={filterableList} SetFilter={setFilters} Direction={'left'} defaultCollumn={{ label: 'Username', key: 'DisplayName', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'} - ShowLoading={userStatus === 'loading'} ResultNote={userStatus === 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' User Account(s)'} + ShowLoading={userStatus === 'loading'} ResultNote={'Displaying User(s) ' + (totalRecords > 0 ? (recordsPerPage * page + 1) : 0) + ' - ' + (recordsPerPage * page + users.length) + ' out of ' + totalRecords} StorageID="UsersFilter" GetEnum={(setOptions, field) => { @@ -214,7 +222,7 @@ const ByUser: Application.Types.iByComponent = (props) => {
-
+
TableClass="table table-hover" Data={users} @@ -301,8 +309,10 @@ const ByUser: Application.Types.iByComponent = (props) => {
{ - if (confirm) + if (confirm) { userAccountController.DBAction('POST', act) + setUserStatus('changed'); + } setAct(newAcct); setShowModal(false); }} From 017f36ab191afd5d4a2b31013ac25e170ebc781f Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 5 Jun 2026 16:04:01 -0400 Subject: [PATCH 15/42] move ByUserGroup from GenericSlice to GenericController --- .../User/UserGroup/ByUserGroup.tsx | 115 ++++++++---------- 1 file changed, 48 insertions(+), 67 deletions(-) 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 0bf745fe7..c4ab7005b 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,78 +41,53 @@ const emptyGroup: ISecurityGroup = { Name: "", CreatedBy: "", CreatedOn: new Dat const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - - const search = useSelector(SecurityGroupSlice.SearchFilters); - const data = useSelector(SecurityGroupSlice.SearchResults); - const currentPage = useSelector(SecurityGroupSlice.CurrentPage); - const totalPages = useSelector(SecurityGroupSlice.TotalPages); - const totalRecords = useSelector(SecurityGroupSlice.TotalRecords); + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName"),[]) + const [filters, setFilters] = React.useState[]>([]); + const [securityGroups, setSecurityGroups] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); const [status, setStatus] = React.useState('uninitiated'); - const searchStatus = useSelector(SecurityGroupSlice.SearchStatus); - + const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [sortField, setSortField] = React.useState('DisplayName'); const [ascending, setAscending] = React.useState(false); - 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]) - - const sort = React.useCallback((d) => { - let asc = ascending - let sort = d.colField - if (sortField === d.colField) { - setAscending(!ascending) - asc = !ascending + const pagedSearch = React.useCallback(() => { + const h = securityGroupController.PagedSearch(filters, sortField as "DisplayName", ascending, currentPage) + h.done((d) => { + setSecurityGroups(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages) + setRecordsPerPage(d.RecordsPerPage) + setTotalRecords(d.TotalRecords) + if (d.NumberOfPages <= currentPage) + setCurrentPage(d.NumberOfPages > 0 ? d.NumberOfPages - 1 : 0) + setStatus('idle') + }).fail(() => setStatus('error')) + return () => { + if (h.abort != undefined) h.abort(); } - else { - setSortField(d.colField) - setAscending(false) - asc = false - } - dispatch(SecurityGroupSlice.PagedSearch({ filter: search, sortField: sort, ascending: asc, page: 0 })) - }, [search, ascending, sortField]) + }, [sortField, ascending, filters, currentPage, securityGroupController.PagedSearch]) - if (pageStatus === 'error') - return
- -
; + React.useEffect(() => { + return pagedSearch() + }, [sortField, ascending, filters, currentPage, pagedSearch]) - useBoundPaging(currentPage, totalPages, setPage) + React.useEffect(() => { + if (status === "uninitiated" || status === "changed") + return pagedSearch() + }, [status, pagedSearch]) 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 () => { } @@ -136,10 +108,17 @@ const ByUser: Application.Types.iByComponent = (props) => {
TableClass="table table-hover" - Data={data} + Data={securityGroups} SortKey={sortField} Ascending={ascending} - OnSort={sort} + OnSort={(d) => { + 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={{ padding: 0, width: '100%', height: '100%', @@ -198,18 +177,20 @@ 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) } - })) + if (confirm) { + securityGroupController.DBAction( + 'POST', + { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } as ISecurityGroup + ) + setStatus('changed') + } setShowModal(false); }} ConfirmShowToolTip={groupError.length > 0} From 87e7fe39e3352c4d9ab5177d364c99bc7e197877 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 8 Jun 2026 13:26:43 -0400 Subject: [PATCH 16/42] paginate GroupUsers --- .../Model/Security/SecurityGroup.cs | 49 +++++ .../User/UserGroup/GroupUsers.tsx | 168 ++++++++++-------- 2 files changed, 139 insertions(+), 78 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs index 2906643a6..534639113 100644 --- a/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs +++ b/Source/Applications/SystemCenter/Model/Security/SecurityGroup.cs @@ -100,6 +100,55 @@ public IHttpActionResult GetUsers(string groupID) "(SELECT COUNT(ID) FROM SecurityGroupUserAccount WHERE SecurityGroupID = {0} AND UserAccountID = UserAccount.ID) > 0", groupID))); } + [HttpPost] + [Route("Users/PagedList/{groupID}/{page:int}")] + public IHttpActionResult GetPagedUsers([FromBody] PostData postData, [FromUri] String groupID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + string orderBySwitch(string orderBy) + { + String[] valid = ["Phone", "Email", "FirstName", "LastName"]; + if (valid.Contains(orderBy)) + return orderBy; + else + return "Name"; + } + + PagedResults pagedResults = new PagedResults(); + + int recordsPerPage = Take ?? 50; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $"SELECT UserAccount.*, UserAccount.Name as AccountName FROM SecurityGroupUserAccount JOIN UserAccount ON UserAccountID = UserAccount.ID WHERE SecurityGroupID = {{0}} ORDER BY {orderBySwitch(postData.OrderBy)} {(postData.Ascending ? "ASC" : "DESC")}"; + string countSql = "SELECT COUNT(*) FROM SecurityGroupUserAccount JOIN UserAccount ON UserAccountID = UserAccount.ID WHERE SecurityGroupID = {0}"; + + DataTable results = connection.RetrieveData(sql, groupID.ToString()); + int count = connection.ExecuteScalar(countSql, groupID); + + DataRow[] rows = results.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = results.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results = pagedTable; + + // paged results setting + pagedResults.Data = JsonConvert.SerializeObject(results); + pagedResults.TotalRecords = count; + pagedResults.NumberOfPages = (count + recordsPerPage - 1) / recordsPerPage; + } + + return Ok(pagedResults); + } + [HttpPost] [Route("{groupID}/PostRoles")] public IHttpActionResult PostGroupRoles([FromBody] IEnumerable record, string groupID) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx index 6149a523b..0a0f50db7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx @@ -26,25 +26,23 @@ import * as _ from 'lodash'; import { LoadingScreen } from '@gpa-gemstone/react-interactive'; import { UserAccountSliceRemote } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; -const GroupUser = (props: {Group: ISecurityGroup}) => { +const GroupUser = (props: { Group: ISecurityGroup }) => { const [showSelect, setShowSelect] = React.useState(false); const [users, setUsers] = React.useState([]); const [asc, setAsc] = React.useState(true); const [sortField, setSortField] = React.useState('AccountName'); const [status, setStatus] = React.useState('uninitiated'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); React.useEffect(() => { const handle = getUsers(); return () => { if (handle != null && handle.abort != null) handle.abort(); } - }, [props.Group.ID, props.Group.Type]) - - React.useEffect(() => { - setUsers((u) => _.orderBy(u, [sortField], asc ? 'asc' : 'desc')); - }, [asc, sortField]) + }, [props.Group.ID, props.Group.Type, asc, sortField, page]) function getUsers() { if (props.Group.Type != 'Database') @@ -52,23 +50,23 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { setStatus('loading') return $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/${props.Group.ID}`, + type: "POST", + url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/PagedList/${props.Group.ID}/${page}`, contentType: "application/json; charset=utf-8", cache: false, - async: true + async: true, + data: JSON.stringify({OrderBy: sortField, Ascending: asc}) }).done((d) => { - setUsers(_.orderBy(d, [sortField], asc ? 'asc' : 'desc')); + setTotalPages(d.NumberOfPages); + setUsers(JSON.parse(d.Data)); setStatus('idle'); - }, () => setStatus('error')); + }, () => setStatus('error')); } function saveUser(u) { if (props.Group.Type != 'Database') return; - setStatus('loading'); - return $.ajax({ type: "POST", url: `${homePath}api/SystemCenter/FullSecurityGroup/AddUser/${props.Group.ID}`, @@ -77,8 +75,7 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { cache: false, async: true }).done((d) => { - setUsers(_.orderBy(d, [sortField], asc ? 'asc' : 'desc')); - setStatus('idle'); + getUsers() }, () => setStatus('error')); } @@ -98,71 +95,86 @@ const GroupUser = (props: {Group: ISecurityGroup}) => { {props.Group.Type == 'Azure' ?
Users in an Azure Group cannot be edited in System Center. To add or remove Users, please contact your Azure Administrator.
: null} - {props.Group.Type == 'AD'?
+ {props.Group.Type == 'AD' ?
Users in an Active Directory Group cannot be edited in System Center. To add or remove Users, please contact your AD Administrator.
: null} {props.Group.Type == 'Database' ? - - TableClass="table table-hover" - Data={users} - SortKey={sortField} - Ascending={asc} - OnSort={(d) => { - if (d.colField === sortField) - setAsc(!asc); - else { - setAsc(true); - setSortField(d.colField); - } - }} - TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1, width: '100%' }} - RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'AccountName'} - AllowSort={true} - Field={'AccountName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Username - - - Key={'FirstName'} - AllowSort={true} - Field={'FirstName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > First Name - - - Key={'LastName'} - AllowSort={true} - Field={'LastName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Last Name - - - Key={'Phone'} - AllowSort={true} - Field={'Phone'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Phone - - - Key={'Email'} - AllowSort={true} - Field={'Email'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Email - - + <> +
+
+ + TableClass="table table-hover" + Data={users} + SortKey={sortField} + Ascending={asc} + OnSort={(d) => { + if (d.colField === sortField) + setAsc(!asc); + else { + setAsc(true); + setSortField(d.colField); + } + }} + TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'auto', flex: 1, width: '100%' }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + + Key={'AccountName'} + AllowSort={true} + Field={'AccountName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Username + + + Key={'FirstName'} + AllowSort={true} + Field={'FirstName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > First Name + + + Key={'LastName'} + AllowSort={true} + Field={'LastName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Last Name + + + Key={'Phone'} + AllowSort={true} + Field={'Phone'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Phone + + + Key={'Email'} + AllowSort={true} + Field={'Email'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > Email + + +
+
+
+
+ {setPage(page - 1)}} + Total={totalPages} + /> +
+
+ : null}
From e3e32ae08400e0f4d95138bdffc17f70bae4b230 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 8 Jun 2026 16:24:37 -0400 Subject: [PATCH 17/42] paginate ExternalDBTableFields --- .../ExternalDB/ExternalDBTableFields.tsx | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 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 6f193240c..9582f506e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx @@ -27,7 +27,7 @@ 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 { SelectPopup } from '@gpa-gemstone/common-pages'; @@ -73,32 +73,41 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu const [overWriteFields, setOverWriteFields] = React.useState([]); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + + const pagedSearch = React.useCallback(() => { + setTableStatus('loading'); + parentID.current = props.ID; + const handle = $.ajax({ + type: "POST", + url: `${homePath}api/SystemCenter/AdditionalFieldView/PagedList/${page}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true, + data: JSON.stringify({ Searches: [{ SearchText: props.TableName, Operator: "LIKE", IsPivotColumn: false, FieldName: "ExternalTable" }], OrderBy: sortKey, Ascending: asc }) + }); + handle.done((results) => { + setFieldsInTable(JSON.parse(results.Data)); + setTotalPages(results.NumberOfPages); + setTableStatus('idle'); + }); + handle.fail(() => { + setTableStatus('error'); + }); + }, [props.TableName, sortKey, asc, page]) + + React.useEffect(() => { + pagedSearch() + }, [pagedSearch]); + 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'); - }); + pagedSearch() } - }, [tableStatus, props.ID, status]); - - React.useEffect(() => { - sortData(fieldsInTable); - }, [sortKey, asc]); + }, [tableStatus, props.ID, status]) React.useEffect(() => { if (status === 'uninitiated' || status === 'changed') @@ -122,10 +131,6 @@ 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 } })); setRecord(emptyRecord); @@ -203,7 +208,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 @@ -212,7 +217,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 @@ -221,7 +226,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 @@ -230,7 +235,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 @@ -255,6 +260,15 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu }
+
+
+ { setPage(page - 1) }} + Total={totalPages} + /> +
+
@@ -381,7 +395,7 @@ export default function ExternalDBTableFields(props: { TableName: string, ID: nu >Key } - /> + />
From e97da4327f3c8606b4ea9b0191235e925ef6cf3d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 10:02:59 -0400 Subject: [PATCH 18/42] paginate byAssetGroup --- .../SystemCenter/AssetGroups/ByAssetGroup.tsx | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) 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 8b453ad23..3f6928ed1 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx @@ -22,7 +22,7 @@ //****************************************************************************************************** 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' @@ -31,7 +31,7 @@ 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 { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppDispatch, useAppSelector, useBoundPaging } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; declare var homePath: string; @@ -48,8 +48,6 @@ 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); @@ -57,22 +55,39 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { const assetType = useAppSelector(AssetTypeSlice.Data); const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - + const currentPage = useAppSelector(AssetGroupSlice.CurrentPage); + const totalPages = useAppSelector(AssetGroupSlice.TotalPages); const [showFilter, setFilter] = React.useState<('None' | 'Meter' | 'Asset' | 'Asset Group' | 'Station')>('None'); const [newAssetGroup, setNewAssetGroup] = React.useState(_.cloneDeep(emptyAssetGroup)); const [showNewGroup, setShowNewGroup] = React.useState(false); const [assetGrpErrors, setAssetGrpErrors] = React.useState([]); + const [sortKey, setSortKey] = React.useState('Name') + const [ascending, setAscending] = React.useState(false) + const [page, setPage] = React.useState(currentPage); React.useEffect(() => { if (status == 'changed' || status == 'uninitiated') dispatch(AssetGroupSlice.Fetch()); }, [status]); + const pagedSearch = React.useCallback((searches?: Search.IFilter[], sortfield?: keyof OpenXDA.Types.AssetGroup, asc?: boolean, page?: number) => { + const searchToUse = searches ?? searchFields; + const sortKeyToUse = sortfield ?? sortKey; + const ascendingToUse = asc ?? ascending; + const pageToUse = page ?? currentPage; + dispatch(AssetGroupSlice.PagedSearch({filter: searchToUse, sortField: sortKeyToUse, ascending: ascendingToUse, page: pageToUse })); + + }, [searchFields, currentPage, ascending, sortKey, AssetGroupSlice.PagedSearch]) + + React.useEffect(() => { + pagedSearch(undefined, sortKey, ascending, page) + }, [sortKey, ascending, page, pagedSearch]) + React.useEffect(() => { if (searchStatus == 'changed' || searchStatus == 'uninitiated') - dispatch(AssetGroupSlice.DBSearch({ filter: searchFields })); - }, [searchStatus]); + pagedSearch(); + }, [searchStatus, pagedSearch]); React.useEffect(() => { if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') @@ -261,9 +276,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 +336,15 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => {
+
+
+ {setPage(page - 1)} } + /> +
+
Date: Tue, 9 Jun 2026 11:07:37 -0400 Subject: [PATCH 19/42] paginate assetGroupSubGroups --- .../OpenXDAAssetGroupsController.cs | 34 ++- .../AssetGroups/AssetGroupAssetGroup.tsx | 202 +++++++++--------- 2 files changed, 133 insertions(+), 103 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 97c92e714..4b56581b4 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -31,17 +31,15 @@ using System; 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 { @@ -293,7 +291,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 +303,33 @@ public IHttpActionResult GetSubGroups(int assetGroupID) return Unauthorized(); } + [HttpPost, Route("{assetGroupID:int}/AssetGroups/{page:int}")] + public IHttpActionResult GetSubGroupsPaged([FromBody] PostData postData, [FromUri] int assetGroupID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID in (SELECT ChildAssetGroupID FROM AssetGroupAssetGroupView WHERE ParentAssetGroupID = {0})", assetGroupID); + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddAssetGroups")] public IHttpActionResult AddSubgroups(int assetGroupID, [FromBody] IEnumerable subGroups) { 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 9faff924a..5fc07c88d 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetGroupAssetGroup.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA } from '@gpa-gemstone/application-typings'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { AssetGroupSlice } from '../Store/Store'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { Search, Warning } from '@gpa-gemstone/react-interactive'; @@ -38,7 +38,7 @@ import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; -function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { +function AssetGroupAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); const dispatch = useAppDispatch(); const [groupList, setGroupList] = React.useState>([]); @@ -47,6 +47,8 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { const [showAdd, setShowAdd] = React.useState(false); const [counter, setCounter] = React.useState(0); const [removeGroup, setRemoveGroup] = React.useState(-1); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); @@ -65,34 +67,35 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return getData(); }, [props.AssetGroupID, counter]); + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AssetGroups/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({OrderBy: sortField, Ascending: ascending}) }); - handle.done((data: Array) => { - const sortedData = sortData(sortField, ascending, data); - setGroupList(sortedData); + handle.done((r) => { + setGroupList(JSON.parse(r.Data)); + setTotalPages(r.NumberOfPages); }); - + return function cleanup() { if (handle.abort != null) handle.abort(); } } - function sortData(key: string, ascending: boolean, data: OpenXDA.Types.AssetGroup[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getEnum(setOptions, field) { let handle = null; if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) @@ -127,7 +130,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { } function saveItems(items: OpenXDA.Types.AssetGroup[]) { - + let handle = $.ajax({ type: "POST", url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/AddAssetGroups`, @@ -151,92 +154,95 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Asset Groups in Asset Group:

+
+
+
+
+

Asset Groups in Asset Group:

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

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

+ + +
+
+
+ setPage(page - 1)} + Total={totalPages} + /> +
+
- -
-
+
@@ -270,7 +276,7 @@ function AssetGroupAssetGroupWindow(props: { AssetGroupID: number}) { -1} Title={'Remove Asset Group from Asset Group'} Message={'This will remove the Asset Group from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeGroup); setRemoveGroup(-1); }} />
- + ); } From 178ebf499f2f985f1102419aba7d2a0e19e83167 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 12:03:07 -0400 Subject: [PATCH 20/42] paginate assetGroupsAssets --- .../OpenXDAAssetGroupsController.cs | 66 ++++++ .../AssetGroups/AssetAssetGroup.tsx | 207 +++++++++--------- 2 files changed, 174 insertions(+), 99 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 4b56581b4..86ed7e86b 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Reflection; using System.Web.Http; @@ -103,6 +104,71 @@ 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; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $@"SELECT + DISTINCT + Asset.ID, + AssetAssetGroup.AssetGroupID, + Asset.AssetKey, + Asset.AssetName, + Asset.VoltageKV, + AssetType.Name as AssetType, + COUNT(DISTINCT Meter.ID) as Meters, + COUNT(DISTINCT Location.ID) as Locations + FROM + Asset Join + AssetType ON Asset.AssetTypeID = AssetType.ID LEFT JOIN + MeterAsset ON MeterAsset.AssetID = Asset.ID LEFT JOIN + Meter ON MeterAsset.MeterID = Meter.ID LEFT JOIN + AssetLocation ON AssetLocation.AssetID = Asset.ID LEFT JOIN + Location ON AssetLocation.LocationID = Location.ID LEFT JOIN + AssetAssetGroup ON Asset.ID = AssetAssetGroup.AssetID + GROUP BY + Asset.ID, + Asset.AssetKey, + Asset.AssetName, + Asset.VoltageKV, + AssetType.Name, + AssetAssetGroup.AssetGroupID + HAVING AssetAssetGroup.AssetGroupID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + "; + + DataTable records = connection.RetrieveData(sql, assetGroupID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddAssets")] public IHttpActionResult AddAssets(int assetGroupID, [FromBody] IEnumerable assets) { 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 2ca4d51c7..005bbc42f 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { AssetGroupSlice, AssetTypeSlice } from '../Store/Store'; import { SystemCenter } from '@gpa-gemstone/application-typings'; import { Warning } from '@gpa-gemstone/react-interactive'; @@ -37,7 +37,7 @@ import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; -function AssetAssetGroupWindow(props: { AssetGroupID: number}) { +function AssetAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); const [assetList, setAssetList] = React.useState>([]); const [sortKey, setSortKey] = React.useState('AssetName'); @@ -45,6 +45,8 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { const [showAdd, setShowAdd] = React.useState(false); const [counter, setCounter] = React.useState(0); const [removeAsset, setRemoveAsset] = React.useState(-1); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const assetType = useAppSelector(AssetTypeSlice.Data); const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); @@ -63,34 +65,36 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { dispatch(AssetTypeSlice.Fetch()); }, [assetTypeStatus]); + + React.useEffect(() => { + return getData(); + }, [ascending, sortKey, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Assets/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) }) - handle.done((data: Array) => { - const sortedData = sortData(sortKey, ascending, data); - setAssetList(sortedData); + handle.done((r) => { + setAssetList(JSON.parse(r.Data)); + setTotalPages(r.NumberOfPages); }); - + return function cleanup() { if (handle.abort != null) handle.abort(); } } - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedAsset[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function saveItems(items: SystemCenter.Types.DetailedAsset[]) { let handle = $.ajax({ @@ -133,99 +137,104 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Transmission Assets:

+
+
+
+
+

Transmission Assets:

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

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

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

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

-
+
{ @@ -234,7 +243,7 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number}) { saveItems(selected.filter(items => assetList.findIndex(g => g.ID == items.ID) < 0)) }} /> -1} Title={'Remove Asset from Asset Group'} Message={'This will remove the Transmission Asset from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeAsset); setRemoveAsset(-1); }} /> - + ) } From c2afd21b300a134f7df65616f9e30ab2942b79b2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 12:15:24 -0400 Subject: [PATCH 21/42] paginate assetGroupMeters --- .../OpenXDAAssetGroupsController.cs | 62 ++++++ .../AssetGroups/MeterAssetGroup.tsx | 185 +++++++++--------- 2 files changed, 158 insertions(+), 89 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs index 86ed7e86b..588449e43 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/AssetGroups/OpenXDAAssetGroupsController.cs @@ -269,6 +269,68 @@ 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; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + string sql = $@"SELECT DISTINCT + Meter.ID, + MeterAssetGroup.AssetGroupID, + Meter.AssetKey, + Meter.Name, + Meter.Make, + Meter.Model, + Location.Name as Location, + COUNT(DISTINCT MeterAsset.AssetID) as MappedAssets + FROM + Meter LEFT JOIN + Location ON Meter.LocationID = Location.ID LEFT JOIN + MeterAsset ON Meter.ID = MeterAsset.MeterID LEFT JOIN + Asset ON MeterAsset.AssetID = Asset.ID LEFT JOIN + MeterAssetGroup ON Meter.ID = MeterAssetGroup.MeterID + GROUP BY + Meter.ID, + Meter.AssetKey, + Meter.Name, + Meter.Make, + Meter.Model, + Location.Name, + MeterAssetGroup.AssetGroupID + HAVING MeterAssetGroup.AssetGroupID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + "; + + DataTable records = connection.RetrieveData(sql, assetGroupID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpPost, Route("{assetGroupID:int}/AddMeters")] public IHttpActionResult AddMeters(int assetGroupID, [FromBody] IEnumerable meters) { diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx index 3f543a15f..1a4dd831a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/MeterAssetGroup.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ByMeterSlice } from '../Store/Store'; import { SystemCenter } from '@gpa-gemstone/application-typings'; import { Search, Warning } from '@gpa-gemstone/react-interactive'; @@ -36,10 +36,9 @@ import { SelectRoles } from '../Store/UserSettings'; import { useAppDispatch, useAppSelector } from '../hooks'; import { AssetGroupSlice } from '../Store/Store'; - declare var homePath: string; -function MeterAssetGroupWindow(props: { AssetGroupID: number}) { +function MeterAssetGroupWindow(props: { AssetGroupID: number }) { let navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -50,6 +49,8 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { const [counter, setCounter] = React.useState(0); const [removeMeter, setRemoveMeter] = React.useState(-1); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -57,34 +58,36 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return getData(); }, [props.AssetGroupID, counter]) + + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + function getData() { if (props.AssetGroupID == null) return () => { }; let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Meters`, + type: "POST", + url: `${homePath}api/OpenXDA/AssetGroup/${props.AssetGroupID}/Meters/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) }); - handle.done((data: Array) => { - const sortedData = sortData(sortField, ascending, data); - setMeterList(sortedData); + handle.done((d) => { + setMeterList(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }); - + return function cleanup() { if (handle.abort != null) handle.abort(); } } - function sortData(key: string, ascending: boolean, data: SystemCenter.Types.DetailedMeter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getEnum(setOptions, field) { let handle = null; if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) @@ -174,89 +177,93 @@ function MeterAssetGroupWindow(props: { AssetGroupID: number}) { return ( <> -
-
-
-
-

Meters in Asset Group:

+
+
+
+
+

Meters in Asset Group:

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

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

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

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

-
+
Model - -1} Title={'Remove Meter from Asset Group'} Message={'This will remove the Meter from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeMeter); setRemoveMeter(-1); }} /> + -1} Title={'Remove Meter from Asset Group'} Message={'This will remove the Meter from this Asset Group.'} CallBack={(c) => { if (c) removeItem(removeMeter); setRemoveMeter(-1); }} /> ); } From b094435b288f3aab4578a070b3bdd87ba802754b Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 12:50:14 -0400 Subject: [PATCH 22/42] paginate assetSubstations --- .../OpenXDA/Assets/OpenXDAAssetController.cs | 30 +++ .../TSX/SystemCenter/Asset/AssetLocation.tsx | 222 +++++++++--------- 2 files changed, 146 insertions(+), 106 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index d920dabc7..c6361abe9 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -25,6 +25,8 @@ using System.Collections.Generic; using System.Data; using System.Diagnostics; +using Newtonsoft.Json; +using System.Reflection; using System.Linq; using System.Transactions; using System.Web.Http; @@ -65,6 +67,34 @@ public IHttpActionResult GetAssetLocations(int assetID) return Unauthorized(); } + [HttpPost, Route("{assetID:int}/Locations/{page:int}")] + public IHttpActionResult GetAssetLocationsPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = {0})", assetID); + + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + return Ok(results); + } + [HttpGet, Route("{assetID:int}/AssetLocations")] public IHttpActionResult GetAssetLocationModels(int assetID) { 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 c63fd3186..608d294dc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetLocation.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { ReactIcons } from '@gpa-gemstone/gpa-symbols' import { useAppSelector } from '../hooks'; @@ -34,15 +34,17 @@ import { ToolTip } from '@gpa-gemstone/react-forms'; declare var homePath: string; -function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ +function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element { let navigate = useNavigate(); const [locations, setLocations] = React.useState>([]); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); const [allLocations, setAllLocations] = React.useState>([]); const [newLocation, setNewLocation] = React.useState(); - const [hover, setHover] = React.useState(undefined); + const [hover, setHover] = React.useState(undefined); const [showModal, setShowModal] = React.useState(false); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); function hasPermissions(): boolean { @@ -51,6 +53,10 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element return true; } + React.useEffect(() => { + return getData(); + }, [ascending, sortField, page]) + React.useEffect(() => { getData(); }, [props.Asset]); @@ -62,22 +68,19 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element function getLocations(): void { $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Locations/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true - }).done(data => { - const sortedLocations = sortData(sortField, ascending, data); - setLocations(sortedLocations); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) + }).done(d => { + setLocations(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }); } - function sortData(key: keyof OpenXDA.Types.Location, ascending: boolean, data: OpenXDA.Types.Location[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getAllOtherLocations(): void { $.ajax({ type: "GET", @@ -139,89 +142,96 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element
-
- - TableClass="table table-hover" - Data={locations} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == sortField) { - setAscending(!ascending); - const ordered = _.orderBy(locations, [d.colKey], [(!ascending ? "asc" : "desc")]); - setLocations(ordered); - } - else { - setAscending(true); - setSortField(d.colField); - const ordered = _.orderBy(locations, [d.colKey], ["asc"]); - setLocations(ordered); - } - }} - TableStyle={{ height: '100%' }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - OnClick={handleSelect} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'Name'} - AllowSort={true} - Field={'Name'} - HeaderStyle={{ width: '30%' }} - RowStyle={{ width: '30%' }} - > Name - - - Key={'LocationKey'} - AllowSort={true} - Field={'LocationKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Key - - - Key={'Latitude'} - AllowSort={true} - Field={'Latitude'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Latitude - - - Key={'Longitude'} - AllowSort={true} - Field={'Longitude'} - HeaderStyle={{ width: '15%' }} - RowStyle={{ width: '15%' }} - > Longitude - - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => <> - - } - >

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

+ + +
+
+
+ setPage(page - 1)} + /> +
+
@@ -241,19 +251,19 @@ function AssetLocationWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element setShowModal(false) }} ConfirmText={'Save'} DisableConfirm={allLocations.length === 0}> - -
- - -
+ +
+ + +
- + ); } From 18c9c587157b1de9ea79b785bc3fa8776dc7a485 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 12:54:29 -0400 Subject: [PATCH 23/42] paginate assetMeters --- .../OpenXDA/Assets/OpenXDAAssetController.cs | 27 ++++++++++++ .../TSX/SystemCenter/Asset/AssetMeter.tsx | 44 ++++++++++++------- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index c6361abe9..37343c73e 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -152,6 +152,33 @@ public IHttpActionResult GetAssetMeters(int assetID) } return Unauthorized(); } + [HttpPost, Route("{assetID:int}/Meters/{page:int}")] + public IHttpActionResult GetAssetMetersPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + { + if (!GetAuthCheck()) + return Unauthorized(); + + int recordsPerPage = Take ?? 50; + + PagedResults results = new PagedResults(); + + results.RecordsPerPage = recordsPerPage; + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + IEnumerable records = new TableOperations(connection).QueryRecordsWhere("ID IN (SELECT MeterID FROM MeterAsset WHERE AssetID = {0})", assetID); + + if (postData.Ascending) + records = records.OrderBy(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + else + records = records.OrderByDescending(record => record.GetType().GetProperty(postData.OrderBy).GetValue(record)); + + results.TotalRecords = records.Count(); + results.NumberOfPages = (records.Count() + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(records.Skip(page * recordsPerPage).Take(recordsPerPage)); + } + return Ok(results); + } [HttpGet, Route("{assetID:int}/AssetConnections")] public IHttpActionResult GetAssetAssetConnections(int assetID) 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 9d1d7ca01..edcca6272 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetMeter.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { DefaultSelects } from '@gpa-gemstone/common-pages'; import { ByMeterSlice } from '../Store/Store'; @@ -46,6 +46,8 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ const mStatus = useAppSelector(ByMeterSlice.Status); const mParentID = useAppSelector(ByMeterSlice.ParentID); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { @@ -54,28 +56,29 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ }, [mStatus, mParentID]); + React.useEffect(() => { + return getMeters(); + }, [ascending, sortField, page]) + React.useEffect(() => { getMeters(); }, [props.Asset]); function getMeters(): void { $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.Asset.ID}/Meters/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true - }).done(meters => { - const sortedMeters = sortData(sortField, ascending, meters); - setMeters(sortedMeters); + async: true, + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending }) + }).done(d => { + setMeters(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }); } - function sortData(key: keyof OpenXDA.Types.Meter, ascending: boolean, data: OpenXDA.Types.Meter[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function addMeter(meterID: number) { return $.ajax({ type: "POST", @@ -176,7 +179,8 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{
-
+
+
TableClass="table table-hover" Data={meters} @@ -185,14 +189,10 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ OnSort={(d) => { if (d.colKey == sortField) { setAscending(!ascending); - const ordered = _.orderBy(meters, [d.colKey], [(!ascending ? "asc" : "desc")]); - setMeters(ordered); } else { setAscending(true); setSortField(d.colField); - const ordered = _.orderBy(meters, [d.colKey], ["asc"]); - setMeters(ordered); } }} TableStyle={{ height: '100%' }} @@ -234,7 +234,17 @@ function AssetMeterWindow(props: { Asset: OpenXDA.Types.Asset }): JSX.Element{ RowStyle={{ width: '20%' }} > Model - + +
+
+
+ setPage(page - 1)} + /> +
+
From d0d14c4c47cbddae0b771249cc43d573721dad94 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 13:11:55 -0400 Subject: [PATCH 24/42] paginate assetConnections --- .../OpenXDA/Assets/OpenXDAAssetController.cs | 55 ++++++++++++++++++- .../SystemCenter/Asset/AssetConnection.tsx | 40 ++++++++------ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs index 37343c73e..8ba67a79c 100644 --- a/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs +++ b/Source/Applications/SystemCenter/Controllers/OpenXDA/Assets/OpenXDAAssetController.cs @@ -220,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(); + + using (AdoDataConnection connection = new AdoDataConnection(Connection)) + { + DataTable records = connection.RetrieveData(@$" + SELECT + AssetRelationship.AssetRelationshipTypeID, + AssetRelationshipType.Name, + Asset.ID as AssetID, + Asset.AssetKey, + Asset.AssetName + FROM + AssetRelationship JOIN + AssetRelationshipType ON AssetRelationship.AssetRelationshipTypeID = AssetRelationshipType.ID JOIN + ASset ON Asset.ID = ( + CASE + WHEN ParentID = {{0}} THEN AssetRelationship.ChildID + ELSE AssetRelationship.ParentID + END + ) + WHERE + ParentID = {{0}} OR ChildID = {{0}} + ORDER BY {postData.OrderBy} {(postData.Ascending ? "ASC" : "DESC")} + ", assetID); + + int totalRecords = records.Rows.Count; + + DataRow[] rows = records.AsEnumerable() + .Skip((page) * recordsPerPage) + .Take(recordsPerPage) + .ToArray(); + + DataTable pagedTable = records.Clone(); + + foreach (DataRow row in rows) + pagedTable.ImportRow(row); + + results.TotalRecords = totalRecords; + results.NumberOfPages = (totalRecords + recordsPerPage - 1) / recordsPerPage; + results.Data = JsonConvert.SerializeObject(pagedTable); + } + + return Ok(results); + } + [HttpGet, Route("{assetID:int}/OtherLocations")] public IHttpActionResult GetOtherLocations(int assetID) { @@ -664,7 +717,7 @@ public IEnumerable GetAssetChannels(int assetID) } [HttpPost, Route("{assetID:int}/ConnectedChannels/{page:int}")] - public IHttpActionResult GetPagedList([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) + public IHttpActionResult GetConnectedChannelsPaged([FromBody] PostData postData, [FromUri] int assetID, [FromUri] int page) { if (!GetAuthCheck()) return Unauthorized(); 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 428cd3ef1..94072f415 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetConnection.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import _ from 'lodash'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { useNavigate } from "react-router-dom"; import { LoadingIcon, Modal, Search, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; @@ -62,12 +62,14 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number const [trigger, setTrigger] = React.useState(0); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); const roles = useAppSelector(SelectRoles); React.useEffect(() => { let handle = getAssetConnections(); return () => { if (handle != null || handle.abort != null) handle.abort();} - }, [props.ID, trigger]) + }, [props.ID, trigger, page, sortKey, ascending]) React.useEffect(() => { if (props.ID > 0) { @@ -111,23 +113,20 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number function getAssetConnections(): JQuery.jqXHR { setStatus('loading'); return $.ajax({ - type: "GET", - url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections`, + type: "POST", + url: `${homePath}api/OpenXDA/Asset/${props.ID}/AssetConnections/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, - async: true + async: true, + data: JSON.stringify({ OrderBy: sortKey, Ascending: ascending }) }).done((d) => { setStatus('idle') - const sortedConnections = sortData(sortKey, ascending, d); - setAssetConnections(sortedConnections) + setAssetConnections(JSON.parse(d.Data)); + setTotalPages(d.NumberOfPages); }).fail(() => setStatus('error')); } - function sortData(key: string, ascending: boolean, data: AssetConnection[]) { - return _.orderBy(data, [key], [(ascending ? "asc" : "desc")]); - } - function getAssets(): JQuery.jqXHR { const filter = [ { FieldName: 'ID', SearchText: `(SELECT AssetID FROM AssetLocation WHERE LocationID IN (SELECT LocationID FROM AssetLocation WHERE AssetID = ${props.ID}))`, Operator: 'IN', Type: 'query', IsPivotColumn: false }, @@ -239,7 +238,8 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number
-
+
+
TableClass="table table-hover" Data={assetConnections} @@ -251,14 +251,10 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number if (d.colKey === sortKey) { setAscending(!ascending); - const ordered = _.orderBy(assetConnections, [d.colKey], [(!ascending ? "asc" : "desc")]); - setAssetConnections(ordered); } else { setAscending(true); setSortKey(d.colKey); - const ordered = _.orderBy(assetConnections, [d.colKey], ["asc"]); - setAssetConnections(ordered); } }} TableStyle={{ height: '100%' }} @@ -306,7 +302,17 @@ function AssetConnectionWindow(props: { Name: string, ID: number, TypeID: number } >

- + +
+
+
+ setPage(page - 1)} + /> +
+
From 6b9f34275e9137c4c3b5a3061c3ad5bbd7eab499 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 14:54:42 -0400 Subject: [PATCH 25/42] paginate customerMeter --- .../SystemCenter/Customer/CustomerMeter.tsx | 163 ++++++++++-------- 1 file changed, 89 insertions(+), 74 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 68f95bdb0..163ce50a3 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerMeter.tsx @@ -27,7 +27,7 @@ import { PQView, OpenXDA as LocalXDA } from '../global'; import { 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 { 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 { ToolTip } from '@gpa-gemstone/react-forms'; @@ -42,8 +42,10 @@ const CustomerMeterWindow = (props: IProps) => { const status = useAppSelector(CustomerMeterSlice.SearchStatus); const [showAdd, setShowAdd] = React.useState(false); - const sortField = useAppSelector(CustomerMeterSlice.SortField); - const ascending = useAppSelector(CustomerMeterSlice.Ascending); + const [sortField, setSortField] = React.useState('MeterName') + const [ascending, setAscending] = React.useState(true) + const totalPages = useAppSelector(CustomerMeterSlice.TotalPages); + const [page, setPage] = React.useState(0); const [removeRecord, setRemoveRecord] = React.useState(null); @@ -57,17 +59,17 @@ const CustomerMeterWindow = (props: IProps) => { React.useEffect(() => { getData(); - }, [props.Customer.ID]) + }, [props.Customer.ID, sortField, ascending, page]) function getData() { - dispatch(CustomerMeterSlice.DBSearch({ + dispatch(CustomerMeterSlice.PagedSearch({ filter: [{ FieldName: 'CustomerID', SearchText: props.Customer.ID.toString(), Operator: '=', Type: 'number', IsPivotColumn: false - }], sortField, ascending + }], sortField, ascending, page })); } @@ -180,82 +182,95 @@ const CustomerMeterWindow = (props: IProps) => {
return <> -
-
-
-
-

Assigned Meters:

+
+
+
+
+

Assigned Meters:

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

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

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

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

-
+
{ if (c) dispatch(CustomerMeterSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> Date: Tue, 9 Jun 2026 14:54:53 -0400 Subject: [PATCH 26/42] paginate customerAsset --- .../SystemCenter/Customer/CustomerAsset.tsx | 171 ++++++++++-------- 1 file changed, 97 insertions(+), 74 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 bb0c3bceb..d85903802 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/CustomerAsset.tsx @@ -27,7 +27,7 @@ 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 { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { LoadingIcon, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; @@ -38,21 +38,31 @@ declare var homePath: string; interface IProps { Customer: OpenXDA.Types.Customer } const CustomerAssetWindow = (props: IProps) => { const dispatch = useAppDispatch(); - const data = useAppSelector(CustomerAssetSlice.Data); + const data = useAppSelector(CustomerAssetSlice.SearchResults); const status = useAppSelector(CustomerAssetSlice.Status); const [showAdd, setShowAdd] = React.useState(false); - const sortField = useAppSelector(CustomerAssetSlice.SortField); - const ascending = useAppSelector(CustomerAssetSlice.Ascending); + const [sortField, setSortField] = React.useState('AssetName') + const [ascending, setAscending] = React.useState(true) + const totalPages = useAppSelector(CustomerAssetSlice.TotalPages); + const [page, setPage] = React.useState(0); const [removeRecord, setRemoveRecord] = React.useState(null); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles) + const getData = React.useCallback(() => { + dispatch(CustomerAssetSlice.PagedSearch({ filter: [], sortField, ascending, page })) + }, [sortField, ascending, page, CustomerAssetSlice.PagedSearch]) + + React.useEffect(() => { + getData(); + }, [props.Customer.ID, sortField, ascending, page]) + React.useEffect(() => { if (status == 'uninitiated' || status == 'changed') - dispatch(CustomerAssetSlice.Fetch()); + getData(); }, [status]); function saveCustomerAssets(m: SystemCenter.Types.DetailedAsset[]) { @@ -115,82 +125,95 @@ const CustomerAssetWindow = (props: IProps) => {
return <> -
-
-
-
-

Assigned Assets:

+
+
+
+
+

Assigned Assets:

+
-
-
-
- - TableClass="table table-hover" - Data={data} - SortKey={sortField} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Remove') - return; - dispatch(CustomerAssetSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'AssetName'} - AllowSort={true} - Field={'AssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Name - - - Key={'AssetKey'} - AllowSort={true} - Field={'AssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Key - - - Key={'AssetType'} - AllowSort={true} - Field={'AssetType'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Type - - - Key={'Remove'} - AllowSort={false} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => - - } - >

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

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

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

-
+
{ if (c) dispatch(CustomerAssetSlice.DBAction({ record: removeRecord, verb: 'DELETE' })); setRemoveRecord(null); }} /> Date: Tue, 9 Jun 2026 15:09:57 -0400 Subject: [PATCH 27/42] paginate MeterTrendChannel --- .../SystemCenter/Meter/MeterTrendChannel.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) 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 cff473223..2dad8f8bc 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(); // left in because it sets the parent ID + }, [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) } + /> +
+
From b4941ad0f700b836e137f811230b2c7e1015737c Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 15:52:18 -0400 Subject: [PATCH 28/42] remove accidental comment --- .../Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2dad8f8bc..fed2618f0 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx @@ -94,7 +94,7 @@ const MeterTrendChannelWindow = (props: IProps) => { React.useEffect(() => { if (status == 'uninitiated' || status == 'changed' || meterID !== props.Meter.ID) - pagedSearch(); // left in because it sets the parent ID + pagedSearch(); }, [props.Meter, status, pagedSearch]); React.useEffect(() => { From e0427aba5c9208f4dcd39fcbc2122ed7d4d17001 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Tue, 9 Jun 2026 15:52:49 -0400 Subject: [PATCH 29/42] paginate meterChannelScaling table --- .../ChannelScaling/ChannelScalingForm.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) 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 fcae92e6c..521b5654d 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}
From 80d6259895aeccae827eed42d2b5d9a5fbd817c3 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 10:08:14 -0400 Subject: [PATCH 30/42] paginate meterEventChannels --- .../SystemCenter/Meter/MeterEventChannel.tsx | 45 +++++++---- .../SystemCenter/Store/EventChannelSlice.ts | 74 ++++++++++++++++++- 2 files changed, 103 insertions(+), 16 deletions(-) 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 211787dd6..a666c262c 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 { SelectAscending, SelectSortKey, 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]) React.useEffect(() => { if (pStatus == 'uninitiated' || pStatus == 'changed') @@ -83,7 +88,7 @@ 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 (!props.IsVisible) @@ -243,12 +248,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 +263,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 +409,15 @@ const MeterEventChannelWindow = (props: IProps) => {

- +
+
+
+ setPage(page - 1)} + /> +
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 288617a56..77c7100ab 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts @@ -28,6 +28,12 @@ import { Application } from '@gpa-gemstone/application-typings'; import { OpenXDA } from '../global'; let fetchHandle = null; +let pagedHandle = null; + +interface PagedResults { + Data: string, + NumberOfPages: number +} export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', async (args: { sortField?: keyof OpenXDA.EventChannel, @@ -61,6 +67,32 @@ export const dBAction = createAsyncThunk(`EventChannels/DBAction`, async (args: return await handle }); +export const PagedSearch = createAsyncThunk('EventChannels/PagedSearch', async (args: { + sortField?: keyof OpenXDA.EventChannel, + ascending?: boolean, + meterId: number, + page: number +}, { 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; + + if (pagedHandle != null && pagedHandle.abort != null) + fetchHandle.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: { @@ -70,7 +102,9 @@ export const EventChannelSlice = createSlice({ ActiveFetchID: [] as string[], Asc: true as boolean, Sort: 'Name' as keyof (OpenXDA.EventChannel), - ParentID: null + ParentID: null, + NumberOfPages: 0, + PagedData: [] as OpenXDA.EventChannel[] }, reducers: { SetChanged: (state) => { @@ -120,6 +154,30 @@ export const EventChannelSlice = createSlice({ 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 +189,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,6 +203,18 @@ function GetRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.E }); } +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: 'POST' | 'DELETE' | 'PATCH', record: OpenXDA.EventChannel): JQuery.jqXHR { let action = ''; if(verb === 'POST') action = 'Add'; From 54a84ce7e58b740b7e125d146cd5fa58b8afea00 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 10:46:02 -0400 Subject: [PATCH 31/42] Paginate remoteXDA assets --- .../SystemCenter/RemoteXDA/RemoteAssetTab.tsx | 269 ++++++++++-------- 1 file changed, 144 insertions(+), 125 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 8b4b2c8e6..19bc321c0 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import { RemoteXDAAssetSlice, ByAssetSlice } from '../Store/Store'; import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; @@ -45,15 +45,17 @@ const RemoteAssetTab = (props: IProps) => { const remoteAssetStatus = useAppSelector(RemoteXDAAssetSlice.Status); const searchResults = useAppSelector(RemoteXDAAssetSlice.SearchResults); const searchState = useAppSelector(RemoteXDAAssetSlice.SearchStatus); + const [page, setPage] = React.useState(0) + const totalPages = useAppSelector(RemoteXDAAssetSlice.TotalPages); const searchFilters: Search.IFilter[] = - [{ - FieldName: 'RemoteXDAInstanceID', - SearchText: props.ID.toString(), - Operator: '=', - Type: 'number', - IsPivotColumn: false - }] + [{ + FieldName: 'RemoteXDAInstanceID', + SearchText: props.ID.toString(), + Operator: '=', + Type: 'number', + IsPivotColumn: false + }] // Edit and Delete Form Consts const [newInstErrors, setNewInstErrors] = React.useState([]); @@ -70,6 +72,10 @@ const RemoteAssetTab = (props: IProps) => { const roles = useAppSelector(SelectRoles); const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); + const pagedSearch = React.useCallback(() => { + dispatch(RemoteXDAAssetSlice.PagedSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey, page })) + }, [page, sortKey, searchFilters, ascending]) + React.useEffect(() => { if (remoteAssetStatus === 'uninitiated' || remoteAssetStatus === 'changed') dispatch(RemoteXDAAssetSlice.Fetch()); @@ -77,12 +83,12 @@ const RemoteAssetTab = (props: IProps) => { React.useEffect(() => { if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAAssetSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); + pagedSearch() }, [dispatch, searchState]); React.useEffect(() => { - dispatch(RemoteXDAAssetSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + pagedSearch() + }, [ascending, sortKey, page]); React.useEffect(() => { if (assetStatus === 'uninitiated' || assetStatus === 'changed') @@ -106,120 +112,133 @@ const RemoteAssetTab = (props: IProps) => { cardBody = } else { cardBody = - - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalAssetName'} - AllowSort={true} - Field={'LocalAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'RemoteAssetName'} - AllowSort={true} - Field={'RemoteAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Name - - - Key={'RemoteAssetKey'} - AllowSort={true} - Field={'RemoteAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null} - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

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

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

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

+ + +
+
+
+ setPage(page - 1)} + Total={totalPages} + /> +
+
+ } return ( From 03a0e2511be10f3a01518b247b6f77c46e3b4f88 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 10:46:15 -0400 Subject: [PATCH 32/42] Paginate remoteXDA meters --- .../SystemCenter/RemoteXDA/RemoteMeterTab.tsx | 293 ++++++++++-------- 1 file changed, 156 insertions(+), 137 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 cf73df784..154b4fbbc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { SystemCenter, Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { RemoteXDAMeterSlice, ByMeterSlice, RemoteXDAAssetSlice } from '../Store/Store'; import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning } from '@gpa-gemstone/react-interactive'; @@ -45,6 +45,8 @@ const RemoteMeterTab = (props: IProps) => { const remoteMeterStatus = useAppSelector(RemoteXDAMeterSlice.Status) as Application.Types.Status; const searchResults = useAppSelector(RemoteXDAMeterSlice.SearchResults); const searchState = useAppSelector(RemoteXDAMeterSlice.SearchStatus); + const [page, setPage] = React.useState(0) + const totalPages = useAppSelector(RemoteXDAMeterSlice.TotalPages); const searchFilters: Search.IFilter[] = [{ @@ -83,6 +85,10 @@ const RemoteMeterTab = (props: IProps) => { const roles = useAppSelector(SelectRoles); const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); + const pagedSearch = React.useCallback(() => { + dispatch(RemoteXDAMeterSlice.PagedSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey, page })) + }, [page, sortKey, searchFilters, ascending]) + React.useEffect(() => { if (remoteMeterStatus === 'uninitiated' || remoteMeterStatus === 'changed') dispatch(RemoteXDAMeterSlice.Fetch()); @@ -95,12 +101,12 @@ const RemoteMeterTab = (props: IProps) => { React.useEffect(() => { if (searchState === 'uninitiated' || searchState === 'changed') - dispatch(RemoteXDAMeterSlice.DBSearch({ filter: searchFilters, ascending: ascending, sortField: sortKey })); + pagedSearch() }, [dispatch, searchState]); React.useEffect(() => { - dispatch(RemoteXDAMeterSlice.DBSearch({ sortField: sortKey, ascending, filter: searchFilters })) - }, [ascending, sortKey]); + pagedSearch() + }, [ascending, sortKey, page]); function isEditable(item: OpenXDA.Types.RemoteXDAMeter): boolean { return item.RemoteXDAMeterID <= 0; @@ -141,137 +147,150 @@ const RemoteMeterTab = (props: IProps) => { cardBody = } else { cardBody = - - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalMeterName'} - AllowSort={true} - Field={'LocalMeterName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'LocalAlias'} - AllowSort={true} - Field={'LocalAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Alias - - - Key={'RemoteXDAName'} - AllowSort={true} - Field={'RemoteXDAName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? item.RemoteXDAName : item.LocalMeterName} - > Remote Name - - - Key={'RemoteXDAAssetKey'} - AllowSort={true} - Field={'RemoteXDAAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'RemoteAlias'} - AllowSort={true} - Field={'RemoteAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Alias - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null } - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

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

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

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

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

Add { assetCount } Associated Assets?

+

Add {assetCount} Associated Assets?

Date: Wed, 10 Jun 2026 10:59:21 -0400 Subject: [PATCH 33/42] simplify settings sort --- .../Scripts/TSX/SystemCenter/Settings/Setting.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx index 94c66314a..da7c5750f 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/Setting.tsx @@ -147,7 +147,14 @@ function Setting(props: IProps) { Data={data} SortKey={sortField as string} Ascending={ascending} - OnSort={sort} + OnSort={(d) => { + if (d.colField === sortField) + setAscending(!ascending); + else { + setAscending(true); + setSortField(d.colField); + } + }} OnClick={(item) => { setEditNewSetting(item.row); setShowModal(true); setEditNew('Edit'); }} TheadStyle={{ fontSize: 'smaller' }} TbodyStyle={{ display: 'block', overflowY: 'scroll', maxHeight: window.innerHeight - 300, width: '100%' }} From 654dbed357bc7cee71296c89c1bcf0864f5503b2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 11:58:14 -0400 Subject: [PATCH 34/42] clean up user and userGroup --- .../Scripts/TSX/SystemCenter/User/User/ByUser.tsx | 14 +++++--------- .../SystemCenter/User/UserGroup/ByUserGroup.tsx | 12 ++++-------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index df1afcf20..65ae4ddf8 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -61,7 +61,7 @@ const newAcct: IUserAccount = { const ByUser: Application.Types.iByComponent = (props) => { - const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true), []) + const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName" as keyof IUserAccount, 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'), []) @@ -74,7 +74,7 @@ const ByUser: Application.Types.iByComponent = (props) => { const [userStatus, setUserStatus] = React.useState('uninitiated'); const [sortField, setSortField] = React.useState("DisplayName"); - const [ascending, setAscending] = React.useState(false); + const [ascending, setAscending] = React.useState(true); const [page, setPage] = React.useState(0); const [totalPages, setTotalPages] = React.useState(0); @@ -110,7 +110,7 @@ const ByUser: Application.Types.iByComponent = (props) => { const pagedSearch = React.useCallback(() => { setUserStatus('loading') - const h = userAccountController.PagedSearch(filters, sortField as "DisplayName", ascending, page); + const h = userAccountController.PagedSearch(filters, sortField, ascending, page); h.done((d) => { setUsers(JSON.parse(d.Data as unknown as string)) setTotalPages(d.NumberOfPages) @@ -126,14 +126,9 @@ const ByUser: Application.Types.iByComponent = (props) => { }, [filters, sortField, ascending, page, userAccountController.PagedSearch]) React.useEffect(() => { - pagedSearch() + return pagedSearch() }, [filters, sortField, ascending, page, pagedSearch]); - React.useEffect(() => { - if (userStatus === 'uninitiated' || userStatus === 'changed') - pagedSearch() - }, [userStatus, pagedSearch]) - React.useEffect(() => { if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') { const h = userAdditionalFieldController.Fetch(); @@ -312,6 +307,7 @@ const ByUser: Application.Types.iByComponent = (props) => { if (confirm) { userAccountController.DBAction('POST', act) setUserStatus('changed'); + pagedSearch(); } 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 c4ab7005b..f35107438 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 @@ -41,7 +41,7 @@ const emptyGroup: ISecurityGroup = { Name: "", CreatedBy: "", CreatedOn: new Dat const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName"),[]) + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup),[]) const [filters, setFilters] = React.useState[]>([]); const [securityGroups, setSecurityGroups] = React.useState([]); @@ -51,13 +51,13 @@ const ByUser: Application.Types.iByComponent = (props) => { const [status, setStatus] = React.useState('uninitiated'); const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [sortField, setSortField] = React.useState('DisplayName'); - const [ascending, setAscending] = React.useState(false); + const [ascending, setAscending] = React.useState(true); const [showModal, setShowModal] = React.useState(false); const [groupError, setGroupError] = React.useState([]); const [newGroup, setNewGroup] = React.useState(emptyGroup); const pagedSearch = React.useCallback(() => { - const h = securityGroupController.PagedSearch(filters, sortField as "DisplayName", ascending, currentPage) + const h = securityGroupController.PagedSearch(filters, sortField, ascending, currentPage) h.done((d) => { setSecurityGroups(JSON.parse(d.Data as unknown as string)) setTotalPages(d.NumberOfPages) @@ -76,11 +76,6 @@ const ByUser: Application.Types.iByComponent = (props) => { return pagedSearch() }, [sortField, ascending, filters, currentPage, pagedSearch]) - React.useEffect(() => { - if (status === "uninitiated" || status === "changed") - return pagedSearch() - }, [status, pagedSearch]) - return (
@@ -190,6 +185,7 @@ const ByUser: Application.Types.iByComponent = (props) => { { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } as ISecurityGroup ) setStatus('changed') + pagedSearch() } setShowModal(false); }} From 8344930aa3febb9ebf032cfacd8b51d9208b1fcb Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 12:43:02 -0400 Subject: [PATCH 35/42] fix generic controller typing --- .../wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx | 2 +- .../Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index 65ae4ddf8..d27d3062d 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -61,7 +61,7 @@ const newAcct: IUserAccount = { const ByUser: Application.Types.iByComponent = (props) => { - const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName" as keyof IUserAccount, true), []) + const userAccountController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName" as keyof IUserAccount, 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'), []) 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 f35107438..8accab8ff 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 @@ -41,7 +41,7 @@ const emptyGroup: ISecurityGroup = { Name: "", CreatedBy: "", CreatedOn: new Dat const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup),[]) + const securityGroupController = React.useMemo(() => new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup),[]) const [filters, setFilters] = React.useState[]>([]); const [securityGroups, setSecurityGroups] = React.useState([]); @@ -182,7 +182,7 @@ const ByUser: Application.Types.iByComponent = (props) => { if (confirm) { securityGroupController.DBAction( 'POST', - { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } as ISecurityGroup + { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } ) setStatus('changed') pagedSearch() From fefd5d43dd113932d59277aa1996afde610aee3d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 14:50:51 -0400 Subject: [PATCH 36/42] fix dependency array --- .../Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a666c262c..5219eb20c 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -73,7 +73,7 @@ const MeterEventChannelWindow = (props: IProps) => { React.useEffect(() => { dispatch(PagedSearch({ meterId: props.Meter.ID, page: page, ascending: ascending, sortField: sortKey })); - }, [ascending, sortKey, page]) + }, [ascending, sortKey, page, props.Meter.ID]) React.useEffect(() => { if (pStatus == 'uninitiated' || pStatus == 'changed') From c25fe92baf6f3fe98bd827bc59226f4475a770ff Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 14:51:15 -0400 Subject: [PATCH 37/42] use stricter typing in EventChannelSlice --- .../SystemCenter/Store/EventChannelSlice.ts | 107 ++++++++++++------ 1 file changed, 70 insertions(+), 37 deletions(-) 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 77c7100ab..df29e36df 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts @@ -23,28 +23,77 @@ -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; -interface PagedResults { +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 + NumberOfPages: number, + RecordsPerPage: number, + TotalRecords: number } -export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', async (args: { +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 = Record; + + +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'); @@ -59,7 +108,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(); @@ -67,18 +116,13 @@ export const dBAction = createAsyncThunk(`EventChannels/DBAction`, async (args: return await handle }); -export const PagedSearch = createAsyncThunk('EventChannels/PagedSearch', async (args: { - sortField?: keyof OpenXDA.EventChannel, - ascending?: boolean, - meterId: number, - page: number -}, { getState, signal }) => { +export const PagedSearch = createAsyncThunk('EventChannels/PagedSearch', async (args: IPagedSearchArgs, { 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 (pagedHandle != null && pagedHandle.abort != null) fetchHandle.abort('Prev'); @@ -91,21 +135,11 @@ export const PagedSearch = createAsyncThunk('EventChannels/PagedSearch', async ( }); 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, - NumberOfPages: 0, - PagedData: [] as OpenXDA.EventChannel[] - }, + initialState, reducers: { SetChanged: (state) => { state.Status = "changed"; @@ -130,7 +164,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); @@ -141,14 +175,13 @@ 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'; @@ -169,7 +202,7 @@ export const EventChannelSlice = createSlice({ Time: new Date().toString() } }); - builder.addCase(PagedSearch.fulfilled, (state, action: PayloadAction) => { + 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); @@ -203,7 +236,7 @@ function GetRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.E }); } -function GetPagedRecords(ascending: (boolean | undefined), sortField: keyof OpenXDA.EventChannel, parentID: number | void, page: number): 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}`, @@ -211,13 +244,13 @@ function GetPagedRecords(ascending: (boolean | undefined), sortField: keyof Open dataType: 'json', cache: false, async: true, - data: JSON.stringify({OrderBy: sortField, Ascending: ascending, Searches: []}) + data: JSON.stringify({ OrderBy: sortField, Ascending: ascending, Searches: [] }) }); } -function Action(verb: 'POST' | 'DELETE' | 'PATCH', record: OpenXDA.EventChannel): JQuery.jqXHR { +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'; From c8f1cb48ae6ad440b6251a724c3c0a8b09a87393 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 14:51:44 -0400 Subject: [PATCH 38/42] changes to store typing --- .../Scripts/TSX/SystemCenter/Store/Store.ts | 237 ++++++++++++------ 1 file changed, 160 insertions(+), 77 deletions(-) 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 73040d5ce..34de0eea0 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'); @@ -140,82 +143,162 @@ export const ChannelTemplateSlice = new GenericSlice Date: Wed, 10 Jun 2026 15:12:28 -0400 Subject: [PATCH 39/42] cleanup import --- .../Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5219eb20c..eb367a4ae 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -31,7 +31,7 @@ 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, SelectPagedData, SelectNumberOfPages } 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'; From 843404e5fdc9ef66c116f10b6dea84360e269a40 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 15:12:41 -0400 Subject: [PATCH 40/42] fix typing --- .../Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 df29e36df..ff285f880 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/EventChannelSlice.ts @@ -84,7 +84,8 @@ interface IPagedSearchArgs extends IFetchArgs { page: number } -type ThunkState = Record; +type ThunkState = {EventChannels: IEventChannelState} + ; export const FetchChannels = createAsyncThunk('EventChannels/FetchChannels', async (args: IFetchArgs, { getState, signal }) => { From 7666d19f21513b11b10a81701253247b00749aa6 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 10 Jun 2026 15:26:32 -0400 Subject: [PATCH 41/42] refresh on changed --- .../Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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 eb367a4ae..4d9b3e323 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -90,6 +90,11 @@ const MeterEventChannelWindow = (props: IProps) => { dispatch(FetchChannels({ meterId: props.Meter.ID })); }, [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) return; From db44f0c8e9fea03d58e396a7b1a64b2ed82adf99 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 11 Jun 2026 13:52:23 -0400 Subject: [PATCH 42/42] fix broken Additional Field selector in ExternalDBTable --- .../ExternalDB/ExternalDBTableFields.tsx | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 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 9582f506e..aeb32ad18 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ExternalDB/ExternalDBTableFields.tsx @@ -371,31 +371,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 +