Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Source/Applications/SystemCenter/Model/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using System.Web.Http;
using GSF.Data.Model;
using GSF.Web.Model;
using openXDA.Model;

namespace SystemCenter.Model
{
Expand Down Expand Up @@ -53,6 +54,15 @@ public class Node
public string AssignedHostRegistrationKey { get; set; }
}

[RoutePrefix("api/SystemCenter/Node")]
public class SystemCenterNodeController : ModelController<Node> {}

[RoutePrefix("api/OpenXDA/NodeTypes")]
public class OpenXDANodeTypesController : ModelController<openXDA.Model.NodeType> {}

[RoutePrefix("api/OpenXDA/HostRegistration")]
public class OpenXDAHostRegistrationController : ModelController<openXDA.Model.HostRegistration> {}

[RoutePrefix("api/OpenXDA/Node")]
public class OpenXDANodeController : ModelController<Node> {}
public class OpenXDANodeController : ModelController<openXDA.Model.Node> { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ const Matcher: React.FunctionComponent = (props: {}) => {
return <RoleAccessErrorPage Logo={`${homePath}Images/GiantLogo.png`} />
return <AppHost Roles={roles} />
}
else if (params['name'] == "Nodes") {
else if (params['name'] == "TaskRunners") {
if (roles.indexOf('Administrator') < 0)
return <RoleAccessErrorPage Logo={`${homePath}Images/GiantLogo.png`} />
return <ByNode Roles={roles} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,36 @@
//******************************************************************************************************

import * as React from 'react';
import { GenericController, Search, SearchBar, LoadingScreen } from '@gpa-gemstone/react-interactive'
import { ReactIcons } from '@gpa-gemstone/gpa-symbols';
import { GenericController, Search, SearchBar, LoadingScreen, Modal } from '@gpa-gemstone/react-interactive'
import { Table, Column, Paging } from '@gpa-gemstone/react-table'
import { Application } from '@gpa-gemstone/application-typings';
import { Application, OpenXDA } from '@gpa-gemstone/application-typings';
import NodeForm from './NodeForm'
import { SystemCenter as SC } from '../global'

const defaultSearchcols: Search.IField<SC.Node>[] = [
{ label: 'Name', key: 'Name', type: 'string', isPivotField: false },
{ label: 'Minimum Host Count', key: 'MinimumHostCount', type: 'number', isPivotField: false },
{ label: 'Type', key: 'NodeType', type: 'string', isPivotField: false },
{ label: 'Host Registration Key', key: 'HostRegistrationKey', type: 'string', isPivotField: false },
{ label: 'Assigned Host Registration Key', key: 'AssignedHostRegistrationKey', type: 'string', isPivotField: false }
];
interface INodeType {
ID: number,
Name: string,
AssemblyName: string,
TypeName: string
}

interface IHostRegistration {
ID: number,
RegistrationKey: string,
APIToken: string,
URL: string,
CheckedIn: string
}

interface IOpenXDANode {
ID: number,
NodeTypeID: number,
HostRegistrationID: number,
AssignedHostRegistrationID: number,
Name: string,
MinimumHostCount: number
}

const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
const [data, setData] = React.useState<SC.Node[]>([])
Expand All @@ -45,10 +63,39 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
const [status, setStatus] = React.useState<Application.Types.Status>('uninitiated')
const [recordsPerPage, setRecordsPerPage] = React.useState<number>(0);
const [totalRecords, setTotalRecords] = React.useState<number>(0);
const [nodeTypes, setNodeTypes] = React.useState<INodeType[]>([]);
const [appHosts, setAppHosts] = React.useState<IHostRegistration[]>([])
const [showModal, setShowModal] = React.useState<boolean>(false)
const [selectedNode, setSelectedNode] = React.useState<SC.Node>({ ID: '-1', Name: "", AssignedHostRegistrationKey: '', HostRegistrationKey: '', NodeType: '', MinimumHostCount: 0 });
const [errors, setErrors] = React.useState<string[]>([]);
const [refreshCount, refreshData] = React.useState<number>(0);


React.useEffect(() => {
if (status === 'uninitiated') {
const nodeTypeController = new GenericController<INodeType>(`${homePath}api/OpenXDA/NodeTypes`, 'Name', true);
const handle = nodeTypeController.Fetch();
handle.done((d: INodeType[]) => {
setNodeTypes(d);
}).fail((d) => {
setStatus('error');
})
}
}, [status])

React.useEffect(() => {
const appHostController = new GenericController<IHostRegistration>(`${homePath}api/OpenXDA/HostRegistration`, 'ID', true);
const handle = appHostController.Fetch();
handle.done((d: IHostRegistration[]) => {
setAppHosts(d);
}).fail((d) => {
setStatus('error');
})
}, [status])

React.useEffect(() => {
setStatus('loading');
const nodeController = new GenericController<SC.Node>(`${homePath}api/OpenXDA/Node`, 'Name', true)
const nodeController = new GenericController<SC.Node>(`${homePath}api/SystemCenter/Node`, 'Name', true)
const handle = nodeController.PagedSearch(filters, sortField, ascending, page);
handle.done((d) => {
setData(JSON.parse(d.Data as unknown as string));
Expand All @@ -58,15 +105,36 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
setStatus('idle');
}).fail((d) => {
setStatus('error');
}) },[filters, sortField, ascending, page])
})
}, [filters, sortField, ascending, page, refreshCount])


const defaultSearchcols: Search.IField<SC.Node>[] = [
{ label: 'Name', key: 'Name', type: 'string', isPivotField: false },
{ label: 'Minimum Host Count', key: 'MinimumHostCount', type: 'number', isPivotField: false },
{ label: 'Type', key: 'NodeType', isPivotField: false, type: 'enum', enum: nodeTypes.map((n) => { return { Value: n.Name, Label: n.Name } }) },
{ label: 'Node', key: 'HostRegistrationKey', isPivotField: false, type: 'enum', enum: appHosts.map((h) => { return { Value: h.RegistrationKey, Label: h.RegistrationKey } })},
{ label: 'Assigned Node', key: 'AssignedHostRegistrationKey', isPivotField: false, type: 'enum', enum: appHosts.map((h) => { return { Value: h.RegistrationKey, Label: h.RegistrationKey } })}
];

const convertToXDANode = React.useCallback((node: SC.Node): IOpenXDANode => {
return {
ID: parseInt(node.ID),
MinimumHostCount: node.MinimumHostCount,
NodeTypeID: nodeTypes.find(nt => nt.Name == node.NodeType).ID,
AssignedHostRegistrationID: appHosts.find(ah => ah.RegistrationKey == node.AssignedHostRegistrationKey)?.ID ?? null,
HostRegistrationID: appHosts.find(ah => ah.RegistrationKey == node.HostRegistrationKey)?.ID ?? null,
Name: node.Name
}
}, [nodeTypes, appHosts])

return <div style={{ width: '100%', height: '100%' }}>
<LoadingScreen Show={status === 'loading'} />
<div className="container-fluid d-flex h-100 flex-column">
<div className="row">
<SearchBar<SC.Node> CollumnList={defaultSearchcols} SetFilter={setFilters}
Direction={'left'} defaultCollumn={{ label: 'Name', key: 'Name', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'}
ShowLoading={status === 'loading'} ResultNote={status === 'error' ? 'Could not complete search.' : `Displaying Node(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`}
ShowLoading={status === 'loading'} ResultNote={status === 'error' ? 'Could not complete search.' : `Displaying TaskRunner(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`}
StorageID="NodesFilter"
>
</SearchBar>
Expand All @@ -87,6 +155,7 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
}}
Selected={(item) => false}
KeySelector={(item) => item.ID}
OnClick={(data) => { setSelectedNode(data.row); setShowModal(true) } }
>
<Column<SC.Node>
Key={'Name'}
Expand All @@ -110,7 +179,7 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
Field={'MinimumHostCount'}
HeaderStyle={{ width: 'auto' }}
RowStyle={{ width: 'auto' }}
> Minimum Host Count
> Minimum Node Count
</Column>
<Column<SC.Node>
Key={'HostRegistrationKey'}
Expand All @@ -119,8 +188,8 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
HeaderStyle={{ width: 'auto' }}
RowStyle={{ width: 'auto' }}
Content={({ item, field }) => {
return <a href={`${homePath}index.cshtml?name=AppHost`} target='_blank'> <span className="badge badge-light">{item[field]}</span></a> }}
> Host Registration Key
return item[field] === 'N/A' ? item[field] : <a href={`${homePath}index.cshtml?name=AppHost`} target='_blank'> <span className='badge badge-light'>{item[field]}</span></a> }}
> Node
</Column>
<Column<SC.Node>
Key={'AssignedHostRegistrationKey'}
Expand All @@ -129,8 +198,9 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
HeaderStyle={{ width: 'auto' }}
RowStyle={{ width: 'auto' }}
Content={({ item, field }) => {
return <a href={`${homePath}index.cshtml?name=AppHost`} target='_blank'> <span className="badge badge-light">{item[field]}</span></a> }}
> Assigned Host Registration Key
return item[field] === 'N/A' ? item[field] : <a href={`${homePath}index.cshtml?name=AppHost`} target='_blank'> <span className='badge badge-light'>{item[field]}</span></a>
}}
> Assigned Nodes
</Column>
</Table>
</div>
Expand All @@ -140,6 +210,27 @@ const ByNode = (props: {Roles: Application.Types.SecurityRoleName[]}) => {
</div>
</div>
</div>
<Modal Show={showModal} Title={'Edit Task Runner'} CallBack={(c) => {
setShowModal(false);
if (!c)
return;
new GenericController<IOpenXDANode>(`${homePath}api/OpenXDA/Node`, 'ID').DBAction('PATCH', convertToXDANode(selectedNode)).then(() => refreshData(x => x + 1))
}}
ShowCancel={false}
ShowX={true}
DisableConfirm={errors.length > 0}
ConfirmShowToolTip={errors.length > 0}
ConfirmToolTipContent={errors.map((t, i) => <p key={i}> <ReactIcons.CrossMark Color='var(--danger)' /> {t}</p>)} >
<div className="row">
<NodeForm
Node={selectedNode}
stateSetter={setSelectedNode}
setErrors={setErrors}
NodeTypeOptions={nodeTypes.map((n) => { return { Value: n.Name, Label: n.Name } })}
HostOptions={appHosts.map((h) => { return { Value: h.RegistrationKey, Label: h.RegistrationKey } })}
/>
</div>
</Modal>
</div>
}
export default ByNode;
export default ByNode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//******************************************************************************************************
// NodeForm.tsx - Gbtc
//
// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/17/2026 - Natalie Beatty
// Generated original version of source code.
//
//******************************************************************************************************

import * as React from 'react';
import { useAppSelector } from '../hooks';
import { Gemstone } from '@gpa-gemstone/application-typings'
import { Input, Select, MultiCheckBoxSelect } from '@gpa-gemstone/react-forms'
import { SelectRoles } from '../Store/UserSettings';
import { SystemCenter as SC } from '../global'

interface IProps {
Node: SC.Node,
stateSetter: (customer: SC.Node) => void,
setErrors?: (e: string[]) => void,
NodeTypeOptions: Gemstone.TSX.Interfaces.ILabelValue<string>[]
HostOptions: Gemstone.TSX.Interfaces.ILabelValue<string>[]
}


const NodeForm = (props: IProps) => {
const roles = useAppSelector(SelectRoles);
function hasPermissions(): boolean {
if (roles.indexOf('Administrator') < 0 && roles.indexOf('Engineer') < 0)
return false;
return true;
}
function valid(field: keyof (SC.Node)): boolean {
if (field == 'Name')
return props.Node.Name != null // && props.Customer.CustomerKey.length > 0 && props.Customer.CustomerKey.length <= 25;
if (field == 'MinimumHostCount')
return props.Node.MinimumHostCount > 0 && props.Node.MinimumHostCount < 100
return true;
}


return <div className="col">
<Input<SC.Node> Record={props.Node} Field={'Name'} Label='Name' Feedback={'A unique Key of less than 25 characters is required.'} Valid={valid} Setter={(record) => props.stateSetter(record)} Disabled={!hasPermissions()} />
<Input<SC.Node> Type={'number'} Record={props.Node} Field={'MinimumHostCount'} Label='Minimum Node Count' Feedback='A number between 0 and 100 is required.' Valid={valid} Setter={(record) => props.stateSetter(record)} Disabled={!hasPermissions()} />
<Select<SC.Node> Options={props.NodeTypeOptions} Record={props.Node} Field={'NodeType'} Setter={(record) => props.stateSetter(record)} />
<Select<SC.Node> Record={props.Node} Options={props.HostOptions} Field={'HostRegistrationKey'} Label={'Node'} EmptyOption={true} Setter={(record) => props.stateSetter(record)} />
<Select<SC.Node> Record={props.Node} Options={props.HostOptions} Field={'AssignedHostRegistrationKey'} Label={'Assigned Nodes'} EmptyOption={true} Setter={(record) => props.stateSetter(record)} />
</div>
}

export default NodeForm;
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const SystemCenter: React.FunctionComponent = (props: {}) => {
</Section>
<Section Label={"SYSTEM SETTINGS"} Style={{ marginLeft: "10px" }} RequiredRoles={["Administrator"]}>
<Page Name={"index.cshtml?name=AppHost"} Label={"App Hosts"} />
<Page Name={"index.cshtml?name=Nodes"} Label={"Nodes"} />
<Page Name={"index.cshtml?name=TaskRunners"} Label={"Task Runners"} />
<Page Name={"index.cshtml?name=Settings&System=SystemCenter"} Label={"System Center"} />
<Page Name={"index.cshtml?name=Settings&System=OpenXDA"} Label={"openXDA"} />
<Page Name={"index.cshtml?name=Settings&System=SEBrowser"} Label={"PQ Browser"} />
Expand Down