diff --git a/TrenDAP.sql b/TrenDAP.sql index 9a395a69..5b23c3f0 100644 --- a/TrenDAP.sql +++ b/TrenDAP.sql @@ -51,58 +51,66 @@ CREATE TABLE Setting ) GO -CREATE TABLE DataSourceType +CREATE TABLE DataSet ( ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, - Name VARCHAR(200) NOT NULL, + [Context] VARCHAR(11) NOT NULL, + [RelativeValue] FLOAT NOT NULL, + [RelativeWindow] VARCHAR(5) NOT NULL, + [From] Date NOT NULL, + [To] Date NOT NULL, + [Hours] INT NOT NULL, + [Days] INT NOT NULL, + [Weeks] BIGINT NOT NULL, + [Months] INT NOT NULL, + [Name] VARCHAR(200) NOT NULL, + [User] VARCHAR(MAX) NOT NULL, + [Public] bit NULL DEFAULT 0, + UpdatedOn DATETIME NULL DEFAULT GETUTCDATE() ) GO -INSERT INTO DataSourceType (Name) VALUES ('TrenDAPDB') -GO -INSERT INTO DataSourceType (Name) VALUES ('OpenHistorian') -GO -INSERT INTO DataSourceType (Name) VALUES ('Sapphire') -GO - CREATE TABLE DataSource ( ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, Name VARCHAR(200) NULL, [User] VARCHAR(MAX) NOT NULL, - DataSourceTypeID INT NOT NULL REFERENCES DataSourceType(ID), + Type VARCHAR(50) NOT NULL, URL VARCHAR(MAX) NULL, [Public] bit NULL DEFAULT 0, RegistrationKey VARCHAR(50) NOT NULL UNIQUE, APIToken VARCHAR(50) NOT NULL, - Expires DATETIME NULL, SettingsString VARCHAR(MAX) NOT NULL DEFAULT '{}' ) GO -CREATE TABLE DataSet +CREATE TABLE DataSourceDataSet ( ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, - [Context] VARCHAR(11) NOT NULL, - [RelativeValue] FLOAT NOT NULL, - [RelativeWindow] VARCHAR(5) NOT NULL, - [From] Date NOT NULL, - [To] Date NOT NULL, - [Hours] INT NOT NULL, - [Days] INT NOT NULL, - [Weeks] BIGINT NOT NULL, - [Months] INT NOT NULL, - [Name] VARCHAR(200) NOT NULL, + DataSourceID INT NOT NULL REFERENCES DataSource(ID), + DataSetID INT NOT NULL REFERENCES DataSet(ID), + SettingsString VARCHAR(MAX) NOT NULL DEFAULT '{}' +) +GO + +CREATE TABLE EventSource +( + ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, + Name VARCHAR(200) NULL, [User] VARCHAR(MAX) NOT NULL, + Type VARCHAR(50) NOT NULL, + URL VARCHAR(MAX) NULL, [Public] bit NULL DEFAULT 0, - UpdatedOn DATETIME NULL DEFAULT GETUTCDATE() + RegistrationKey VARCHAR(50) NOT NULL, + APIToken VARCHAR(50) NOT NULL, + SettingsString VARCHAR(MAX) NOT NULL DEFAULT '{}' ) GO -CREATE TABLE DataSourceDataSet +CREATE TABLE EventSourceDataSet ( ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, - DataSourceID INT NOT NULL REFERENCES DataSource(ID), + EventSourceID INT NOT NULL REFERENCES EventSource(ID), DataSetID INT NOT NULL REFERENCES DataSet(ID), SettingsString VARCHAR(MAX) NOT NULL DEFAULT '{}' ) @@ -137,8 +145,6 @@ GO INSERT INTO ApplicationRole(Name, Description) VALUES('Administrator', 'Admin Role') GO - - CREATE TABLE SecurityGroup ( ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() PRIMARY KEY, diff --git a/TrenDAP/Attributes/CustomViewAttribute.cs b/TrenDAP/Attributes/CustomViewAttribute.cs new file mode 100644 index 00000000..30585bc7 --- /dev/null +++ b/TrenDAP/Attributes/CustomViewAttribute.cs @@ -0,0 +1,51 @@ +//****************************************************************************************************** +// ParentKeyAttribute.cs - Gbtc +// +// Copyright © 2021, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/01/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace GSF.Data.Model +{ + /// + /// Defines an attribute that will allow setting a custom view a modeled table. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class CustomViewAttribute : Attribute + { + /// + /// Gets field name to use for property. + /// + public string CustomView + { + get; + } + + /// + /// Creates a new . + /// + /// SQL to generate custom view. + public CustomViewAttribute(string customView) + { + CustomView = customView; + } + } +} \ No newline at end of file diff --git a/TrenDAP/Attributes/ParentKeyAttribute.cs b/TrenDAP/Attributes/ParentKeyAttribute.cs new file mode 100644 index 00000000..496acce7 --- /dev/null +++ b/TrenDAP/Attributes/ParentKeyAttribute.cs @@ -0,0 +1,52 @@ +//****************************************************************************************************** +// ParentKeyAttribute.cs - Gbtc +// +// Copyright © 2021, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/01/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace TrenDAP.Attributes +{ + /// + /// Defines an attribute that will allow a foreign key in the model to point back to parent table + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class ParentKeyAttribute : Attribute + { + /// + /// Gets field name to use for property. + /// + public Type Model + { + get; + } + + + /// + /// Creates a new . + /// + /// Type of modeled table that key points back to. + public ParentKeyAttribute(Type model) + { + Model = model; + } + } +} \ No newline at end of file diff --git a/TrenDAP/Controllers/OpenHistorianController.cs b/TrenDAP/Controllers/Datasources/OpenHistorianController.cs similarity index 91% rename from TrenDAP/Controllers/OpenHistorianController.cs rename to TrenDAP/Controllers/Datasources/OpenHistorianController.cs index 8441ec76..3d12fc68 100644 --- a/TrenDAP/Controllers/OpenHistorianController.cs +++ b/TrenDAP/Controllers/Datasources/OpenHistorianController.cs @@ -173,16 +173,15 @@ public OpenHistorianController(IConfiguration configuration) #region [ Http Methods ] [HttpGet, Route("{dataSourceID:int}/{table?}")] - public virtual ActionResult Get(int dataSourceID, string table = "") + public virtual ActionResult GetTable(int dataSourceID, string table = "") { using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) { try { DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceID); - DataSourceHelper helper = new DataSourceHelper(dataSource); - Task rsp = helper.GetAsync($"api/trendap/{table}"); - return Ok(rsp.Result); + if (dataSource.Type == "OpenHistorian") return GetOpenXDA(dataSource, table); + else return StatusCode(StatusCodes.Status500InternalServerError, "Only TrenDAPDB datasources are supported by this endpoint."); } catch (Exception ex) { @@ -215,6 +214,22 @@ public Task Query(int dataSourceID, Post post, Cancellation #region [ Static ] + private ActionResult GetOpenXDA(DataSource dataSource, string table, JObject filter = null) + { + try + { + DataSourceHelper helper = new DataSourceHelper(dataSource); + Task rsp; + if (filter is null) rsp = helper.GetAsync($"api/{table}"); + else rsp = helper.PostAsync($"api/{table}/SearchableList", new StringContent(filter.ToString(), Encoding.UTF8, "application/json")); + return Ok(rsp.Result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex); + } + } + public static Task Query(int dataSourceID, Post post, IConfiguration configuration, CancellationToken cancellationToken) { using (AdoDataConnection connection = new AdoDataConnection(configuration["SystemSettings:ConnectionString"], configuration["SystemSettings:DataProviderString"])) diff --git a/TrenDAP/Controllers/SapphireController.cs b/TrenDAP/Controllers/Datasources/SapphireController.cs similarity index 100% rename from TrenDAP/Controllers/SapphireController.cs rename to TrenDAP/Controllers/Datasources/SapphireController.cs diff --git a/TrenDAP/Controllers/TrenDAPDBController.cs b/TrenDAP/Controllers/Datasources/TrenDAPDBController.cs similarity index 86% rename from TrenDAP/Controllers/TrenDAPDBController.cs rename to TrenDAP/Controllers/Datasources/TrenDAPDBController.cs index 872f12cf..de5b0a2a 100644 --- a/TrenDAP/Controllers/TrenDAPDBController.cs +++ b/TrenDAP/Controllers/Datasources/TrenDAPDBController.cs @@ -61,7 +61,6 @@ public class PostData public string OrderBy { get; set; } public bool Ascending { get; set; } } - public class XDADataSetData { public string By { get; set; } @@ -95,22 +94,16 @@ public TrenDAPDBController(IConfiguration configuration) #endregion #region [ Http Methods ] - [HttpGet, Route("{dataSourceID:int}/{table?}")] - public virtual ActionResult GetTable(int dataSourceID, string table = "") + [HttpGet, Route("{sourceID:int}/{table?}")] + public virtual ActionResult GetTable(int sourceID, string table = "") { using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) { - try { - DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceID); - string type = connection.ExecuteScalar("SELECT Name FROM DataSourceType WHERE ID = {0}", dataSource.DataSourceTypeID); - - if (type == "TrenDAPDB") - return GetOpenXDA(dataSource, table); - else if (type == "OpenHistorian") - return GetOpenHistorian(dataSource, table); - else return StatusCode(StatusCodes.Status400BadRequest, "Datasource type not supported"); + DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", sourceID); + if (dataSource.Type == "TrenDAPDB") return GetOpenXDA(dataSource, table); + else return StatusCode(StatusCodes.Status500InternalServerError, "Only TrenDAPDB datasources are supported by this endpoint."); } catch (Exception ex) { @@ -128,12 +121,11 @@ public virtual ActionResult GetChannels(int dataSourceID, [FromBody] JObject fil { DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceID); DataSourceHelper helper = new DataSourceHelper(dataSource); - string type = connection.ExecuteScalar("SELECT Name FROM DataSourceType WHERE ID = {0}", dataSource.DataSourceTypeID); Task rsp; - if (type == "TrenDAPDB") + if (dataSource.Type == "TrenDAPDB") { - rsp = helper.PostAsync("api/Channel/GetTrendSearchData", new StringContent(filter.ToString(), Encoding.UTF8, "application/json")); + rsp = helper.PostAsync("api/Channel/TrenDAP", new StringContent(filter.ToString(), Encoding.UTF8, "application/json")); } else return StatusCode(StatusCodes.Status500InternalServerError, "Only TrenDAPDB datasources supported by this endpoint."); @@ -145,7 +137,7 @@ public virtual ActionResult GetChannels(int dataSourceID, [FromBody] JObject fil } } } - + private ActionResult GetOpenXDA(DataSource dataSource, string table, JObject filter = null) { try @@ -162,20 +154,6 @@ private ActionResult GetOpenXDA(DataSource dataSource, string table, JObject fil } } - private ActionResult GetOpenHistorian(DataSource dataSource, string table) - { - try - { - DataSourceHelper helper = new DataSourceHelper(dataSource); - Task rsp = helper.GetAsync($"api/trendap/{table}"); - return Ok(rsp.Result); - } - catch (Exception ex) - { - return StatusCode(StatusCodes.Status500InternalServerError, ex); - } - } - [HttpGet, Route("HIDS/{dataSourceID:int}/{dataSetID:int}")] public ActionResult Get(int dataSourceID, int dataSetID, CancellationToken cancellationToken) diff --git a/TrenDAP/Controllers/EventSources/OpenXDAController.cs b/TrenDAP/Controllers/EventSources/OpenXDAController.cs new file mode 100644 index 00000000..c4898323 --- /dev/null +++ b/TrenDAP/Controllers/EventSources/OpenXDAController.cs @@ -0,0 +1,88 @@ +//****************************************************************************************************** +// TrenDAPDBController.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 10/07/2020 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; +using HIDS; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TrenDAP.Model; +using AdoDataConnection = Gemstone.Data.AdoDataConnection; +using DataSet = TrenDAP.Model.DataSet; +namespace TrenDAP.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class OpenXDAController : ControllerBase + { + public OpenXDAController(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + [HttpGet, Route("{sourceID:int}/{table?}")] + public virtual ActionResult GetTable(int sourceID, string table = "") + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) + { + try + { + EventSource eventSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", sourceID); + if (eventSource.Type == "OpenXDA") return GetOpenXDA(eventSource, table); + else return StatusCode(StatusCodes.Status500InternalServerError, "Only OpenXDA eventsources are supported by this endpoint."); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex); + } + } + } + + private ActionResult GetOpenXDA(EventSource eventSource, string table, JObject filter = null) + { + try + { + EventSourceHelper helper = new EventSourceHelper(eventSource); + Task rsp; + if (filter is null) rsp = helper.GetAsync($"api/{table}"); + else rsp = helper.PostAsync($"api/{table}/SearchableList", new StringContent(filter.ToString(), Encoding.UTF8, "application/json")); + return Ok(rsp.Result); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex); + } + } + } +} diff --git a/TrenDAP/Controllers/ModelController.cs b/TrenDAP/Controllers/ModelController.cs index 3b789a6f..4e26a6be 100644 --- a/TrenDAP/Controllers/ModelController.cs +++ b/TrenDAP/Controllers/ModelController.cs @@ -24,14 +24,17 @@ using Gemstone.Data; using Gemstone.Data.Model; using Gemstone.Reflection.MemberInfoExtensions; +using GSF.Data.Model; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Reflection; +using TrenDAP.Attributes; using TrenDAP.Model; namespace TrenDAP.Controllers @@ -66,26 +69,9 @@ public class PostData public ModelController(IConfiguration configuration) { Configuration = configuration; - } - - public ModelController(IConfiguration configuration, bool hasParent, string parentKey, string primaryKeyField = "ID") - { - Configuration = configuration; - HasParent = hasParent; - ParentKey = parentKey; - HasUniqueKey = false; - UniqueKeyField = ""; - PrimaryKeyField = "ID"; - - } - - public ModelController(IConfiguration configuration, bool hasParent, string parentKey, bool hasUniqueKey, string uniqueKey) - { - Configuration = configuration; - HasParent = hasParent; - ParentKey = parentKey; - HasUniqueKey = hasUniqueKey; - UniqueKeyField = uniqueKey; + PrimaryKeyField = typeof(T).GetProperties().FirstOrDefault(p => p.GetCustomAttributes().Any())?.Name ?? "ID"; + ParentKey = typeof(T).GetProperties().FirstOrDefault(p => p.GetCustomAttributes().Any())?.Name ?? ""; + CustomView = typeof(T).GetCustomAttribute()?.CustomView ?? ""; } #endregion @@ -93,7 +79,6 @@ public ModelController(IConfiguration configuration, bool hasParent, string pare #region [ Properties ] protected IConfiguration Configuration { get; } - protected virtual bool HasParent { get; } = false; protected virtual string ParentKey { get; } = ""; protected virtual string PrimaryKeyField { get; } = "ID"; protected virtual bool HasUniqueKey { get; } = false; @@ -103,6 +88,7 @@ public ModelController(IConfiguration configuration, bool hasParent, string pare protected virtual string PostRoles { get; } = "Administrator"; protected virtual string PatchRoles { get; } = "Administrator"; protected virtual string DeleteRoles { get; } = "Administrator"; + protected virtual string CustomView { get; } = ""; protected virtual string GetOrderByExpression { get; } = null; #endregion @@ -143,18 +129,18 @@ public virtual ActionResult> Get(string parentID = null) try { IEnumerable result; - if (HasParent && parentID != null) + if (!string.IsNullOrEmpty(ParentKey) && parentID != null) { PropertyInfo parentKey = typeof(T).GetProperty(ParentKey); if (parentKey.PropertyType == typeof(int)) - result = new TableOperations(connection).QueryRecords(GetOrderByExpression, new RecordRestriction(ParentKey + " = {0}", int.Parse(parentID))); + result = QueryRecordsWhere(null, false, ParentKey + " = {0}", int.Parse(parentID)); else if (parentKey.PropertyType == typeof(Guid)) - result = new TableOperations(connection).QueryRecords(GetOrderByExpression, new RecordRestriction(ParentKey + " = {0}", Guid.Parse(parentID))); + result = QueryRecordsWhere(null, false, ParentKey + " = {0}", Guid.Parse(parentID)); else - result = new TableOperations(connection).QueryRecords(GetOrderByExpression, new RecordRestriction(ParentKey + " = {0}", parentID)); + result = QueryRecordsWhere(null, false, ParentKey + " = {0}", parentID); } else - result = new TableOperations(connection).QueryRecords(GetOrderByExpression); + result = QueryRecordsWhere(null, false, null); return Ok(result); } @@ -182,11 +168,11 @@ public virtual ActionResult GetOne(string id) T result = null; PropertyInfo primaryKey = typeof(T).GetProperty(PrimaryKeyField); if (primaryKey.PropertyType == typeof(int)) - result = new TableOperations(connection).QueryRecordWhere(PrimaryKeyField + " = {0}", int.Parse(id)); + result = QueryRecordWhere(PrimaryKeyField + " = {0}", int.Parse(id)); else if (primaryKey.PropertyType == typeof(Guid)) - result = new TableOperations(connection).QueryRecordWhere(PrimaryKeyField + " = {0}", Guid.Parse(id)); + result = QueryRecordWhere(PrimaryKeyField + " = {0}", Guid.Parse(id)); else - result = new TableOperations(connection).QueryRecordWhere(PrimaryKeyField + " = {0}", id); + result = QueryRecordWhere(PrimaryKeyField + " = {0}", id); if (result == null) { @@ -214,12 +200,68 @@ public virtual ActionResult GetOne(string id) } + protected virtual IEnumerable QueryRecordsWhere(string orderBy, bool ascending, string filterExpression, params object[] parameters) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration[SettingCategory + ":ConnectionString"], Configuration[SettingCategory + ":DataProviderString"])) + { + RecordRestriction restriction = string.IsNullOrEmpty(filterExpression) ? null : new RecordRestriction(filterExpression, parameters); + if (string.IsNullOrEmpty(CustomView)) + return new TableOperations(connection).QueryRecords(null, restriction); + + string flt = filterExpression; + + string orderString = null; + if (!string.IsNullOrEmpty(orderBy)) + orderString = orderBy + (ascending ? " ASC" : " DESC"); + + string sql = $@" + SELECT * FROM + ({CustomView}) FullTbl + {(string.IsNullOrEmpty(filterExpression) ? "" : $"WHERE { flt}")} + {(string.IsNullOrEmpty(orderBy) ? "" : " ORDER BY " + orderString)}"; + + + DataTable dataTbl = connection.RetrieveData(sql, parameters); + + List result = new List(); + TableOperations tblOperations = new TableOperations(connection); + foreach (DataRow row in dataTbl.Rows) + { + result.Add(tblOperations.LoadRecord(row)); + } + return result; + } + } + + protected virtual T QueryRecordWhere(string filterExpression, params object[] parameters) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration[SettingCategory + ":ConnectionString"], Configuration[SettingCategory + ":DataProviderString"])) + { + if (CustomView == String.Empty) + return new TableOperations(connection).QueryRecordWhere(filterExpression, parameters); + + string whereClause = filterExpression; + object[] param = parameters; + + whereClause = " WHERE " + whereClause; + string sql = "SELECT * FROM (" + CustomView + ") FullTbl"; + DataTable dataTbl = connection.RetrieveData(sql + whereClause, param); + + TableOperations tblOperations = new TableOperations(connection); + if (dataTbl.Rows.Count > 0) + return tblOperations.LoadRecord(dataTbl.Rows[0]); + return null; + + } + } + [HttpPost] public virtual ActionResult Post([FromBody] JObject record) { try { + if (CustomView != String.Empty) return BadRequest(); if (PostRoles == string.Empty || User.IsInRole(PostRoles)) { using (AdoDataConnection connection = new AdoDataConnection(Configuration[SettingCategory + ":ConnectionString"], Configuration[SettingCategory + ":DataProviderString"])) @@ -258,6 +300,7 @@ public virtual ActionResult Patch([FromBody] JObject record) { try { + if (CustomView != String.Empty) return BadRequest(); if (PatchRoles == string.Empty || User.IsInRole(PatchRoles)) { @@ -287,6 +330,7 @@ public virtual ActionResult Delete(T record) { try { + if (CustomView != String.Empty) return BadRequest(); if (DeleteRoles == string.Empty || User.IsInRole(DeleteRoles)) { diff --git a/TrenDAP/Model/DataSet.cs b/TrenDAP/Model/DataSet.cs index e37cfd14..0670020f 100644 --- a/TrenDAP/Model/DataSet.cs +++ b/TrenDAP/Model/DataSet.cs @@ -32,7 +32,6 @@ using System.Collections.Generic; using System.Data; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using TrenDAP.Controllers; @@ -46,6 +45,7 @@ namespace TrenDAP.Model public class DataSet { + #region [ Fields ] [PrimaryKey(true)] public int ID { get; set; } [UseEscapedName] @@ -73,6 +73,28 @@ public class DataSet [UseEscapedName] public bool Public { get; set; } public DateTime UpdatedOn { get; set; } + #endregion + + #region [ Methods ] + public Tuple ComputeTimeEnds() + { + DateTime startTime = From; + DateTime endTime = To; + if (Context == "Relative") + { + endTime = DateTime.Now; + if (RelativeWindow == "Day") + startTime = endTime.AddDays(-RelativeValue); + else if (RelativeWindow == "Week") + startTime = endTime.AddDays(-RelativeValue * 7); + else if (RelativeWindow == "Month") + startTime = endTime.AddMonths(-int.Parse(RelativeValue.ToString())); + else + startTime = endTime.AddYears(-int.Parse(RelativeValue.ToString())); + } + return new Tuple(startTime, endTime); + } + #endregion } public class DataSetController : ModelController @@ -94,14 +116,80 @@ public ActionResult PostConnections([FromBody] JObject record) dataSetRecord.User = Request.HttpContext.User.Identity.Name; int result = new TableOperations(connection).AddNewRecord(dataSetRecord); int dataSetId = connection.ExecuteScalar("SELECT @@IDENTITY"); - JArray connections = (JArray)record.GetValue("Connections"); - foreach (JObject conn in connections) + JArray dataConnections = (JArray)record.GetValue("DataConnections"); + foreach (JObject conn in dataConnections) { conn["SettingsString"] = record["Settings"].ToString(); conn["DataSetID"] = dataSetId; DataSourceDataSet connRecord = conn.ToObject(); result += new TableOperations(connection).AddNewRecord(connRecord); } + JArray eventConnections = (JArray)record.GetValue("EventConnections"); + foreach (JObject conn in eventConnections) + { + conn["SettingsString"] = record["Settings"].ToString(); + conn["DataSetID"] = dataSetId; + EventSourceDataSet connRecord = conn.ToObject(); + result += new TableOperations(connection).AddNewRecord(connRecord); + } + return Ok(result); + } + } + + [HttpPost, Route("UpdateWithConnections")] + public ActionResult PostByDataSet([FromBody] JObject postData) + { + DataSet dataSet = postData["DataSet"].ToObject(); + JArray dataConenctions = (JArray)postData["DataConnections"]; + IEnumerable newDataRecords = dataConenctions.Select(recordToken => + { + JObject record = (JObject)recordToken; + record["SettingsString"] = record["Settings"].ToString(); + DataSourceDataSet recordObj = record.ToObject(); + return recordObj; + }); + JArray eventConenctions = (JArray)postData["EventConnections"]; + IEnumerable newEventRecords = eventConenctions.Select(recordToken => + { + JObject record = (JObject)recordToken; + record["SettingsString"] = record["Settings"].ToString(); + EventSourceDataSet recordObj = record.ToObject(); + return recordObj; + }); + using (AdoDataConnection connection = new AdoDataConnection(Configuration[SettingCategory + ":ConnectionString"], Configuration[SettingCategory + ":DataProviderString"])) + { + // Handle Data Set Changes + int result = new TableOperations(connection).UpdateRecord(dataSet); + // Handle Data Records + TableOperations dataTbl = new TableOperations(connection); + IEnumerable currentDataRecords = dataTbl.QueryRecordsWhere("DataSetID = {0}", dataSet.ID); + foreach (DataSourceDataSet newRecord in newDataRecords) + { + if (newRecord.ID >= 0) + { + currentDataRecords = currentDataRecords.Where(rec => rec.ID != newRecord.ID); + result += dataTbl.UpdateRecord(newRecord); + } + else + result += dataTbl.AddNewRecord(newRecord); + } + foreach (DataSourceDataSet removedRecord in currentDataRecords) + result += dataTbl.DeleteRecord(removedRecord); + // Handle Event Records + TableOperations eventTbl = new TableOperations(connection); + IEnumerable currentEventRecords = eventTbl.QueryRecordsWhere("DataSetID = {0}", dataSet.ID); + foreach (EventSourceDataSet newRecord in newEventRecords) + { + if (newRecord.ID >= 0) + { + currentEventRecords = currentEventRecords.Where(rec => rec.ID != newRecord.ID); + result += eventTbl.UpdateRecord(newRecord); + } + else + result += eventTbl.AddNewRecord(newRecord); + } + foreach (EventSourceDataSet removedRecord in currentEventRecords) + result += eventTbl.DeleteRecord(removedRecord); return Ok(result); } } diff --git a/TrenDAP/Model/DataSource.cs b/TrenDAP/Model/DataSource.cs index 7a346dc3..9ab427c0 100644 --- a/TrenDAP/Model/DataSource.cs +++ b/TrenDAP/Model/DataSource.cs @@ -23,6 +23,7 @@ using Gemstone.Data; using Gemstone.Data.Model; +using InfluxDB.Client.Api.Domain; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -45,13 +46,12 @@ public class DataSource { [PrimaryKey(true)] public int ID { get; set; } + public string Type { get; set; } public string Name { get; set; } - public int DataSourceTypeID { get; set; } public string URL { get; set; } // Todo: maybe we want to break datasource from api auth? two tables where a source is linked to an auth row? public string RegistrationKey { get; set; } public string APIToken { get; set; } - public DateTime? Expires { get; set; } [UseEscapedName] public bool Public { get; set; } [UseEscapedName] @@ -75,92 +75,6 @@ public static DataSource GetDataSource(IConfiguration configuration, int id) } } - public class DataSourceController: ModelController - { - public DataSourceController(IConfiguration configuration) : base(configuration){ } - - public override ActionResult Post([FromBody] JObject record) - { - record["SettingsString"] = record["Settings"].ToString(); - return base.Post(record); - } - public override ActionResult Patch([FromBody] JObject record) - { - record["SettingsString"] = record["Settings"].ToString(); - return base.Patch(record); - } - - [HttpGet, Route("TestAuth/{dataSourceID:int}")] - public ActionResult TestAuth(int dataSourceID) - { - using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) - { - try - { - DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceID); - DataSourceHelper helper = new DataSourceHelper(dataSource); - HttpResponseMessage rsp = helper.GetResponseTask($"api/TestAuth").Result; - switch (rsp.StatusCode) - { - default: - case HttpStatusCode.Unauthorized: - return Ok("Failed to authorize with datasource credentials."); - case HttpStatusCode.NotFound: - return Ok("Unable to find datasource."); - case HttpStatusCode.OK: - return Ok("1"); - } - } - catch (Exception ex) - { - return StatusCode(StatusCodes.Status500InternalServerError, ex); - } - } - } - } - - public class DataSourceType - { - [PrimaryKey(true)] - public int ID { get; set; } - public string Name { get; set; } - } - - - public class DataSourceTypeController : ModelController - { - public DataSourceTypeController(IConfiguration configuration) : base(configuration) {} - } - - public class RspConverter : IActionResult - { - Task rspTask; - public RspConverter(Task rsp) - { - rspTask = rsp; - } - //Note for reviewer: This should just be copying the stream over and sending it out. Idea is to sidestep loading all data server-side, then send it out. - public async Task ExecuteResultAsync(ActionContext context) - { - HttpResponseMessage rsp = rspTask.Result; - context.HttpContext.Response.StatusCode = (int)rsp.StatusCode; - - // Have to clear chunking header, results in errors client side otherwise - if (rsp.Headers.TransferEncodingChunked == true && rsp.Headers.TransferEncoding.Count == 1) rsp.Headers.TransferEncoding.Clear(); - - foreach (KeyValuePair> header in rsp.Headers) context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); - - if (rsp.Content != null) - { - using (var stream = await rsp.Content.ReadAsStreamAsync()) - { - await stream.CopyToAsync(context.HttpContext.Response.Body); - await context.HttpContext.Response.Body.FlushAsync(); - } - } - } - } - public class DataSourceHelper : XDAAPIHelper { private DataSource m_dataSource; @@ -169,13 +83,13 @@ public DataSourceHelper(IConfiguration config, int dataSourceId) { using (AdoDataConnection connection = new AdoDataConnection(config["SystemSettings:ConnectionString"], config["SystemSettings:DataProviderString"])) { - m_dataSource = new Gemstone.Data.Model.TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceId); + m_dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceId); } } - public DataSourceHelper(DataSource dataSource) + public DataSourceHelper(DataSource source) { - m_dataSource = dataSource; + m_dataSource = source; } protected override string Token @@ -208,4 +122,48 @@ public IActionResult GetActionResult(string requestURI, HttpContent content = nu return new RspConverter(rsp); } } + + public class DataSourceController: ModelController + { + public DataSourceController(IConfiguration configuration) : base(configuration){ } + + public override ActionResult Post([FromBody] JObject record) + { + record["SettingsString"] = record["Settings"].ToString(); + return base.Post(record); + } + public override ActionResult Patch([FromBody] JObject record) + { + record["SettingsString"] = record["Settings"].ToString(); + return base.Patch(record); + } + + [HttpGet, Route("TestAuth/{dataSourceID:int}")] + public ActionResult TestAuth(int dataSourceID) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) + { + try + { + DataSource dataSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceID); + DataSourceHelper helper = new DataSourceHelper(dataSource); + HttpResponseMessage rsp = helper.GetResponseTask($"api/TestAuth").Result; + switch (rsp.StatusCode) + { + default: + case HttpStatusCode.Unauthorized: + return Ok("Failed to authorize with datasource credentials."); + case HttpStatusCode.NotFound: + return Ok("Unable to find datasource."); + case HttpStatusCode.OK: + return Ok("1"); + } + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex); + } + } + } + } } diff --git a/TrenDAP/Model/DataSourceDataSet.cs b/TrenDAP/Model/DataSourceDataSet.cs index d9f92a8f..adea30c1 100644 --- a/TrenDAP/Model/DataSourceDataSet.cs +++ b/TrenDAP/Model/DataSourceDataSet.cs @@ -34,6 +34,9 @@ using System.Threading; using Gemstone.Data; using System.Linq; +using TrenDAP.Attributes; +using GSF.Data.Model; +using System.Reflection; namespace TrenDAP.Model { @@ -42,6 +45,7 @@ public class DataSourceDataSet [PrimaryKey(true)] public int ID { get; set; } public int DataSourceID { get; set; } + [ParentKey(typeof(DataSet))] public int DataSetID { get; set; } public string SettingsString { get; set; } [NonRecordField] @@ -55,27 +59,34 @@ public JObject Settings } } - public class DataSourceDataSetController : ModelController + [CustomView(@" + SELECT + DataSourceDataSet.ID, + DataSourceDataSet.DataSourceID, + DataSourceDataSet.DataSetID, + DataSourceDataSet.SettingsString, + DataSource.Name as DataSourceName, + DataSet.Name as DataSetName + From + DataSourceDataSet LEFT JOIN + DataSource ON DataSourceDataSet.DataSourceID = DataSource.ID LEFT JOIN + DataSet ON DataSourceDataSet.DataSetID = DataSet.ID + ")] + public class DataSourceDataSetView : DataSourceDataSet + { + public string DataSourceName { get; set; } + public string DataSetName { get; set; } + } + + public class DataSourceDataSetController : ModelController { public DataSourceDataSetController(IConfiguration configuration) : base(configuration) { } - - public override ActionResult Post([FromBody] JObject record) - { - record["SettingsString"] = record["Settings"].ToString(); - return base.Post(record); - } - public override ActionResult Patch([FromBody] JObject record) - { - record["SettingsString"] = record["Settings"].ToString(); - return base.Patch(record); - } [HttpGet, Route("Query/{dataSourceDataSetID:int}")] public IActionResult GetData(int dataSourceDataSetID, CancellationToken cancellationToken) { using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) { - List dataSourceTypes = new TableOperations(connection).QueryRecords().ToList(); DataSourceDataSet sourceSet = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceDataSetID); if (sourceSet == null) return BadRequest($"Could not find source set relationship with ID {dataSourceDataSetID}"); DataSet dataSet = new TableOperations(connection).QueryRecordWhere("ID = {0}", sourceSet.DataSetID); @@ -89,11 +100,9 @@ private IActionResult Query(DataSet dataset, DataSource dataSource, JObject json { using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) { - List dataSourceTypes = new TableOperations(connection).QueryRecords().ToList(); - string type = dataSourceTypes.Find(dst => dst.ID == dataSource.DataSourceTypeID).Name; DataSourceHelper helper = new DataSourceHelper(dataSource); - if (type == "TrenDAPDB") + if (dataSource.Type == "TrenDAPDB") { HIDSPost postData = TrenDAPDBController.CreatePost(dataset, json.ToObject()); JObject jObj = (JObject)JToken.FromObject(postData); diff --git a/TrenDAP/Model/EventSource.cs b/TrenDAP/Model/EventSource.cs new file mode 100644 index 00000000..ed52896d --- /dev/null +++ b/TrenDAP/Model/EventSource.cs @@ -0,0 +1,164 @@ +//****************************************************************************************************** +// EventSource.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/23/2020 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using System; +using System.Net.Http; +using System.Net; +using TrenDAP.Controllers; +using PrimaryKeyAttribute = Gemstone.Data.Model.PrimaryKeyAttribute; +using openXDA.APIAuthentication; +using System.Threading.Tasks; + +namespace TrenDAP.Model +{ + public class EventSource + { + [PrimaryKey(true)] + public int ID { get; set; } + public string Type { get; set; } + public string Name { get; set; } + public string URL { get; set; } + // Todo: maybe we want to break source from api auth? two tables where a source is linked to an auth row? + public string RegistrationKey { get; set; } + public string APIToken { get; set; } + [UseEscapedName] + public bool Public { get; set; } + [UseEscapedName] + public string User { get; set; } + public string SettingsString { get; set; } + [NonRecordField] + public JObject Settings + { + get + { + try { return JObject.Parse(SettingsString); } + catch { return new JObject(); } + } + } + public static EventSource GetEventSource(IConfiguration configuration, int id) + { + using (AdoDataConnection connection = new AdoDataConnection(configuration["SystemSettings:ConnectionString"], configuration["SystemSettings:DataProviderString"])) + { + return new TableOperations(connection).QueryRecordWhere("ID = {0}", id); + } + } + } + + public class EventSourceHelper : XDAAPIHelper + { + private EventSource m_eventSource; + + public EventSourceHelper(IConfiguration config, int dataSourceId) + { + using (AdoDataConnection connection = new AdoDataConnection(config["SystemSettings:ConnectionString"], config["SystemSettings:DataProviderString"])) + { + m_eventSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", dataSourceId); + } + } + + public EventSourceHelper(EventSource source) + { + m_eventSource = source; + } + + protected override string Token + { + get + { + return m_eventSource.APIToken; + } + + } + protected override string Key + { + get + { + return m_eventSource.RegistrationKey; + } + + } + protected override string Host + { + get + { + return m_eventSource.URL; + } + } + + public IActionResult GetActionResult(string requestURI, HttpContent content = null) + { + Task rsp = GetResponseTask(requestURI, content); + return new RspConverter(rsp); + } + } + + public class EventSourceController: ModelController + { + public EventSourceController(IConfiguration configuration) : base(configuration){ } + + public override ActionResult Post([FromBody] JObject record) + { + record["SettingsString"] = record["Settings"].ToString(); + return base.Post(record); + } + public override ActionResult Patch([FromBody] JObject record) + { + record["SettingsString"] = record["Settings"].ToString(); + return base.Patch(record); + } + + [HttpGet, Route("TestAuth/{eventSourceID:int}")] + public ActionResult TestAuth(int eventSourceID) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) + { + try + { + EventSource eventSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventSourceID); + EventSourceHelper helper = new EventSourceHelper(eventSource); + HttpResponseMessage rsp = helper.GetResponseTask($"api/TestAuth").Result; + switch (rsp.StatusCode) + { + default: + case HttpStatusCode.Unauthorized: + return Ok("Failed to authorize with datasource credentials."); + case HttpStatusCode.NotFound: + return Ok("Unable to find datasource."); + case HttpStatusCode.OK: + return Ok("1"); + } + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex); + } + } + } + } +} diff --git a/TrenDAP/Model/EventSourceDataSet.cs b/TrenDAP/Model/EventSourceDataSet.cs new file mode 100644 index 00000000..b8102706 --- /dev/null +++ b/TrenDAP/Model/EventSourceDataSet.cs @@ -0,0 +1,112 @@ +//****************************************************************************************************** +// EventSourceDataSet.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/02/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using System; +using System.Text; +using TrenDAP.Controllers; +using System.Threading; +using Gemstone.Data; +using GSF.Data.Model; +using TrenDAP.Attributes; +using System.Net.Http; + +namespace TrenDAP.Model +{ + public class EventSourceDataSet + { + [PrimaryKey(true)] + public int ID { get; set; } + public int EventSourceID { get; set; } + [ParentKey(typeof(DataSet))] + public int DataSetID { get; set; } + public string SettingsString { get; set; } + [NonRecordField] + public JObject Settings + { + get + { + try { return JObject.Parse(SettingsString); } + catch { return new JObject(); } + } + } + } + + [CustomView(@" + SELECT + EventSourceDataSet.ID, + EventSourceDataSet.EventSourceID, + EventSourceDataSet.DataSetID, + EventSourceDataSet.SettingsString, + EventSource.Name as EventSourceName, + DataSet.Name as DataSetName + From + EventSourceDataSet LEFT JOIN + EventSource ON EventSourceDataSet.EventSourceID = EventSource.ID LEFT JOIN + DataSet ON EventSourceDataSet.DataSetID = DataSet.ID + ")] + public class EvenSourceDataSetView : EventSourceDataSet + { + public string EventSourceName { get; set; } + public string DataSetName { get; set; } + } + + public class EventSourceDataSetController : ModelController + { + public EventSourceDataSetController(IConfiguration configuration) : base(configuration) { } + + [HttpGet, Route("Query/{eventSourceDataSetID:int}")] + public IActionResult GetData(int eventSourceDataSetID, CancellationToken cancellationToken) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) + { + EventSourceDataSet sourceSet = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventSourceDataSetID); + if (sourceSet == null) return BadRequest($"Could not find source set relationship with ID {eventSourceDataSetID}"); + DataSet dataSet = new TableOperations(connection).QueryRecordWhere("ID = {0}", sourceSet.DataSetID); + EventSource eventSource = new TableOperations(connection).QueryRecordWhere("ID = {0}", sourceSet.EventSourceID); + if (dataSet is null || eventSource is null) return BadRequest("Failure loading event source or data set."); + return Query(dataSet, eventSource, sourceSet.Settings, cancellationToken); + } + } + + private IActionResult Query(DataSet dataset, EventSource eventSource, JObject json, CancellationToken cancellationToken) + { + using (AdoDataConnection connection = new AdoDataConnection(Configuration["SystemSettings:ConnectionString"], Configuration["SystemSettings:DataProviderString"])) + { + EventSourceHelper helper = new EventSourceHelper(eventSource); + if (eventSource.Type == "OpenXDA") + { + Tuple timeEnds = dataset.ComputeTimeEnds(); + json.Add("StartTime", timeEnds.Item1); + json.Add("EndTime", timeEnds.Item2); + return helper.GetActionResult("api/Event/TrenDAP", new StringContent(json.ToString(), Encoding.UTF8, "application/json")); + } + else + throw new ArgumentException($"Type of {eventSource.Type} not supported."); + } + } + } +} diff --git a/TrenDAP/Model/RspConverter.cs b/TrenDAP/Model/RspConverter.cs new file mode 100644 index 00000000..bb62f08e --- /dev/null +++ b/TrenDAP/Model/RspConverter.cs @@ -0,0 +1,61 @@ +//****************************************************************************************************** +// RspConverter.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/28/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace TrenDAP.Model +{ + public class RspConverter : IActionResult + { + Task rspTask; + public RspConverter(Task rsp) + { + rspTask = rsp; + } + //Note for reviewer: This should just be copying the stream over and sending it out. Idea is to sidestep loading all data server-side, then send it out. + public async Task ExecuteResultAsync(ActionContext context) + { + HttpResponseMessage rsp = rspTask.Result; + context.HttpContext.Response.StatusCode = (int)rsp.StatusCode; + + // Have to clear chunking header, results in errors client side otherwise + if (rsp.Headers.TransferEncodingChunked == true && rsp.Headers.TransferEncoding.Count == 1) rsp.Headers.TransferEncoding.Clear(); + + foreach (KeyValuePair> header in rsp.Headers) context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); + + if (rsp.Content != null) + { + using (var stream = await rsp.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(context.HttpContext.Response.Body); + await context.HttpContext.Response.Body.FlushAsync(); + } + } + } + } +} diff --git a/TrenDAP/TrenDAP.csproj b/TrenDAP/TrenDAP.csproj index a0929a1c..96a7714d 100644 --- a/TrenDAP/TrenDAP.csproj +++ b/TrenDAP/TrenDAP.csproj @@ -24,27 +24,22 @@ - - - - - - - + + @@ -104,7 +99,7 @@ - <_ContentIncludedByDefault Remove="wwwroot\typescript\features\datasources\reactdatasources\OpenHistorianDataSource.tsx" /> + <_ContentIncludedByDefault Remove="wwwroot\TypeScript\Features\DataSources\Implementations\OpenHistorianDataSource.tsx" /> @@ -140,13 +135,13 @@ - + Code - + + Code - @@ -157,7 +152,7 @@ - + @@ -171,18 +166,9 @@ Code - - Code - - - Code - Code - - Code - Code diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/AddNewDataSet.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/AddNewDataSet.tsx deleted file mode 100644 index f968099e..00000000 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/AddNewDataSet.tsx +++ /dev/null @@ -1,165 +0,0 @@ -//****************************************************************************************************** -// AddNewDataSet.tsx - Gbtc -// -// Copyright © 2020, 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: -// ---------------------------------------------------------------------------------------------------- -// 09/25/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { DataSourceTypes, TrenDAP } from '../../global'; -import { useAppDispatch, useAppSelector } from '../../hooks'; -import { SelectDataSets, SelectDataSetsStatus, SelectNewDataSet, FetchDataSets } from './DataSetsSlice'; -import { FetchDataSourceDataSets } from '../DataSources/DataSourceDataSetSlice'; -import DataSet from './DataSet'; -import { ToolTip, TabSelector } from '@gpa-gemstone/react-interactive'; -import { Warning } from '@gpa-gemstone/gpa-symbols'; -import moment from 'moment'; -import { CrossMark } from '../../Constants'; -import { useNavigate } from 'react-router-dom'; -import $ from 'jquery'; -import { SelectDataSources } from '../DataSources/DataSourcesSlice' -import * as _ from "lodash" - -const AddNewDataSet: React.FunctionComponent<{}> = (props) => { - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - - const allDataSets = useAppSelector(SelectDataSets); - const setStatus = useAppSelector(SelectDataSetsStatus); - const dataSources = useAppSelector(SelectDataSources); - - const [warnings, setWarning] = React.useState([]); - const [errors, setErrors] = React.useState([]); - const [sourceErrors, setSourceErrors] = React.useState([]); - const [hover, setHover] = React.useState(false); - const [dataSet, setDataSet] = React.useState(SelectNewDataSet()); - const [connections, setConnections] = React.useState([]); - const [tab, setTab] = React.useState('settings'); - - React.useEffect(() => { - if (setStatus === 'unitiated' || setStatus === 'changed') dispatch(FetchDataSets()); - }, [setStatus]); - - React.useEffect(() => { - const w = []; - if (dataSet.Context == 'Relative' && dataSet.RelativeWindow == 'Day' && dataSet.RelativeValue < 7) - w.push("With the current Time Context and Day of Week Filter it is possible for the dataset to be empty at times.") - if (dataSet.Context == 'Relative' && dataSet.RelativeWindow == 'Week' && dataSet.RelativeValue < 53) - w.push("With the current Time Context and Week of Year Filter it is possible for the dataset to be empty at times.") - if (dataSet.Context == 'Relative' && dataSet.RelativeWindow == 'Day' && dataSet.RelativeValue < 366) - w.push("With the current Time Context and Week of Year Filter it is possible for the dataset to be empty at times.") - setWarning(w); - }, [dataSet]); - - React.useEffect(() => { - const e = []; - if (dataSet.Name == null || dataSet.Name.trim().length == 0) - e.push("A Name has to be entered.") - if (dataSet.Name != null && dataSet.Name.length > 200) - e.push("Name has to be less than 200 characters."); - if (dataSet.Name != null && allDataSets.findIndex(ds => ds.ID !== dataSet.ID && ds.Name.toLowerCase() == dataSet.Name.toLowerCase()) > -1) - e.push("A DataSet with this name already exists."); - if (dataSet.Context == 'Fixed Dates' && moment(dataSet.From).isAfter(moment(dataSet.To))) - e.push("A valid Timeframe has to be selected.") - if (dataSet.Hours == 0) - e.push("At least 1 Hour has to be selected.") - if (dataSet.Days == 0) - e.push("At least 1 Day has to be selected.") - if (dataSet.Months == 0) - e.push("At least 1 Month has to be selected.") - if (dataSet.Weeks == 0) - e.push("At least 1 Week has to be selected.") - if (connections.length == 0) - e.push("At least 1 DataSource needs to be added."); - setErrors(e.concat(sourceErrors)); - }, [dataSet, connections, sourceErrors]); - - return ( -
-
-
- New Data Set {dataSet.Name !== null && dataSet.Name.trim().length > 0 ? ('(' + dataSet.Name + ')') : ''} -
-
- ({ - Label: dataSources.find(ds => ds.ID === item.DataSourceID)?.Name, - Id: dataSources.find(ds => ds.ID === item.DataSourceID)?.Name + index.toString(), - })), - ]} - SetTab={(item) => setTab(item)} CurrentTab={tab} /> - -
-
-
-
- -
- 0 || errors.length > 0)} Position={'top'}> - {warnings.map((w, i) =>

{Warning} {w}

)} - {errors.map((e, i) =>

{CrossMark} {e}

)} -
- {tab !== 'settings' ? -
- -
: null - } -
-
-
-
- ); -} - -export default AddNewDataSet; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSet.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSet.tsx deleted file mode 100644 index 106b42b0..00000000 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSet.tsx +++ /dev/null @@ -1,95 +0,0 @@ -//****************************************************************************************************** -// DataSet.tsx - Gbtc -// -// Copyright © 2020, 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: -// ---------------------------------------------------------------------------------------------------- -// 09/25/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { TrenDAP, DataSourceTypes } from '../../global'; -import { useAppDispatch, useAppSelector } from '../../hooks'; -import { SelectDataSources, SelectDataSourcesStatus, FetchDataSources } from '../DataSources/DataSourcesSlice'; - -import DataSetGlobalSettings from './Types/DataSetGlobalSettings'; -import DataSourceWrapper from '../DataSources/DataSourceWrapper'; - -interface IProps { - DataSet: TrenDAP.iDataSet, - SetDataSet: (ws: TrenDAP.iDataSet) => void, - Connections: DataSourceTypes.IDataSourceDataSet[], - SetConnections: (arg: DataSourceTypes.IDataSourceDataSet[]) => void, - Tab: string, - SetErrors: (e: string[]) => void -} - -const DataSet: React.FunctionComponent = (props: IProps) => { - const dispatch = useAppDispatch(); - const dataSources = useAppSelector(SelectDataSources); - const dsStatus = useAppSelector(SelectDataSourcesStatus); - const wrapperErrors = React.useRef>(new Map()); - - React.useEffect(() => { - if (dsStatus === 'unitiated' || dsStatus === 'changed') dispatch(FetchDataSources()); - }, [dsStatus]); - - const changeConn = React.useCallback((index: number, conn: DataSourceTypes.IDataSourceDataSet) => { - const newConns = [...props.Connections]; - if (index < 0 || index >= newConns.length) { - console.error(`Could not find connection ${index} in connection array.`); - return; - } - newConns.splice(index, 1, conn); - props.SetConnections(newConns); - }, [props.Connections, props.SetConnections]); - - const setSourceErrors = React.useCallback((newErrors: string[], name: string) => { - if (newErrors.length > 0) wrapperErrors.current.set(name, newErrors); - else wrapperErrors.current.delete(name); - - const allErrors: string[] = []; - [...wrapperErrors.current.keys()].forEach(key => { - allErrors.push(`The following errors exist for datasource ${key}:`); - const keyErrors = wrapperErrors.current.get(key); - keyErrors.forEach(error => allErrors.push(`\t${error}`)); - }); - props.SetErrors(allErrors); - }, [props.SetErrors]); - - return ( -
-
- -
- { - props.Connections.map((conn, index) => { - const src = dataSources.find(ds => ds.ID === conn.DataSourceID); - return ( -
- setSourceErrors(e, src?.Name)} - DataSetConn={conn} SetDataSetConn={newConn => changeConn(index, newConn)} /> -
- ); - }) - } -
- ); -} - -export default DataSet; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSets.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSets.tsx index faf1f637..71dcb80a 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSets.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSets.tsx @@ -55,8 +55,8 @@ const DataSets: React.FunctionComponent = (props: {}) => { }, [dispatch, dsStatus]); return ( -
-
+
+
@@ -67,7 +67,7 @@ const DataSets: React.FunctionComponent = (props: {}) => {
diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSetsSlice.ts b/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSetsSlice.ts index 744b97e9..dc73a59f 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSetsSlice.ts +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/DataSetsSlice.ts @@ -54,6 +54,8 @@ export const RemoveDataSet = createAsyncThunk('DataSets/RemoveDataSet', async (D return await DeleteDataSet(DataSet); }); +export const DataSetsHaveChanged = createAsyncThunk('DataSets/DataSetsHaveChanged', async () => { return; }); + export const UpdateDataSet = createAsyncThunk('DataSets/UpdateDataSet', async (DataSet: TrenDAP.iDataSet, { dispatch }) => { return await PatchDataSet(DataSet); }); @@ -68,7 +70,7 @@ export const PatchDataSetData = createAsyncThunk('DataSets/PatchDataSetData', as // #region [ Consts ] const newDataSet: TrenDAP.iDataSet = { - ID: 0, + ID: -1, Name: '', User: '', Context: 'Fixed Dates', @@ -95,7 +97,8 @@ export const DataSetsSlice = createSlice({ SortField: 'UpdatedOn', Ascending: false, Record: { - ID: 0, Name: '', User: '', Context: 'Relative', RelativeValue: 30, RelativeWindow: 'Day', From: moment().subtract(30, 'days').format('YYYY-MM-DD'), To: moment().format('YYYY-MM-DD'), Hours: Math.pow(2, 24) - 1, Days: Math.pow(2, 7) - 1, Weeks: Math.pow(2, 53) - 1, Months: Math.pow(2, 12) - 1, Data: {Status: 'unitiated', Error: null} } + ID: 0, Name: '', User: '', Context: 'Relative', RelativeValue: 30, RelativeWindow: 'Day', From: moment().subtract(30, 'days').format('YYYY-MM-DD'), To: moment().format('YYYY-MM-DD'), Hours: Math.pow(2, 24) - 1, Days: Math.pow(2, 7) - 1, Weeks: Math.pow(2, 53) - 1, Months: Math.pow(2, 12) - 1, Data: { Status: 'unitiated', Error: null } + } } as Redux.State, reducers: { Sort: (state, action) => { @@ -124,7 +127,7 @@ export const DataSetsSlice = createSlice({ builder.addCase(FetchDataSets.fulfilled, (state, action) => { state.Status = 'idle'; state.Error = null; - const results = action.payload.map(r => ({ ...r, From: moment(r.From).format('YYYY-MM-DD'), To: moment(r.To).format('YYYY-MM-DD'), Data: { Status: 'unitiated', Error: null}})); + const results = action.payload.map(r => ({ ...r, From: moment(r.From).format('YYYY-MM-DD'), To: moment(r.To).format('YYYY-MM-DD'), Data: { Status: 'unitiated', Error: null } })); const sorted = _.orderBy(results, [state.SortField], [state.Ascending ? "asc" : "desc"]) as TrenDAP.iDataSet[]; state.Data = sorted; }); @@ -140,7 +143,7 @@ export const DataSetsSlice = createSlice({ builder.addCase(FetchDataSetData.fulfilled, (state, action) => { //state.Status = 'idle'; //state.Error = null; - state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'idle', Error: null } ; + state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'idle', Error: null }; }); builder.addCase(FetchDataSetData.pending, (state, action) => { @@ -213,16 +216,16 @@ export const DataSetsSlice = createSlice({ state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'error', Error: action.error.message }; }); builder.addCase(UpdateDataSetDataFlag.fulfilled, (state, action) => { - if(action.payload.ID != null) + if (action.payload.ID != null) state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'idle', Error: action.payload.Created }; else - state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'unitiated', Error: null}; + state.Data.find(d => d.ID === action.meta.arg.ID).Data = { Status: 'unitiated', Error: null }; }); builder.addCase(PatchDataSetData.pending, (state, action) => { state.Data.find(d => d.ID === action.meta.arg.DataSet.ID).Data = { Status: 'changed', Error: null }; }); - + builder.addCase(DataSetsHaveChanged.pending, (state) => { state.Status = 'changed'; }); } }); diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/EditDataSet.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/EditDataSet.tsx index 79d298f3..382f9930 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/EditDataSet.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/EditDataSet.tsx @@ -22,56 +22,80 @@ //****************************************************************************************************** import * as React from 'react'; -import { DataSourceTypes, TrenDAP } from '../../global'; -import { useAppDispatch, useAppSelector } from '../../hooks'; -import { UpdateDataSet, SelectDataSetsStatus, FetchDataSets, SelectDataSets, SetRecordByID, Update } from './DataSetsSlice' -import { SelectDataSourceDataSets, SelectDataSourceDataSetStatus, FetchDataSourceDataSets, RemoveDataSourceDataSet, UpdateDataSourceDataSet, AddDataSourceDataSet } from '../DataSources/DataSourceDataSetSlice'; -import DataSet from './DataSet'; -import { useNavigate } from "react-router-dom"; -import { TabSelector, ToolTip } from '@gpa-gemstone/react-interactive'; import * as _ from 'lodash'; -import { SelectDataSources } from '../DataSources/DataSourcesSlice' import moment from 'moment'; +import * as $ from 'jquery'; +import { useNavigate } from "react-router-dom"; +import { TabSelector, ToolTip } from '@gpa-gemstone/react-interactive'; import { CrossMark, Warning } from '@gpa-gemstone/gpa-symbols'; +import { DataSourceTypes, TrenDAP } from '../../global'; +import { useAppDispatch, useAppSelector } from '../../hooks'; +import { SelectDataSetsStatus, FetchDataSets, SelectDataSets, DataSetsHaveChanged, SelectNewDataSet } from './DataSetsSlice'; +import DataSetSettingsTab from './Tabs/DataSetSettingsTab'; +import DataSourceConnectionTab from './Tabs/DataSourceConnectionTab'; +import EventSourceConnectionTab from './Tabs/EventSourceConnectionTab'; +import { EventSourceTypes } from '../EventSources/Interface'; const EditDataSet: React.FunctionComponent<{}> = (props) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const sourceSetConnections = useAppSelector(SelectDataSourceDataSets); - const dsdsStatus = useAppSelector(SelectDataSourceDataSetStatus); + const dataSets = useAppSelector(SelectDataSets); - const wsStatus = useAppSelector(SelectDataSetsStatus); - const dataSources = useAppSelector(SelectDataSources); + const dataSetStatus = useAppSelector(SelectDataSetsStatus); const [warnings, setWarning] = React.useState([]); const [errors, setErrors] = React.useState([]); - const [sourceErrors, setSourceErrors] = React.useState([]); + + const [dataErrors, setDataErrors] = React.useState([]); + const [eventErrors, setEventErrors] = React.useState([]); + const [hover, setHover] = React.useState(false); - const [connections, setConnections] = React.useState([]); - const [deletedConnections, setDeletedConnections] = React.useState([]); + + const [dataConnections, setDataConnections] = React.useState([]); + const [eventConnections, setEventConnections] = React.useState([]); const [dataSet, setDataSet] = React.useState(undefined); const [tab, setTab] = React.useState('settings'); React.useEffect(() => { - if (wsStatus === 'unitiated' || wsStatus === 'changed') + if (dataSetStatus === 'unitiated' || dataSetStatus === 'changed') dispatch(FetchDataSets()); - }, [wsStatus]); + }, [dataSetStatus]); React.useEffect(() => { - if (wsStatus === 'idle') - setDataSet(dataSets.find(set => set.ID == (props['useParams']?.id ?? -1))); - }, [wsStatus, props['useParams']?.id]); + if (dataSetStatus !== 'idle') return; //SelectNewDataSet + const id = (props['useParams']?.id ?? -1); + if (id < 0) setDataSet(SelectNewDataSet()); + else setDataSet(dataSets.find(set => set.ID == id)); + }, [dataSetStatus, props['useParams']?.id]); React.useEffect(() => { - if (dsdsStatus === 'unitiated' || dsdsStatus === 'changed') - dispatch(FetchDataSourceDataSets()); - }, [dsdsStatus]); - - React.useEffect(() => { - if (dataSet === undefined) return; - if (dsdsStatus === 'idle') - setConnections(sourceSetConnections.filter(conn => conn.DataSetID === dataSet.ID)); - }, [dsdsStatus, dataSet?.ID]); + const id = props['useParams']?.id ?? -1; + if (id === -1) return; + const dataConnectionHandle = $.ajax({ + type: "GET", + url: `${homePath}api/DataSourceDataSet/${id}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true + }).done((data: DataSourceTypes.IDataSourceDataSet[]) => { + setDataConnections(data); + }); + const eventConnectionHandle = $.ajax({ + type: "GET", + url: `${homePath}api/EventSourceDataSet/${id}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true + }).done((data: EventSourceTypes.IEventSourceDataSet[]) => { + setEventConnections(data); + }); + return () => { + if (dataConnectionHandle != null && dataConnectionHandle.abort != null) dataConnectionHandle.abort(); + if (eventConnectionHandle != null && eventConnectionHandle.abort != null) eventConnectionHandle.abort(); + } + }, [props['useParams']?.id]); React.useEffect(() => { if (dataSet == null) return; @@ -104,72 +128,74 @@ const EditDataSet: React.FunctionComponent<{}> = (props) => { e.push("At least 1 Month has to be selected.") if (dataSet.Weeks == 0) e.push("At least 1 Week has to be selected.") - if (connections.length == 0) - e.push("At least 1 DataSource needs to be added."); - setErrors(e.concat(sourceErrors)); - }, [dataSet, connections, sourceErrors]); + if (dataConnections.length == 0) + e.push("At least 1 Trend DataSource needs to be added."); + setErrors(e.concat(dataErrors).concat(eventErrors)); + }, [dataSet, dataConnections, dataErrors, eventErrors]); if (dataSet === undefined) return null; return ( -
-
- Edit Data Set -
-
- ({ - Label: dataSources.find(ds => ds.ID === item.DataSourceID)?.Name, - Id: dataSources.find(ds => ds.ID === item.DataSourceID)?.Name + index.toString(), - })), - ]} - SetTab={(item) => setTab(item)} CurrentTab={tab} /> - -
-
-
-
- +
+
+
+
+ Edit Data Set +
+
+
+ +
+ {tab === 'trend' ? + : <>} + {tab === 'event' ? + : <>} + {tab === 'settings' ? + : <>} +
+
+
+
+
+ +
+ 0 || errors.length > 0)} Position={'top'}> + {warnings.map((w, i) =>

{Warning} {w}

)} + {errors.map((e, i) =>

{CrossMark} {e}

)} +
+
- 0 || errors.length > 0)} Position={'top'}> - {warnings.map((w, i) =>

{Warning} {w}

)} - {errors.map((e, i) =>

{CrossMark} {e}

)} -
- {tab !== 'settings' ? -
- -
: null - }
diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/HelperFunctions.ts b/TrenDAP/wwwroot/TypeScript/Features/DataSets/HelperFunctions.ts index 3ae3c6c5..c6bb0e28 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/HelperFunctions.ts +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/HelperFunctions.ts @@ -24,6 +24,8 @@ import { TrenDAP } from "../../global"; import moment from "moment"; +const DateFormat = 'YYYY-MM-DD'; + const ComputeValidDays = (ds: TrenDAP.iDataSet) => { if (ds.Context == 'Relative') return 127; @@ -82,4 +84,29 @@ const ComputeValidWeeks = (ds: TrenDAP.iDataSet) => { } -export { ComputeValidDays, ComputeValidWeeks } \ No newline at end of file +const ComputeTimeEnds = (ds: TrenDAP.iDataSet) => { + let startTime = moment.utc(ds.From, DateFormat); + let endTime = moment.utc(ds.To, DateFormat); + if (ds.Context == "Relative") { + endTime = moment.utc(moment().format(DateFormat), DateFormat); + if (ds.RelativeWindow == "Day") + startTime = endTime.add(-ds.RelativeValue, "day"); + else if (ds.RelativeWindow == "Week") + startTime = endTime.add(-ds.RelativeValue * 7, "day"); + else if (ds.RelativeWindow == "Month") + startTime = endTime.add(-ds.RelativeValue, "month"); + else + startTime = endTime.add(-ds.RelativeValue, "year"); + } + return { Start: startTime, End: endTime }; +} + +// Computes center of window and size of window in hours +const ComputeTimeCenterAndSize = (ds: TrenDAP.iDataSet, granularity: moment.unitOfTime.Diff = 'hours') => { + const timeEnds = ComputeTimeEnds(ds); + const windowSize = timeEnds.End.diff(timeEnds.Start, granularity, true) / 2; + const center = timeEnds.Start.add(windowSize, granularity); + return { Center: center, Size: windowSize, Unit: granularity } +} + +export { DateFormat, ComputeTimeEnds, ComputeTimeCenterAndSize, ComputeValidDays, ComputeValidWeeks } \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/Types/DataSetGlobalSettings.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSetSettingsTab.tsx similarity index 57% rename from TrenDAP/wwwroot/TypeScript/Features/DataSets/Types/DataSetGlobalSettings.tsx rename to TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSetSettingsTab.tsx index 6655368a..6b20ddd1 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSets/Types/DataSetGlobalSettings.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSetSettingsTab.tsx @@ -22,65 +22,40 @@ //****************************************************************************************************** import * as React from 'react'; -import { TrenDAP, Redux, DataSourceTypes } from '../../../global'; +import { TrenDAP, DataSourceTypes } from '../../../global'; import { Input, CheckBox, EnumCheckBoxes } from '@gpa-gemstone/react-forms'; -import { Plus } from '../../../Constants'; -import { SelectDataSourcesStatus, SelectDataSourcesAllPublicNotUser, SelectDataSourcesForUser, FetchDataSources } from '../../DataSources/DataSourcesSlice'; -import { AddDataSourceDataSet } from '../../DataSources/DataSourceDataSetSlice'; -import { GetReactDataSource } from '../../DataSources/DataSourceWrapper'; import { useAppSelector, useAppDispatch } from '../../../hooks'; -import { SelectDataSourceTypes, SelectDataSourceTypesStatus, FetchDataSourceTypes } from '../../DataSourceTypes/DataSourceTypesSlice'; -import { SelectDataSets } from './../DataSetsSlice'; +import { SelectDataSets, SelectDataSetsStatus, FetchDataSets } from './../DataSetsSlice'; import { ComputeValidDays, ComputeValidWeeks } from '../HelperFunctions'; +import { EventSourceTypes } from '../../EventSources/Interface'; interface IProps { DataSet: TrenDAP.iDataSet, SetDataSet: (ws: TrenDAP.iDataSet) => void, - Connections: DataSourceTypes.IDataSourceDataSet[], - SetConnections: (arg: DataSourceTypes.IDataSourceDataSet[]) => void + DataConnections: DataSourceTypes.IDataSourceDataSet[], + SetDataConnections: (arg: DataSourceTypes.IDataSourceDataSet[]) => void, + EventConnections: EventSourceTypes.IEventSourceDataSet[], + SetEventConnections: (arg: EventSourceTypes.IEventSourceDataSet[]) => void } -const DataSetGlobalSettings: React.FunctionComponent = (props: IProps) => { +const DataSetSettingsTab: React.FunctionComponent = (props: IProps) => { const dispatch = useAppDispatch(); - const dataSources = useAppSelector((state: Redux.StoreState) => SelectDataSourcesForUser(state, userName)) as DataSourceTypes.IDataSourceView[]; - const publicDataSources = useAppSelector((state: Redux.StoreState) => SelectDataSourcesAllPublicNotUser(state, userName)) as DataSourceTypes.IDataSourceView[]; - const dsStatus = useAppSelector(SelectDataSourcesStatus); - const dataSourceTypes = useAppSelector(SelectDataSourceTypes) as DataSourceTypes.IDataSourceType[]; - const dstStatus = useAppSelector(SelectDataSourceTypesStatus); - const allDataSets = useAppSelector(SelectDataSets); + const dataSets = useAppSelector(SelectDataSets); + const dataSetStatus = useAppSelector(SelectDataSetsStatus); React.useEffect(() => { - if (dsStatus != 'unitiated' && dsStatus != 'changed') return; - dispatch(FetchDataSources()); - - return function () { - } - }, [dispatch, dsStatus]); - - React.useEffect(() => { - if (dstStatus != 'unitiated') return; - - dispatch(FetchDataSourceTypes()); - return function () { - } - }, [dispatch, dstStatus]); - + if (dataSetStatus != 'unitiated' && dataSetStatus != 'changed') return; + dispatch(FetchDataSets()); + }, [dataSetStatus]); function valid(field: keyof (TrenDAP.iDataSet)): boolean { if (field == 'Name') return props.DataSet.Name != null && props.DataSet.Name.trim().length > 0 && - props.DataSet.Name.length <= 200 && allDataSets.find(ws => ws.Name.toLowerCase() == props.DataSet.Name.toLowerCase() && ws.ID != props.DataSet.ID) == null + props.DataSet.Name.length <= 200 && dataSets.find(ws => ws.Name.toLowerCase() == props.DataSet.Name.toLowerCase() && ws.ID != props.DataSet.ID) == null else return true; } - function AddDS(dataSource: DataSourceTypes.IDataSourceView) { - const dataSourceReact = GetReactDataSource(dataSource, dataSourceTypes); - const newConns = [...props.Connections]; - newConns.push({ ID: -1, DataSourceID: dataSource.ID, DataSetID: props.DataSet.ID, Settings: JSON.stringify(dataSourceReact.DefaultDataSetSettings) }) - props.SetConnections(newConns); - } - function validDay(d: string) { const dayOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] @@ -96,35 +71,20 @@ const DataSetGlobalSettings: React.FunctionComponent = (props: IProps) = } return ( -
-
- Record={props.DataSet} Field="Name" Setter={(record) => props.SetDataSet(record)} Valid={valid} Feedback={"A Unique Name has to be specified"} /> - props.SetDataSet(record)} /> - Record={props.DataSet} Field="Hours" Label="Hour of Day" Setter={(record) => props.SetDataSet(record)} Enum={Array.from({ length: 24 }, (_, i) => i.toString())} /> - Record={props.DataSet} Field="Days" Label="Day of Week" Setter={(record) => props.SetDataSet(record)} Enum={['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']} IsDisabled={validDay} /> - Record={props.DataSet} Field="Weeks" Label="Week of Year" Setter={(record) => props.SetDataSet(record)} Enum={Array.from({ length: 53 }, (_, i) => i.toString())} IsDisabled={validWeek} /> - Record={props.DataSet} Field="Months" Label="Month of Year" Setter={(record) => props.SetDataSet(record)} Enum={['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']} /> - Record={props.DataSet} Field="Public" Label='Shared' Setter={(record) => props.SetDataSet(record)} /> -
-
- - -
-
- -
+ <> + Record={props.DataSet} Field="Name" Setter={(record) => props.SetDataSet(record)} Valid={valid} Feedback={"A Unique Name has to be specified"} /> + props.SetDataSet(record)} /> + Record={props.DataSet} Field="Hours" Label="Hour of Day" Setter={(record) => props.SetDataSet(record)} Enum={Array.from({ length: 24 }, (_, i) => i.toString())} /> + Record={props.DataSet} Field="Days" Label="Day of Week" Setter={(record) => props.SetDataSet(record)} Enum={['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']} IsDisabled={validDay} /> + Record={props.DataSet} Field="Weeks" Label="Week of Year" Setter={(record) => props.SetDataSet(record)} Enum={Array.from({ length: 53 }, (_, i) => i.toString())} IsDisabled={validWeek} /> + Record={props.DataSet} Field="Months" Label="Month of Year" Setter={(record) => props.SetDataSet(record)} Enum={['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']} /> + Record={props.DataSet} Field="Public" Label='Shared' Setter={(record) => props.SetDataSet(record)} /> + ); } -export default DataSetGlobalSettings; +export default DataSetSettingsTab; const RelativeDateRangePicker = (props: { Record: TrenDAP.iDataSet, Setter: (record: TrenDAP.iDataSet) => void }) => { const [context, setContext] = React.useState<'Relative' | 'Fixed Dates'>(props.Record.Context); diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSourceConnectionTab.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSourceConnectionTab.tsx new file mode 100644 index 00000000..f3014142 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/DataSourceConnectionTab.tsx @@ -0,0 +1,186 @@ +//****************************************************************************************************** +// DataSourceCOnnectionTab.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/01/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import * as _ from 'lodash'; +import { ReactTable } from '@gpa-gemstone/react-table'; +import { DataSourceTypes, TrenDAP } from '../../../global'; +import { FetchDataSources, SelectDataSources, SelectDataSourcesStatus } from '../../DataSources/DataSourcesSlice'; +import { Pencil, Plus, TrashCan } from '@gpa-gemstone/gpa-symbols'; +import { useAppSelector, useAppDispatch } from '../../../hooks'; +import DataSourceWrapper from '../../DataSources/DataSourceWrapper'; + +interface IProps { + DataSourceConnections: DataSourceTypes.IDataSourceDataSet[], + SetDataSourceConnections: (newConns: DataSourceTypes.IDataSourceDataSet[]) => void, + DataSet: TrenDAP.iDataSet, + SetErrors: (e: string[]) => void +} + +const DataSourceConnectionTab: React.FC = (props) => { + const dispatch = useAppDispatch(); + const dataSources = useAppSelector(SelectDataSources); + const dataSourceStatus = useAppSelector(SelectDataSourcesStatus); + const errors = React.useRef>(new Array().fill(null)); + const [currentIndex, setCurrentIndex] = React.useState(0); + + React.useEffect(() => { + if (dataSourceStatus === 'unitiated' || dataSourceStatus === 'changed') + dispatch(FetchDataSources()); + }, [dataSourceStatus]); + + React.useEffect(() => { + props.SetErrors([]); + errors.current = new Array().fill(null); + }, [props.DataSet.ID]); + + const dataSource = React.useMemo(() => { + const srcId = props.DataSourceConnections[currentIndex]?.DataSourceID; + if (srcId == undefined) return undefined; + return dataSources.find(src => srcId === src.ID); + }, [dataSourceStatus, currentIndex, props.DataSourceConnections]); + + const AddDS = React.useCallback((dataSource: DataSourceTypes.IDataSourceView) => { + const newConns = [...props.DataSourceConnections]; + newConns.push({ ID: -1, DataSourceID: dataSource.ID, DataSourceName: dataSource.Name, DataSetID: props.DataSet.ID, DataSetName: props.DataSet.Name, Settings: {} }); + setCurrentIndex(newConns.length - 1); + props.SetDataSourceConnections(newConns); + }, [props.DataSourceConnections, props.SetDataSourceConnections, props.DataSet, currentIndex, dataSourceStatus]); + + const pushErrors = React.useCallback(() => { + let e: string[] = []; + errors.current.forEach(errorList => { + if (errorList != null) e = e.concat(errorList); + }) + props.SetErrors(e); + }, [props.SetErrors]); + + const addWrapperErrors = React.useCallback((e: string[]) => { + if (e.length === 0) errors.current[currentIndex] = null; + else errors.current[currentIndex] = [`Errors from ${props.DataSourceConnections[currentIndex]?.DataSourceName}:`].concat(e); + pushErrors(); + }, [pushErrors, currentIndex, props.DataSourceConnections]); + + return ( +
+
+
+
+

Trend Connections

+
+
+
+ +
+
Your DataSources
+ {dataSources.filter(src => src.User === userName).map(ds => AddDS(ds)}>{ds.Name} ({ds.Type}))} +
Shared DataSources
+ {dataSources.filter(src => src.Public && src.User !== userName).map(ds => AddDS(ds)}>{ds.Name} ({ds.Type}))} +
+
+
+
+
+
+ + TableClass="table table-hover" + Data={props.DataSourceConnections} + SortKey={null} + Ascending={null} + OnSort={(d) => { }} + TableStyle={{ + padding: 0, width: '100%', height: '100%', + tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' + }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'scroll', flex: 1 }} + RowStyle={{ display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(_item, index) => currentIndex === index} + KeySelector={(_item, index) => index} + OnClick={(item) => { setCurrentIndex(item.index); }} + > + + Key={'DataSourceName'} + AllowSort={true} + Field={'DataSourceName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > DataSource + + + Key={'ID'} + AllowSort={false} + Field={'ID'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={row => + + + + } + ><> + +
+
+
+
+
+
+
+ {props.DataSourceConnections[currentIndex] != null ? + { + const newConns = [...props.DataSourceConnections]; + newConns.splice(currentIndex, 1, newConn) + props.SetDataSourceConnections(newConns); + }} /> : <>} +
+
+
+
+
+ ); + +} + +export default DataSourceConnectionTab; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/EventSourceConnectionTab.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/EventSourceConnectionTab.tsx new file mode 100644 index 00000000..3e29654f --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSets/Tabs/EventSourceConnectionTab.tsx @@ -0,0 +1,189 @@ +//****************************************************************************************************** +// EventSourceCOnnectionTab.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/01/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import * as _ from 'lodash'; +import { ReactTable } from '@gpa-gemstone/react-table'; +import { Pencil, Plus, TrashCan } from '@gpa-gemstone/gpa-symbols'; +import { TrenDAP } from '../../../global'; +import { useAppSelector, useAppDispatch } from '../../../hooks'; +import { FetchEventSources, SelectEventSources, SelectEventSourcesStatus } from '../../EventSources/Slices/EventSourcesSlice'; +import { EventSourceTypes } from '../../EventSources/Interface'; +import EventDataSourceWrapper from '../../EventSources/EventDataSourceWrapper'; + +interface IProps { + EventSourceConnections: EventSourceTypes.IEventSourceDataSet[], + SetEventSourceConnections: (newConns: EventSourceTypes.IEventSourceDataSet[]) => void, + DataSet: TrenDAP.iDataSet, + SetErrors: (e: string[]) => void +} + +const EventSourceConnectionTab: React.FC = (props) => { + const dispatch = useAppDispatch(); + const eventSources = useAppSelector(SelectEventSources); + const eventSourceStatus = useAppSelector(SelectEventSourcesStatus); + const errors = React.useRef>(new Array().fill(null)); + const [currentIndex, setCurrentIndex] = React.useState(0); + + React.useEffect(() => { + if (eventSourceStatus === 'unitiated' || eventSourceStatus === 'changed') + dispatch(FetchEventSources()); + }, [eventSourceStatus]); + + React.useEffect(() => { + props.SetErrors([]); + errors.current = new Array().fill(null); + }, [props.DataSet.ID]); + + const eventSource = React.useMemo(() => { + const srcId = props.EventSourceConnections[currentIndex]?.EventSourceID; + if (srcId == undefined) return undefined; + return eventSources.find(src => srcId === src.ID); + }, [eventSourceStatus, currentIndex, props.EventSourceConnections]); + + const AddDS = React.useCallback((src: EventSourceTypes.IEventSourceView) => { + const newConns = [...props.EventSourceConnections]; + newConns.push({ ID: -1, EventSourceID: src.ID, EventSourceName: src.Name, DataSetID: props.DataSet.ID, DataSetName: props.DataSet.Name, Settings: {} }); + setCurrentIndex(newConns.length - 1); + props.SetEventSourceConnections(newConns); + }, [props.EventSourceConnections, props.SetEventSourceConnections, props.DataSet, currentIndex, eventSourceStatus]); + + const pushErrors = React.useCallback(() => { + let e: string[] = []; + errors.current.forEach(errorList => { + if (errorList != null) e = e.concat(errorList); + }) + props.SetErrors(e); + }, [props.SetErrors]); + + const addWrapperErrors = React.useCallback((e: string[]) => { + if (e.length === 0) errors.current[currentIndex] = null; + else errors.current[currentIndex] = [`Errors from ${props.EventSourceConnections[currentIndex]?.EventSourceName}:`].concat(e); + pushErrors(); + }, [pushErrors, currentIndex, props.EventSourceConnections]); + + return ( +
+
+
+
+

Event Connections

+
+
+
+ +
+
Your EventSources
+ {eventSources.filter(src => src.User === userName).map(ds => AddDS(ds)}>{ds.Name} ({ds.Type}))} +
Shared EventSources
+ {eventSources.filter(src => src.Public && src.User !== userName).map(ds => AddDS(ds)}>{ds.Name} ({ds.Type}))} +
+
+
+
+
+
+ + TableClass="table table-hover" + Data={props.EventSourceConnections} + SortKey={null} + Ascending={null} + OnSort={(d) => { }} + TableStyle={{ + padding: 0, width: '100%', height: '100%', + tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column' + }} + TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'scroll', flex: 1 }} + RowStyle={{ display: 'table', tableLayout: 'fixed', width: '100%' }} + Selected={(_item, index) => currentIndex === index} + KeySelector={(_item, index) => index} + OnClick={(item) => { setCurrentIndex(item.index); }} + > + + Key={'EventSourceName'} + AllowSort={true} + Field={'EventSourceName'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + > EventSource + + + Key={'ID'} + AllowSort={false} + Field={'ID'} + HeaderStyle={{ width: 'auto' }} + RowStyle={{ width: 'auto' }} + Content={row => + + + + } + ><> + +
+
+
+
+
+
+
+ {props.EventSourceConnections[currentIndex] != null ? + { + const newConns = [...props.EventSourceConnections]; + newConns.splice(currentIndex, 1, newConn) + props.SetEventSourceConnections(newConns); + }} /> : <>} +
+
+
+
+
+ ); + +} + +export default EventSourceConnectionTab; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSourceTypes/DataSourceTypesSlice.ts b/TrenDAP/wwwroot/TypeScript/Features/DataSourceTypes/DataSourceTypesSlice.ts deleted file mode 100644 index 69ca9519..00000000 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSourceTypes/DataSourceTypesSlice.ts +++ /dev/null @@ -1,92 +0,0 @@ -//****************************************************************************************************** -// DataSourceTypesSlice.ts - Gbtc -// -// Copyright © 2020, 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: -// ---------------------------------------------------------------------------------------------------- -// 09/24/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - - -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { DataSourceTypes, Redux } from '../../global'; -import $ from 'jquery'; - -export const FetchDataSourceTypes = createAsyncThunk('DataSources/FetchDataSourceTypes', async (_,{ dispatch }) => { - return await GetDataSourceTypes() -}); - - -export const DataSourceTypesSlice = createSlice({ - name: 'DataSourceTypes', - initialState: { - Status: 'unitiated', - Data: [], - SortField: 'Name', - Ascending: true, - Error: null - } as Redux.State, - reducers: { - Add: (state, action) => { - state.Data.push(action.payload); - }, - AddRange: (state, action) => { - state = action.payload; - }, - Remove: state => { - - } - }, - extraReducers: (builder) => { - - builder.addCase(FetchDataSourceTypes.fulfilled, (state, action) => { - state.Status = 'idle'; - state.Error = null; - state.Data.push(...action.payload); - FetchDataSourceTypes(); - }); - builder.addCase(FetchDataSourceTypes.pending, (state, action) => { - state.Status = 'loading'; - }); - builder.addCase(FetchDataSourceTypes.rejected, (state, action) => { - state.Status = 'error'; - state.Error = action.error.message; - - }); - - } - -}); - -export const { Add, AddRange } = DataSourceTypesSlice.actions; -export default DataSourceTypesSlice.reducer; -export const SelectDataSourceTypes = (state: Redux.StoreState) => state.DataSourceTypes.Data; -export const SelectDataSourceTypesStatus = (state: Redux.StoreState) => state.DataSourceTypes.Status; - -function GetDataSourceTypes(): JQuery.jqXHR { - return $.ajax({ - type: "GET", - url: `${homePath}api/DataSourceType`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); -} - - - diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/AddNewDataSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/AddNewDataSource.tsx index eb4891ab..fe098c85 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/AddNewDataSource.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/AddNewDataSource.tsx @@ -28,11 +28,11 @@ import { AddDataSource } from './DataSourcesSlice' import DataSource from './DataSource'; import { Modal } from '@gpa-gemstone/react-interactive'; import { CrossMark } from '@gpa-gemstone/gpa-symbols'; +import { AllSources } from './DataSources'; const AddNewDataSource: React.FunctionComponent = () => { const dispatch = useAppDispatch(); - - const [dataSource, setDataSource] = React.useState({ ID: -1, Name: "", DataSourceTypeID: 1, URL: '', RegistrationKey: '', Expires: null, Public: false, User: '', Settings: '{}' }); + const [dataSource, setDataSource] = React.useState({ ID: -1, Name: "", Type: AllSources[0].Name, URL: '', RegistrationKey: '', APIToken: '', Public: false, User: '', Settings: '{}' }); const [showModal, setShowModal] = React.useState(false); const [errors, setErrors] = React.useState([]); diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSource.tsx index a73a001e..b00119e7 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSource.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSource.tsx @@ -22,11 +22,12 @@ //****************************************************************************************************** import * as React from 'react'; +import * as _ from 'lodash'; import { DataSourceTypes, Redux } from '../../global'; import { useAppDispatch, useAppSelector } from '../../hooks'; import { Input, Select, CheckBox, DatePicker } from '@gpa-gemstone/react-forms'; -import { SelectDataSourceTypes } from '../DataSourceTypes/DataSourceTypesSlice'; -import DataSourceWrapper from './DataSourceWrapper'; +import { IDataSource } from './Interface'; +import { AllSources } from './DataSources'; import { SelectDataSourcesStatus, SelectDataSourcesAllPublicNotUser, SelectDataSourcesForUser, FetchDataSources } from './DataSourcesSlice'; const DataSource: React.FunctionComponent<{ DataSource: DataSourceTypes.IDataSourceView, SetDataSource: (ds: DataSourceTypes.IDataSourceView) => void, SetErrors: (e: string[]) => void }> = (props) => { @@ -34,11 +35,22 @@ const DataSource: React.FunctionComponent<{ DataSource: DataSourceTypes.IDataSou const dataSources = useAppSelector((state: Redux.StoreState) => SelectDataSourcesForUser(state, userName)) as DataSourceTypes.IDataSourceView[]; const publicDataSources = useAppSelector((state: Redux.StoreState) => SelectDataSourcesAllPublicNotUser(state, userName)) as DataSourceTypes.IDataSourceView[]; const dsStatus = useAppSelector(SelectDataSourcesStatus); + const [configErrors, setConfigErrors] = React.useState([]); + const implementation: IDataSource | null = React.useMemo(() => + AllSources.find(t => t.Name == props.DataSource.Type), [props.DataSource.Type]); + const settings = React.useMemo(() => { + if (implementation == null) + return {}; + const s = _.cloneDeep(implementation.DefaultSourceSettings ?? {}); + let custom = props.DataSource.Settings; - const dataSourceTypes: DataSourceTypes.IDataSourceType[] = useAppSelector(SelectDataSourceTypes); - const [useExpiredField, setUseExpiredField] = React.useState(props.DataSource.Expires != null); - const [wrapperErrors, setWrapperErrors] = React.useState([]); + for (const [k] of Object.entries(implementation?.DefaultSourceSettings ?? {})) { + if (custom.hasOwnProperty(k)) + s[k] = _.cloneDeep(custom[k]); + } + return s; + }, [implementation, props.DataSource.Settings]); React.useEffect(() => { if (dsStatus === 'unitiated' || dsStatus === 'changed') dispatch(FetchDataSources()); @@ -52,8 +64,8 @@ const DataSource: React.FunctionComponent<{ DataSource: DataSourceTypes.IDataSou else if (dataSources.filter(ds => ds.ID !== props.DataSource.ID).concat(publicDataSources).map(ds => ds.Name.toLowerCase()).includes(props.DataSource.Name.toLowerCase())) errors.push("A shared datasource with this name was already created by another user."); - props.SetErrors(wrapperErrors.concat(errors)); - }, [props.DataSource, wrapperErrors]); + props.SetErrors(errors.concat(configErrors)); + }, [props.DataSource, configErrors]); function valid(field: keyof (DataSourceTypes.IDataSourceView)): boolean { if (field == 'Name') @@ -64,29 +76,19 @@ const DataSource: React.FunctionComponent<{ DataSource: DataSourceTypes.IDataSou return (
Record={props.DataSource} Field="Name" Setter={props.SetDataSource} Valid={valid} /> - Record={props.DataSource} Label="DataSource Type" Field="DataSourceTypeID" Setter={item => { - const newRecord = { ...props.DataSource, DataSourceTypeID: Number(item.DataSourceTypeID) } - props.SetDataSource(newRecord); - }} Options={dataSourceTypes.map(x => ({ Value: x.ID.toString(), Label: x.Name }))} /> + Record={props.DataSource} Label="Type" Field="Type" Setter={props.SetDataSource} + Options={AllSources.map((type) => ({ Value: type.Name, Label: type.Name }))} /> Record={props.DataSource} Field="URL" Setter={props.SetDataSource} Valid={() => true} /> Record={props.DataSource} Field="RegistrationKey" Label={'Registration Key'} Setter={props.SetDataSource} Valid={() => true} /> - Record={{ expires: useExpiredField }} Field="expires" Label='Expires' Setter={item => { - if(!item.expires) - props.SetDataSource({ ...props.DataSource, Expires: null }); - else if (props.DataSource.Expires == null) - props.SetDataSource({ ...props.DataSource, Expires: new Date().toISOString() }) - setUseExpiredField(item.expires) - }} /> - {useExpiredField ? - Record={props.DataSource} Field={"Expires"} Type={'datetime-local'} Valid={() => true} Label={"Expiration Date"} Setter={props.SetDataSource} Feedback={"Date can not expire today."} /> - : null - }
Record={props.DataSource} Field="Public" Label='Shared' Setter={props.SetDataSource} />
- + {implementation != null ? + props.SetDataSource({ ...props.DataSource, Settings: s })} /> : <> + } ); } diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceDataSetSlice.ts b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceDataSetSlice.ts deleted file mode 100644 index 6e882fda..00000000 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceDataSetSlice.ts +++ /dev/null @@ -1,202 +0,0 @@ -//****************************************************************************************************** -// DataSourceDataSetsSlice.ts - Gbtc -// -// Copyright © 2020, 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: -// ---------------------------------------------------------------------------------------------------- -// 04/04/2020 - Gabriel Santos -// Generated original version of source code. -// -//****************************************************************************************************** - -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { Redux, DataSourceTypes } from '../../global'; -import _ from 'lodash'; -import $ from 'jquery'; - -// #region [ Consts ] -const blankConnection: DataSourceTypes.IDataSourceDataSet = { - ID: -1, Settings: '{}', DataSourceID: -1, DataSetID: -1 -} -// #endregion - -// #region [ Thunks ] -export const FetchDataSourceDataSets = createAsyncThunk('DataSourceDataSets/FetchDataSourceDataSets', async (_, { dispatch }) => { - return await GetDataSourceDataSets(); -}); - -export const AddDataSourceDataSet = createAsyncThunk('DataSourceDataSets/AddDataSourceDataSet', async (DataSet: DataSourceTypes.IDataSourceDataSet, { dispatch }) => { - return await PostDataSourceDataSet(DataSet); -}); - -export const RemoveDataSourceDataSet = createAsyncThunk('DataSourceDataSets/RemoveDataSourceDataSet', async (DataSet: DataSourceTypes.IDataSourceDataSet, { dispatch }) => { - return await DeleteDataSourceDataSet(DataSet); -}); - -export const UpdateDataSourceDataSet = createAsyncThunk('DataSourceDataSets/UpdateDataSourceDataSet', async (DataSet: DataSourceTypes.IDataSourceDataSet, { dispatch }) => { - return await PatchDataSourceDataSet(DataSet); -}); - -// #endregion - -// #region [ Slice ] -export const DataSourceDataSetSlice = createSlice({ - name: 'DataSourceDataSets', - initialState: { - Status: 'unitiated', - Data: [], - Error: null, - SortField: 'ID', - Ascending: false, - Record: blankConnection - } as Redux.State, - reducers: { - Sort: (state, action) => { - if (state.SortField === action.payload.SortField) - state.Ascending = !action.payload.Ascending; - else - state.SortField = action.payload.SortField; - - const sorted = _.orderBy(state.Data, [state.SortField], [state.Ascending ? "asc" : "desc"]) - state.Data = sorted as DataSourceTypes.IDataSourceDataSet[]; - }, - New: (state, action) => { - state.Record = blankConnection - }, - SetRecordByID: (state, action) => { - const record = state.Data.find(ds => ds.ID === action.payload); - if (record !== undefined) - state.Record = record; - }, - Update: (state, action) => { - state.Record = action.payload; - } - }, - extraReducers: (builder) => { - builder.addCase(FetchDataSourceDataSets.fulfilled, (state, action) => { - state.Status = 'idle'; - state.Error = null; - const sorted = _.orderBy(action.payload, [state.SortField], [state.Ascending ? "asc" : "desc"]) as DataSourceTypes.IDataSourceDataSet[]; - state.Data = sorted; - }); - builder.addCase(FetchDataSourceDataSets.pending, (state, action) => { - state.Status = 'loading'; - }); - builder.addCase(FetchDataSourceDataSets.rejected, (state, action) => { - state.Status = 'error'; - state.Error = action.error.message; - }); - - builder.addCase(AddDataSourceDataSet.pending, (state, action) => { - state.Status = 'loading'; - }); - builder.addCase(AddDataSourceDataSet.rejected, (state, action) => { - state.Status = 'error'; - state.Error = action.error.message; - - }); - builder.addCase(AddDataSourceDataSet.fulfilled, (state, action) => { - state.Status = 'changed'; - state.Error = null; - }); - - builder.addCase(RemoveDataSourceDataSet.pending, (state, action) => { - state.Status = 'loading'; - }); - builder.addCase(RemoveDataSourceDataSet.rejected, (state, action) => { - state.Status = 'error'; - state.Error = action.error.message; - - }); - builder.addCase(RemoveDataSourceDataSet.fulfilled, (state, action) => { - state.Status = 'changed'; - state.Error = null; - }); - - builder.addCase(UpdateDataSourceDataSet.pending, (state, action) => { - state.Status = 'loading'; - }); - builder.addCase(UpdateDataSourceDataSet.rejected, (state, action) => { - state.Status = 'error'; - state.Error = action.error.message; - - }); - builder.addCase(UpdateDataSourceDataSet.fulfilled, (state, action) => { - state.Status = 'changed'; - state.Error = null; - }); - } -}); -// #endregion - -// #region [ Selectors ] -export const { Sort, New, Update, SetRecordByID } = DataSourceDataSetSlice.actions; -export default DataSourceDataSetSlice.reducer; -export const SelectDataSourceDataSets = (state: Redux.StoreState) => state.DataSourceDataSets.Data; -export const SelectRecord = (state: Redux.StoreState) => state.DataSourceDataSets.Record; -export const SelectNewDataSourceDataSet = () => blankConnection; -export const SelectDataSourceDataSetStatus = (state: Redux.StoreState) => state.DataSourceDataSets.Status; -export const SelectDataSourceDataSetField = (state: Redux.StoreState) => state.DataSourceDataSets.SortField; -export const SelectDataSourceDataSetAscending = (state: Redux.StoreState) => state.DataSourceDataSets.Ascending; -// #endregion - -// #region [ Async Functions ] -function GetDataSourceDataSets(): JQuery.jqXHR { - return $.ajax({ - type: "GET", - url: `${homePath}api/DataSourceDataSet`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); -} - -function PostDataSourceDataSet(dataSourceDataSet: DataSourceTypes.IDataSourceDataSet): JQuery.jqXHR { - return $.ajax({ - type: "POST", - url: `${homePath}api/DataSourceDataSet`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - data: JSON.stringify(dataSourceDataSet), - cache: false, - async: true - }); -} - -function DeleteDataSourceDataSet(dataSourceDataSet: DataSourceTypes.IDataSourceDataSet){ - return $.ajax({ - type: "DELETE", - url: `${homePath}api/DataSourceDataSet`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - data: JSON.stringify(dataSourceDataSet), - cache: false, - async: true - }); -} - -function PatchDataSourceDataSet(dataSourceDataSet: DataSourceTypes.IDataSourceDataSet): JQuery.jqXHR { - return $.ajax({ - type: "PATCH", - url: `${homePath}api/DataSourceDataSet`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - data: JSON.stringify(dataSourceDataSet), - cache: false, - async: true - }); -} -// #endregion \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceWrapper.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceWrapper.tsx index 554a9cd9..4bf9bac9 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceWrapper.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSourceWrapper.tsx @@ -21,82 +21,47 @@ // //****************************************************************************************************** import * as React from 'react'; +import * as _ from 'lodash'; import { ServerErrorIcon } from '@gpa-gemstone/react-interactive'; -import { cloneDeep } from 'lodash'; import { DataSourceTypes, TrenDAP } from '../../global'; -import { useAppSelector, useAppDispatch } from '../../hooks'; -import { SelectDataSourceTypes, SelectDataSourceTypesStatus, FetchDataSourceTypes } from '../DataSourceTypes/DataSourceTypesSlice'; -import XDADataSource from './ReactDataSources/XDADataSource'; -import SapphireDataSource from './ReactDataSources/SapphireDataSource'; -import OpenHistorianDataSource from './ReactDataSources/OpenHistorianDataSource'; +import { IDataSource, EnsureTypeSafety } from './Interface'; +import { AllSources } from './DataSources'; -const AllSources: DataSourceTypes.IDataSource[] = [XDADataSource, SapphireDataSource, OpenHistorianDataSource]; - -interface IPropsCommon { +interface IProps { DataSource: DataSourceTypes.IDataSourceView, - SetErrors: (e: string[]) => void -} - -interface IPropsDataset extends IPropsCommon { - ComponentType: 'datasetConfig', + SetErrors: (e: string[]) => void, DataSet: TrenDAP.iDataSet, - DataSetConn: DataSourceTypes.IDataSourceDataSet, - SetDataSetConn: (arg: DataSourceTypes.IDataSourceDataSet) => void -} - -interface IPropsSetting extends IPropsCommon { - ComponentType: 'sourceConfig', - SetDataSource: (newSource: DataSourceTypes.IDataSourceView) => void + Connection: DataSourceTypes.IDataSourceDataSet, + SetConnection: (arg: DataSourceTypes.IDataSourceDataSet) => void } -const DataSourceWrapper: React.FC = (props: IPropsDataset | IPropsSetting) => { - const dispatch = useAppDispatch(); - const dstStatus = useAppSelector(SelectDataSourceTypesStatus); - const dataSourceTypes = useAppSelector(SelectDataSourceTypes); - const [dataSource, setDataSource] = React.useState>(undefined); - - React.useEffect(() => { - // Need Cleanup for errors since outside changes may effect errors - return () => props.SetErrors([]); - }, [props.DataSource.DataSourceTypeID]); - - React.useEffect(() => { - if (dstStatus === 'unitiated' || dstStatus === 'changed') dispatch(FetchDataSourceTypes()); - }, [dstStatus]); - - React.useEffect(() => { - if (props.DataSource == null) return; - setDataSource(GetReactDataSource(props.DataSource, dataSourceTypes)); - }, [props.DataSource?.DataSourceTypeID, dstStatus]); - - const SourceSettings = React.useMemo(() => { - if (props.DataSource?.Settings == null) - return dataSource?.DefaultSourceSettings ?? {}; - return TypeCorrectSettings(props.DataSource.Settings, dataSource?.DefaultSourceSettings ?? {}); - }, [dataSource, props.DataSource?.Settings]); - - const SetSourceSettings = React.useCallback(newSetting => { - if (props.DataSource == null || props.ComponentType !== 'sourceConfig') return; - const newDataSource = { ...props.DataSource }; - newDataSource.Settings = newSetting; - props.SetDataSource(newDataSource); - }, [props.DataSource, props['SetDataSource']]); +const DataSourceWrapper: React.FC = (props: IProps) => { + const implementation: IDataSource | null = + React.useMemo(() => AllSources.find(t => t.Name == props.DataSource?.Type), [props.DataSource?.Type]); const DataSetSettings = React.useMemo(() => { - if (props.DataSource == null || props.ComponentType !== 'datasetConfig') return; - if (props.DataSetConn?.Settings == null) - return dataSource?.DefaultDataSetSettings ?? {}; - return TypeCorrectSettings(props.DataSetConn.Settings, dataSource?.DefaultDataSetSettings ?? {}); - }, [dataSource, props['DataSetConn']?.Settings]); - - const SetDataSetSettings = React.useCallback(newSetting => { - if (props.DataSource == null || props.ComponentType !== 'datasetConfig') return; - const newConn = { ...props.DataSetConn }; - newConn.Settings = newSetting; - props.SetDataSetConn(newConn); - }, [props['DataSetConn'], props['SetDataSetConn']]); + if (props.DataSource == null) return; + if (props.Connection?.Settings == null) + return implementation?.DefaultDataSetSettings ?? {}; + return EnsureTypeSafety(props.Connection.Settings, implementation?.DefaultDataSetSettings ?? {}); + }, [implementation, props.Connection?.Settings]); + + // Ensure that source settings are valid + const dataSource = React.useMemo(() => { + if (implementation == null) + return props.DataSource; + const src = _.cloneDeep(props.DataSource); + const sourceSettings = _.cloneDeep(implementation.DefaultSourceSettings ?? {}); + let custom = props.DataSource.Settings; + for (const [k] of Object.entries(sourceSettings)) { + if (custom.hasOwnProperty(k)) + sourceSettings[k] = _.cloneDeep(custom[k]); + } + src.Settings = sourceSettings; + return src; + }, [props.DataSource]); - return <>{dataSource == null ?
+ return <>{implementation == null ?
{props.DataSource?.Name} - Error
@@ -107,44 +72,17 @@ const DataSourceWrapper: React.FC = (props: IProp
: - {props.ComponentType === 'datasetConfig' ? - : - - } + props.SetConnection({ ...props.Connection, Settings: s })} + SetErrors={props.SetErrors} + /> } } -// Function finds react datasource definition given a list of dataSourceTypes -function GetReactDataSource(dataSource: DataSourceTypes.IDataSourceView, dataSourceTypes: DataSourceTypes.IDataSourceType[]) { - // Find Type - const dataSourceType = dataSourceTypes.find(type => type.ID === dataSource.DataSourceTypeID); - if (dataSourceType === undefined) return undefined; - - return AllSources.find(item => item.Name === dataSourceType.Name); -} - -// Function to parse DataSourceDataSet Settings -function TypeCorrectSettings(settingsObj: any, defaultSettings: T): T { - const s = cloneDeep(defaultSettings); - for (const [k] of Object.entries(defaultSettings)) { - if (settingsObj.hasOwnProperty(k)) - s[k] = cloneDeep(settingsObj[k]); - } - return s; -} - interface IError { name: string, message: string @@ -161,7 +99,7 @@ class ErrorBoundary extends React.Component<{ Name: string }, IError> { name: error.name, message: error.message }); - console.log(error); + console.error(error); } render() { @@ -184,5 +122,4 @@ class ErrorBoundary extends React.Component<{ Name: string }, IError> { } } -export { AllSources, DataSourceWrapper, GetReactDataSource, TypeCorrectSettings } export default DataSourceWrapper; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSources.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSources.tsx index 667e439b..a7c42637 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSources.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/DataSources.tsx @@ -27,25 +27,21 @@ import { DataSourceTypes } from '../../global'; import { useAppSelector, useAppDispatch } from '../../hooks'; import { FetchDataSources, SelectDataSourcesStatus, RemoveDataSource, SelectDataSources } from './DataSourcesSlice' import { ReactTable } from '@gpa-gemstone/react-table'; -import { SelectDataSourceTypes, SelectDataSourceTypesStatus, FetchDataSourceTypes } from '../DataSourceTypes/DataSourceTypesSlice'; import EditDataSource from './EditDataSource'; import { TrashCan, HeavyCheckMark } from './../../Constants'; import AddNewDataSource from './AddNewDataSource'; import { Warning } from '@gpa-gemstone/react-interactive'; +import XDADataSource from './Implementations/XDADataSource'; +import SapphireDataSource from './Implementations/SapphireDataSource'; +import OpenHistorianDataSource from './Implementations/OpenHistorianDataSource'; +import { IDataSource } from './Interface'; +export const AllSources: IDataSource[] = [XDADataSource, SapphireDataSource, OpenHistorianDataSource]; const DataSources: React.FunctionComponent = () => { - const dispatch = useAppDispatch(); - const dstStatus = useAppSelector(SelectDataSourceTypesStatus); - - React.useEffect(() => { - if (dstStatus === 'unitiated' || dstStatus === 'changed') - dispatch(FetchDataSourceTypes()); - }, [dstStatus]); - return (
-
+
@@ -62,7 +58,7 @@ const DataSources: React.FunctionComponent = () => {
-
+

Shared DataSources

@@ -84,7 +80,6 @@ const DataSourceTable = React.memo((props: ITableProps) => { const [dataSources, setDataSources] = React.useState([]); const dispatch = useAppDispatch(); - const dataSourceTypes = useAppSelector(SelectDataSourceTypes); const dsStatus = useAppSelector(SelectDataSourcesStatus); const allDataSources = useAppSelector(SelectDataSources); const [deleteItem, setDeleteItem] = React.useState(null); @@ -118,8 +113,7 @@ const DataSourceTable = React.memo((props: ITableProps) => { KeySelector={source => source.ID} Ascending={ascending}> Key={'Name'} Field={'Name'}>Name - Key={'DataSourceTypeID'} Field={'DataSourceTypeID'} - Content={row => dataSourceTypes.find(dst => row.item.DataSourceTypeID === dst.ID)?.Name}>Type + Key={'Type'} Field={'Type'}>Type { props.OwnedByUser ? AllowSort={false} Key={'Edit'} Field={'Public'} diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/OpenHistorianDataSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/OpenHistorianDataSource.tsx similarity index 97% rename from TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/OpenHistorianDataSource.tsx rename to TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/OpenHistorianDataSource.tsx index 7bce974d..8e9260c2 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/OpenHistorianDataSource.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/OpenHistorianDataSource.tsx @@ -26,9 +26,10 @@ import { DataSourceTypes, TrenDAP, Redux, DataSetTypes } from '../../../global'; import { useAppSelector, useAppDispatch } from '../../../hooks'; import * as React from 'react'; import { SelectOpenHistorian, FetchOpenHistorian } from '../../OpenHistorian/OpenHistorianSlice'; +import { IDataSource } from '../Interface'; -const OpenHistorianDataSource: DataSourceTypes.IDataSource<{}, TrenDAP.iOpenHistorianDataSet> = { +const OpenHistorianDataSource: IDataSource<{}, TrenDAP.iOpenHistorianDataSet> = { Name: 'OpenHistorian', DefaultSourceSettings: {}, DefaultDataSetSettings: { Devices: [], Phases: [], Types: [], Instance: "", Aggregate: '1w'}, diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/SapphireDataSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/SapphireDataSource.tsx similarity index 98% rename from TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/SapphireDataSource.tsx rename to TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/SapphireDataSource.tsx index 6f78e28c..93b54965 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/SapphireDataSource.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/SapphireDataSource.tsx @@ -26,8 +26,9 @@ import { DataSourceTypes, TrenDAP, Redux, DataSetTypes } from '../../../global'; import { Select, ArrayCheckBoxes, ArrayMultiSelect, Input } from '@gpa-gemstone/react-forms'; import { useAppSelector, useAppDispatch } from '../../../hooks'; import { SelectSapphire, FetchSapphire, SelectSapphireStatus } from '../../Sapphire/SapphireSlice'; +import { IDataSource } from '../Interface'; -const SapphireDataSource: DataSourceTypes.IDataSource<{}, TrenDAP.iSapphireDataSet> = { +const SapphireDataSource: IDataSource<{}, TrenDAP.iSapphireDataSet> = { Name: 'Sapphire', DefaultSourceSettings: {}, DefaultDataSetSettings: { IDs: [], Phases: [], Types: [], Aggregate: "", Harmonics: "" }, diff --git a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/XDADataSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/XDADataSource.tsx similarity index 96% rename from TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/XDADataSource.tsx rename to TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/XDADataSource.tsx index 9f148e5c..a6b345c2 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/DataSources/ReactDataSources/XDADataSource.tsx +++ b/TrenDAP/wwwroot/TypeScript/Features/DataSources/Implementations/XDADataSource.tsx @@ -27,7 +27,7 @@ import { OpenXDA } from '@gpa-gemstone/application-typings'; import { DataSourceTypes, TrenDAP, Redux, DataSetTypes } from '../../../global'; import { useAppSelector, useAppDispatch } from '../../../hooks'; import { SelectOpenXDA, FetchOpenXDA, SelectOpenXDAStatus } from '../../OpenXDA/OpenXDASlice'; -import { TypeCorrectSettings } from '../DataSourceWrapper'; +import { IDataSource, EnsureTypeSafety } from '../Interface'; import $ from 'jquery'; import queryString from 'querystring'; import moment from 'moment'; @@ -46,11 +46,11 @@ interface XDAChannel extends OpenXDA.Types.Channel { Latitude: number } -const XDADataSource: DataSourceTypes.IDataSource = { +const XDADataSource: IDataSource = { Name: 'TrenDAPDB', DefaultSourceSettings: { PQBrowserUrl: "http://localhost:44368/"}, DefaultDataSetSettings: { By: 'Meter', IDs: [], Phases: [], Groups: [], ChannelIDs: [], Aggregate: ''}, - ConfigUI: (props: DataSourceTypes.IConfigProps) => { + ConfigUI: (props: TrenDAP.ISourceConfig) => { React.useEffect(() => { const errors: string[] = []; if (props.Settings.PQBrowserUrl === null || props.Settings.PQBrowserUrl.length === 0) @@ -163,7 +163,7 @@ const XDADataSource: DataSourceTypes.IDataSource { return new Promise((resolve, reject) => { - const dataSetSettings = TypeCorrectSettings(setConn.Settings, XDADataSource.DefaultDataSetSettings); + const dataSetSettings = EnsureTypeSafety(setConn.Settings, XDADataSource.DefaultDataSetSettings); const returnData: DataSetTypes.IDataSetMetaData[] = dataSetSettings.ChannelIDs.map(id => ({ ID: id.toString(), Name: '', @@ -208,7 +208,7 @@ const XDADataSource: DataSourceTypes.IDataSource { return new Promise((resolve, reject) => { - const dataSetSettings = TypeCorrectSettings(setConn.Settings, XDADataSource.DefaultDataSetSettings); + const dataSetSettings = EnsureTypeSafety(setConn.Settings, XDADataSource.DefaultDataSetSettings); const returnData: DataSetTypes.IDataSetData[] = dataSetSettings.ChannelIDs.map(id => ({ ID: id.toString(), Name: '', @@ -272,8 +272,8 @@ const XDADataSource: DataSourceTypes.IDataSource Default Settings Objects, Unintiated Fields Match this Default +*/ +export function EnsureTypeSafety(settingsObj: any, defaultSettings: T): T { + const s = cloneDeep(defaultSettings); + for (const [k] of Object.entries(defaultSettings)) { + if (settingsObj.hasOwnProperty(k)) + s[k] = cloneDeep(settingsObj[k]); + } + return s; +} + +/* + Interface that needs to be implemented by an DataSource + {T} => Settings Associated with this Datasource + {U} => Settings associated with the specific Datasource and Dataset +*/ +export interface IDataSource { + DataSetUI: React.FC>, + ConfigUI: React.FC>, + LoadDataSetMeta: (dataSource: DataSourceTypes.IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: DataSourceTypes.IDataSourceDataSet) + => Promise, + LoadDataSet: (dataSource: DataSourceTypes.IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: DataSourceTypes.IDataSourceDataSet) + => Promise, + QuickViewDataSet?: (dataSource: DataSourceTypes.IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: DataSourceTypes.IDataSourceDataSet) + => string, + TestAuth: (dataSource: DataSourceTypes.IDataSourceView) + => Promise, + DefaultSourceSettings: T, + DefaultDataSetSettings: U, + Name: string, +} \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/AddEditEventSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/AddEditEventSource.tsx new file mode 100644 index 00000000..c3853483 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/AddEditEventSource.tsx @@ -0,0 +1,70 @@ +//****************************************************************************************************** +// AddEditEventSource.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + + +import * as React from 'react'; +import { useAppDispatch } from '../../hooks'; +import { UpdateEventSource, AddEventSource } from './Slices/EventSourcesSlice'; +import EventSource from './EventSource'; +import { EventSourceTypes } from './Interface'; +import { Modal } from '@gpa-gemstone/react-interactive'; +import { CrossMark } from '@gpa-gemstone/gpa-symbols'; + +interface IProps { + EventSource: EventSourceTypes.IEventSourceView, + Show: boolean, + SetShow: (shw: boolean) => void +} + +const AddEditEventSource: React.FunctionComponent = (props: IProps) => { + const dispatch = useAppDispatch(); + const [eventSource, setEventSource] = React.useState(props.EventSource); + const [errors, setErrors] = React.useState([]); + + React.useEffect(() => { setEventSource(props.EventSource); }, [props.EventSource]) + + if (eventSource == null) return <>; + return ( + 0} + Show={props.Show} + ShowX={true} + ConfirmText={'Save'} + ConfirmShowToolTip={errors.length > 0} + ConfirmToolTipContent={errors.map((e, i) =>

{CrossMark} {e}

)} + Title={`${eventSource.ID > -1 ? 'Edit' : 'Add New'} Event Data Source`} + CallBack={conf => { + if (conf) { + if (eventSource.ID > -1) dispatch(UpdateEventSource(eventSource)); + else dispatch(AddEventSource(eventSource)); + } + props.SetShow(false); + }} + > + +
+ ); +} + +export default AddEditEventSource; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/ByEventSources.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/ByEventSources.tsx new file mode 100644 index 00000000..b6e3b0d6 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/ByEventSources.tsx @@ -0,0 +1,168 @@ +//****************************************************************************************************** +// ByEventSources.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import _ from 'lodash'; +import { EventSourceTypes, IEventSource } from './Interface'; +import { useAppSelector, useAppDispatch } from '../../hooks'; +import { ReactTable } from '@gpa-gemstone/react-table'; +import { SelectEventSources, SelectEventSourcesStatus, FetchEventSources, RemoveEventSource } from './Slices/EventSourcesSlice'; +import { TrashCan, HeavyCheckMark, Pencil } from './../../Constants'; +import AddEditEventSource from './AddEditEventSource'; +import { Warning } from '@gpa-gemstone/react-interactive'; +import RandomEvents from './Implementations/RandomEvents'; +import OpenXDAEvents from './Implementations/OpenXDAEvents'; + +export const EventDataSources: IEventSource[] = [OpenXDAEvents, RandomEvents]; + +const ByEventSources: React.FunctionComponent = () => { + const dispatch = useAppDispatch(); + const [editEvt, setEditEvt] = React.useState(undefined); + const [showDelete, setShowDelete] = React.useState(false); + const [showEdit, setShowEdit] = React.useState(false); + + return ( +
+
+
+
+
+
+

My Event Data Sources

+
+
+ +
+
+
+
+ +
+
+
+
+
+

Shared Event Data Sources

+
+ +
+
+
+ + { + if (c) dispatch(RemoveEventSource(editEvt)); + setShowDelete(false); + }} /> +
+ ); +} + +interface ITableProps { + OwnedByUser: boolean, + SetEventSource: (evt: EventSourceTypes.IEventSourceView) => void, + SetShowEdit: (shw: boolean) => void, + SetShowDelete: (shw: boolean) => void +} + +const EventSourceTable = React.memo((props: ITableProps) => { + const [sortField, setSortField] = React.useState('Name'); + const [ascending, setAscending] = React.useState(true); + const [eventSources, setEventSources] = React.useState([]); + + const dispatch = useAppDispatch(); + const allEventSources = useAppSelector(SelectEventSources); + const evtStatus = useAppSelector(SelectEventSourcesStatus); + + + React.useEffect(() => { + if (evtStatus === 'unitiated' || evtStatus === 'changed') + dispatch(FetchEventSources()); + }, [evtStatus]); + + // #ToDO Clean up Slicing and sorting + React.useEffect(() => { + setEventSources(_.orderBy(allEventSources.filter(source => { + if (props.OwnedByUser) return source.User === userName; + else return source.Public && source.User !== userName; + }), [sortField], [ascending ? 'asc' : 'desc'])); + }, [sortField, ascending, allEventSources, props.OwnedByUser]); + + return ( +
+ + TableClass="table table-hover" + TableStyle={{ + padding: 0, width: 'calc(100%)', height: '100%', + tableLayout: 'fixed', overflow: 'hidden', display: 'flex', flexDirection: 'column', marginBottom: 0 + }} + TheadStyle={{ fontSize: 'auto', tableLayout: 'fixed', display: 'table', width: '100%' }} + TbodyStyle={{ display: 'block', overflowY: 'scroll', flex: 1 }} + RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} + SortKey={sortField} + OnClick={() => { }} + OnSort={data => { + if (data.colKey === sortField) setAscending(s => !s); + else setSortField(data.colKey); + }} + Data={eventSources} + KeySelector={source => source.ID} + Ascending={ascending}> + Key={'Name'} Field={'Name'}>Name + Key={'Type'} Field={'Type'}>Type + { + props.OwnedByUser ? + AllowSort={false} Key={'Edit'} Field={'Public'} + Content={row => {row.item.Public ? HeavyCheckMark : null}}>Shared + : <> + } + { + props.OwnedByUser ? + AllowSort={false} Key={'Delete'} Field={'Public'} + Content={row => + + + + } + ><> + : <> + } + +
+ ); +}); + +export default ByEventSources; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventDataSourceWrapper.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventDataSourceWrapper.tsx new file mode 100644 index 00000000..835428d7 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventDataSourceWrapper.tsx @@ -0,0 +1,81 @@ +//****************************************************************************************************** +// EventDataSourceWrapper.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 09/25/2020 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import * as _ from 'lodash'; +import { TrenDAP } from '../../global'; +import { EventSourceTypes, IEventSource } from './Interface'; +import { EventDataSources } from './ByEventSources'; + +interface IProps { + DataSet: TrenDAP.iDataSet, + Connection: EventSourceTypes.IEventSourceDataSet, + EventDataSource: EventSourceTypes.IEventSourceView, + SetConnection: (arg: EventSourceTypes.IEventSourceDataSet) => void, + SetErrors: (e: string[]) => void +} + +const EventDataSourceWrapper: React.FunctionComponent = (props: IProps) => { + const implementation: IEventSource | null = React.useMemo(() => EventDataSources.find(t => t.Name == props.EventDataSource?.Type), [props.EventDataSource?.Type]); + + const settings = React.useMemo(() => { + if (implementation == null) + return {}; + const s = _.cloneDeep(implementation.DefaultDataSetSettings ?? {}); + let custom = props.Connection.Settings; + + for (const [k] of Object.entries(implementation?.DefaultDataSetSettings ?? {})) { + if (custom.hasOwnProperty(k)) + s[k] = _.cloneDeep(custom[k]); + } + return s; + }, [implementation, props.Connection.Settings]); + + // Ensure that source settings are valid + const eventSource = React.useMemo(() => { + if (implementation == null) + return props.EventDataSource; + const src = _.cloneDeep(props.EventDataSource); + const sourceSettings = _.cloneDeep(implementation.DefaultSourceSettings ?? {}); + let custom = props.EventDataSource.Settings; + for (const [k] of Object.entries(sourceSettings)) { + if (custom.hasOwnProperty(k)) + sourceSettings[k] = _.cloneDeep(custom[k]); + } + src.Settings = sourceSettings; + return src; + }, [props.EventDataSource]); + + return ( +
+ {implementation != null ? props.SetConnection({ ...props.Connection, Settings: s })} /> : <>} +
+ ); +} + +export default EventDataSourceWrapper; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventSource.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventSource.tsx new file mode 100644 index 00000000..7bf30743 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/EventSource.tsx @@ -0,0 +1,78 @@ +//****************************************************************************************************** +// EventSources.tsx - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import * as _ from 'lodash'; +import { EventSourceTypes, IEventSource } from './Interface'; +import { EventDataSources } from './ByEventSources'; +import { Input, Select, CheckBox } from '@gpa-gemstone/react-forms'; + +interface IProps { + EventSource: EventSourceTypes.IEventSourceView, + SetEventSource: (ds: EventSourceTypes.IEventSourceView) => void, + SetErrors: (e: string[]) => void +} + +const EventSource: React.FunctionComponent = (props: IProps) => { + const [configErrors, setConfigErrors] = React.useState([]); + const implementation: IEventSource | null = React.useMemo(() => EventDataSources.find(t => t.Name == props.EventSource.Type), [props.EventSource.Type]) + + const settings = React.useMemo(() => { + if (implementation == null) + return {}; + const s = _.cloneDeep(implementation.DefaultSourceSettings ?? {}); + let custom = props.EventSource.Settings; + + for (const [k] of Object.entries(implementation?.DefaultSourceSettings ?? {})) { + if (custom.hasOwnProperty(k)) + s[k] = _.cloneDeep(custom[k]); + } + return s; + }, [implementation, props.EventSource.Settings]); + + React.useEffect(() => { + const errors: string[] = []; + if (!valid('Name')) errors.push("Name between 0 and 200 characters is required."); + props.SetErrors([...errors, ...configErrors]); + }, [props.EventSource.Name, configErrors]) + + function valid(field: keyof (EventSourceTypes.IEventSourceView)): boolean { + if (field == 'Name') + return props.EventSource.Name != null && props.EventSource.Name.length > 0 && props.EventSource.Name.length <= 200; + return false; + } + + return ( +
+ Record={props.EventSource} Field="Name" Setter={props.SetEventSource} Valid={valid} /> + Record={props.EventSource} Label="Type" Field="Type" Setter={props.SetEventSource} + Options={EventDataSources.map((type) => ({ Value: type.Name, Label: type.Name }))} /> + Record={props.EventSource} Field="URL" Setter={props.SetEventSource} Valid={() => true} /> + Record={props.EventSource} Field="RegistrationKey" Label={'API Key'} Setter={props.SetEventSource} Valid={() => true} /> + Record={props.EventSource} Field="Public" Label='Shared' Setter={props.SetEventSource} /> + {implementation != null ? props.SetEventSource({ ...props.EventSource, Settings: s })} /> : <>} + + ); +} + +export default EventSource; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/OpenXDAEvents.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/OpenXDAEvents.tsx new file mode 100644 index 00000000..dd3b571f --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/OpenXDAEvents.tsx @@ -0,0 +1,488 @@ +//****************************************************************************************************** +// OpenXDAEvents.tsx - Gbtc +// +// Copyright © 2024, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/07/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import * as $ from 'jquery'; +import _ from 'lodash'; +import moment from 'moment'; +import queryString from 'querystring'; +import { TrenDAP, Redux } from '../../../global'; +import { SelectOpenXDA, FetchOpenXDA, SelectOpenXDAStatus } from '../../OpenXDA/OpenXDASlice'; +import { useAppSelector, useAppDispatch } from '../../../hooks'; +import { EventSourceTypes, IEventSource, EnsureTypeSafety } from '../Interface'; +import { ComputeTimeCenterAndSize } from '../../DataSets/HelperFunctions'; +import { ArrayCheckBoxes, ArrayMultiSelect, Input, Select } from '@gpa-gemstone/react-forms'; +import { OpenXDA } from '@gpa-gemstone/application-typings'; + +const encodedDateFormat = 'MM/DD/YYYY'; +const encodedTimeFormat = 'HH:mm:ss.SSS'; + +interface ISetting { PQBrowserUrl: string } +interface IDatasetSetting { + // Todo: Replace this with 4 arrays that match eventsearch on sebrowser after we get access to generic slices + By: 'Asset' | 'Meter', IDs: number[], + Phases: number[], + LegacyPhases: { AN: boolean, BN: boolean, CN: boolean, AB: boolean, BC: boolean, CA: boolean, ABG: boolean, BCG: boolean, ABC: boolean, ABCG: boolean }, + Types: number[], + CurveID: number | null, + CurveInside: boolean, + DurationMin: number | null, + DurationMax: number | null, + TransientMin: number | null, + TransientMax: number | null, + TransientType: 'LL' | 'LN' | 'both', + SagMin: number | null, + SagMax: number | null, + SagType: 'LL' | 'LN' | 'both', + SwellMin: number | null, + SwellMax: number | null, + SwellType: 'LL' | 'LN' | 'both' +} + +const OpenXDAEvents: IEventSource = { + Name: 'OpenXDA', + DefaultSourceSettings: { PQBrowserUrl: "http://localhost:44368/" }, + DefaultDataSetSettings: { + By: 'Meter', + IDs: [], + Phases: [], + LegacyPhases: { AN: false, BN: false, CN: false, AB: false, BC: false, CA: false, ABG: false, BCG: false, ABC: false, ABCG: false }, + Types: [], + DurationMin: null, + DurationMax: null, + SwellMin: null, + SwellMax: null, + SagMin: null, + SagMax: null, + TransientMin: null, + TransientMax: null, + CurveID: null, + CurveInside: true, + TransientType: 'both', + SagType: 'both', + SwellType: 'both' + }, + ConfigUI: (props: TrenDAP.ISourceConfig) => { + React.useEffect(() => { + const errors: string[] = []; + if (props.Settings.PQBrowserUrl === null || props.Settings.PQBrowserUrl.length === 0) + errors.push("PQ Browser URL is required by datasource."); + props.SetErrors(errors); + }, [props.Settings]); + + function valid(field: string): boolean { + if (field === 'PQBrowserUrl') return (props.Settings.PQBrowserUrl !== null && props.Settings.PQBrowserUrl.length !== 0); + return true; + } + + return ; + }, + DataSetUI: (props: EventSourceTypes.IEventSourceDataSetProps) => { + const dispatch = useAppDispatch(); + const phases: OpenXDA.Types.Phase[] = useAppSelector((state: Redux.StoreState) => SelectOpenXDA(state, props.EventSource.ID, 'Phase', 'event')); + const phaseStatus: TrenDAP.Status = useAppSelector((state: Redux.StoreState) => SelectOpenXDAStatus(state, props.EventSource.ID, 'Phase', 'event')); + const meters: OpenXDA.Types.Meter[] = useAppSelector((state: Redux.StoreState) => SelectOpenXDA(state, props.EventSource.ID, 'Meter', 'event')); + const meterStatus: TrenDAP.Status = useAppSelector((state: Redux.StoreState) => SelectOpenXDAStatus(state, props.EventSource.ID, 'Meter', 'event')); + const assets: OpenXDA.Types.Asset[] = useAppSelector((state: Redux.StoreState) => SelectOpenXDA(state, props.EventSource.ID, 'Asset', 'event')); + const assetStatus: TrenDAP.Status = useAppSelector((state: Redux.StoreState) => SelectOpenXDAStatus(state, props.EventSource.ID, 'Asset', 'event')); + const types: OpenXDA.Types.EventType[] = useAppSelector((state: Redux.StoreState) => SelectOpenXDA(state, props.EventSource.ID, 'EventType', 'event')); + const typeStatus: TrenDAP.Status = useAppSelector((state: Redux.StoreState) => SelectOpenXDAStatus(state, props.EventSource.ID, 'EventType', 'event')); + const curves: any[] = useAppSelector((state: Redux.StoreState) => SelectOpenXDA(state, props.EventSource.ID, 'StandardMagDurCurve', 'event')); + const curveStatus: TrenDAP.Status = useAppSelector((state: Redux.StoreState) => SelectOpenXDAStatus(state, props.EventSource.ID, 'StandardMagDurCurve', 'event')); + + React.useEffect(() => { + const errors: string[] = []; + if (!valid('DurationMin') || !valid('DurationMax')) + errors.push('Duration range is not valid.'); + if (!valid('SagMin') || !valid('SagMax')) + errors.push('Sag range is not valid.'); + if (!valid('SwellMin') || !valid('SwellMax')) + errors.push('Swell range is not valid.'); + if (!valid('TransientMin') || !valid('TransientMax')) + errors.push('Transient range is not valid.'); + props.SetErrors(errors); + }, [props.Settings]); + + React.useEffect(() => { + if (phaseStatus === 'unitiated' || phaseStatus === 'changed') + dispatch(FetchOpenXDA({ dataSourceID: props.EventSource.ID, sourceType: 'event', table: 'Phase' })); + }, [phaseStatus]); + + React.useEffect(() => { + if (meterStatus === 'unitiated' || meterStatus === 'changed') + dispatch(FetchOpenXDA({ dataSourceID: props.EventSource.ID, sourceType: 'event', table: 'Meter' })); + }, [meterStatus]); + + React.useEffect(() => { + if (assetStatus === 'unitiated' || assetStatus === 'changed') + dispatch(FetchOpenXDA({ dataSourceID: props.EventSource.ID, sourceType: 'event', table: 'Asset' })); + }, [assetStatus]); + + React.useEffect(() => { + if (typeStatus === 'unitiated' || typeStatus === 'changed') + dispatch(FetchOpenXDA({ dataSourceID: props.EventSource.ID, sourceType: 'event', table: 'EventType' })); + }, [typeStatus]); + + React.useEffect(() => { + if (curveStatus === 'unitiated' || curveStatus === 'changed') + dispatch(FetchOpenXDA({ dataSourceID: props.EventSource.ID, sourceType: 'event', table: 'StandardMagDurCurve' })); + }, [curveStatus]); + + function valid(field: keyof IDatasetSetting) { + function NullOrNaN(val) { + return val == null || isNaN(val); + } + + if (field == 'DurationMin') + return NullOrNaN(props.Settings.DurationMin) || ( + props.Settings.DurationMin >= 0 && props.Settings.DurationMin < 100 && + (NullOrNaN(props.Settings.DurationMax) || + props.Settings.DurationMax >= props.Settings.DurationMin)); + if (field == 'DurationMax') + return NullOrNaN(props.Settings.DurationMax) || ( + props.Settings.DurationMax >= 0 && props.Settings.DurationMax < 100 && + (NullOrNaN(props.Settings.DurationMin) || + props.Settings.DurationMax >= props.Settings.DurationMin)); + if (field == 'SagMin') + return NullOrNaN(props.Settings.SagMin) || ( + props.Settings.SagMin >= 0 && props.Settings.SagMin < 1 && + (NullOrNaN(props.Settings.SagMax) || + props.Settings.SagMax >= props.Settings.SagMin)); + if (field == 'SagMax') + return NullOrNaN(props.Settings.SagMax) || ( + props.Settings.SagMax >= 0 && props.Settings.SagMax < 1 && + (NullOrNaN(props.Settings.SagMax) || + props.Settings.SagMax >= props.Settings.SagMax)); + if (field == 'SwellMin') + return NullOrNaN(props.Settings.SwellMin) || ( + props.Settings.SwellMin >= 1 && props.Settings.SwellMin < 9999 && + (NullOrNaN(props.Settings.SwellMax) || + props.Settings.SwellMax >= props.Settings.SwellMin)); + if (field == 'SwellMax') + return NullOrNaN(props.Settings.SwellMax) || ( + props.Settings.SwellMax >= 1 && props.Settings.SwellMax < 9999 && + (NullOrNaN(props.Settings.SwellMin) || + props.Settings.SwellMax >= props.Settings.SwellMin)); + if (field == 'TransientMin') + return NullOrNaN(props.Settings.TransientMin) || ( + props.Settings.TransientMin >= 0 && props.Settings.TransientMin < 9999 && + (NullOrNaN(props.Settings.TransientMax) || + props.Settings.TransientMax >= props.Settings.TransientMin)); + if (field == 'TransientMax') + return NullOrNaN(props.Settings.TransientMax) || ( + props.Settings.TransientMax >= 0 && props.Settings.TransientMax < 9999 && + (NullOrNaN(props.Settings.TransientMin) || + props.Settings.TransientMax >= props.Settings.TransientMin)); + + return true; + } + + function setPhases(record: IDatasetSetting) { + const phaseFilter = { ...props.Settings.LegacyPhases }; + Object.keys(phaseFilter).forEach(phaseField => { + const phaseId = phases.find(p => p.Name == phaseField)?.ID ?? -1; + phaseFilter[phaseField] = (phaseId !== -1) && + (record.Phases.findIndex(p => p == phaseId) !== -1); + }); + const newRecord: IDatasetSetting = { ...record, LegacyPhases: phaseFilter }; + props.SetSettings(newRecord); + } + + return ( +
+
+ Record={props.Settings} Field="By" Options={[{ Value: 'Meter', Label: 'Meter' }, { Value: 'Asset', Label: 'Asset' }]} Setter={props.SetSettings} /> + Style={{ height: window.innerHeight - 560 }} Record={props.Settings} Field="IDs" Setter={props.SetSettings} + Options={(props.Settings.By == 'Meter' ? meters?.map(m => ({ Value: m.ID.toString(), Label: m.Name })) : assets?.map(m => ({ Value: m.ID.toString(), Label: m.AssetName }))) ?? []} /> +
+
+ Record={props.Settings} Checkboxes={types?.map(m => ({ ID: m.ID.toString(), Label: m.Name })) ?? []} Field="Types" Setter={props.SetSettings} /> + Record={props.Settings} Checkboxes={phases?.map(m => ({ ID: m.ID.toString(), Label: m.Name })) ?? []} Field="Phases" Setter={setPhases} /> +
+
+
+
+
+ Record={props.Settings} Label='Mag-Dur:' Field='CurveID' Setter={props.SetSettings} EmptyOption={true} EmptyLabel='' + Options={curves.map((v) => (v.Area != null && v.Area.length > 0 ? { Value: v.ID.toString(), Label: v.Name } : null))} /> +
+
+ props.SetSettings({ ...props.Settings, CurveInside: true })} + checked={props.Settings.CurveInside} /> + +
+
+ props.SetSettings({ ...props.Settings, CurveInside: false })} + checked={!props.Settings.CurveInside} /> + +
+
+
+
+
+
+
+
+ +
+
+
+
+ Record={props.Settings} Label='' Field='SagMin' Setter={props.SetSettings} + Valid={valid} Feedback={'Min must be less than max and between 0 and 1'} Type='number' Size={'small'} AllowNull={true} /> +
+
+ to +
+
+ Record={props.Settings} Label='' Field='SagMax' Setter={props.SetSettings} + Valid={valid} Feedback={'Max must be greater than min and between 0 and 1'} Type='number' Size={'small'} AllowNull={true} /> +
+
+
+
+
+ props.SetSettings({ ...props.Settings, SagType: 'LL' })} + checked={props.Settings?.SagType === 'LL'} /> + +
+
+ props.SetSettings({ ...props.Settings, SagType: 'LN' })} + checked={props.Settings?.SagType === 'LN'} /> + +
+
+ props.SetSettings({ ...props.Settings, SagType: 'both' })} + checked={props.Settings?.SagType === 'both'} /> + +
+
+
+
+
+
+
+ +
+
+
+
+ Record={props.Settings} Label='' Field='TransientMin' Setter={props.SetSettings} + Valid={valid} Feedback={'Min must be less than max and between 0 and 9999'} Type='number' Size={'small'} AllowNull={true} /> +
+
+ to +
+
+ Record={props.Settings} Label='' Field='TransientMax' Setter={props.SetSettings} + Valid={valid} Feedback={'Max must be greater than min and between 0 and 9999'} Type='number' Size={'small'} AllowNull={true} /> +
+
+
+
+
+ props.SetSettings({ ...props.Settings, TransientType: 'LL' })} + checked={props.Settings?.TransientType === 'LL'} /> + +
+
+ props.SetSettings({ ...props.Settings, TransientType: 'LN' })} + checked={props.Settings?.TransientType === 'LN'} /> + +
+
+ props.SetSettings({ ...props.Settings, TransientType: 'both' })} + checked={props.Settings?.TransientType === 'both'} /> + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ Record={props.Settings} Label='' Field='SwellMin' Setter={props.SetSettings} + Valid={valid} Feedback={'Min must be less than max and between 1 and 9999'} Type='number' Size={'small'} AllowNull={true} /> +
+
+ to +
+
+ Record={props.Settings} Label='' Field='SwellMax' Setter={props.SetSettings} + Valid={valid} Feedback={'Max must be greater than min and between 1 and 9999'} Type='number' Size={'small'} AllowNull={true} /> +
+
+
+
+
+ props.SetSettings({ ...props.Settings, SwellType: 'LL' })} + checked={props.Settings?.SwellType === 'LL'} /> + +
+
+ props.SetSettings({ ...props.Settings, SwellType: 'LN' })} + checked={props.Settings?.SwellType === 'LN'} /> + +
+
+ props.SetSettings({ ...props.Settings, SwellType: 'both' })} + checked={props.Settings?.SwellType === 'both'} /> + +
+
+
+
+
+
+
+ +
+
+
+ Record={props.Settings} Label='' Field='DurationMin' Setter={props.SetSettings} + Valid={valid} Feedback={'Min must be less than max and between 0 and 100'} Type='number' Size={'small'} AllowNull={true} /> +
+
+ to +
+
+ Record={props.Settings} Label='' Field='DurationMax' Setter={props.SetSettings} + Valid={valid} Feedback={'Max must be greater than min and between 0 and 100'} Type='number' Size={'small'} AllowNull={true} /> +
+
+
+
+
+
+
+
+ ); + + }, + Load: function (_dataSource: EventSourceTypes.IEventSourceView, _dataSet: TrenDAP.iDataSet, setConn: EventSourceTypes.IEventSourceDataSet): Promise { + return new Promise((resolve, reject) => { + $.ajax({ + type: "Get", + url: `${homePath}api/EventSourceDataSet/Query/${setConn.ID}`, + contentType: "application/json; charset=utf-8", + dataType: 'text', + cache: true, + async: true + }).done((data: string) => { + resolve(JSON.parse(data)); + }).fail(err => reject(err)); + }); + }, + QuickView: function (eventSource: EventSourceTypes.IEventSourceView, dataSet: TrenDAP.iDataSet, setConn: EventSourceTypes.IEventSourceDataSet): string { + const dataSetSettings = EnsureTypeSafety(setConn.Settings, OpenXDAEvents.DefaultDataSetSettings); + const sourceSettings = EnsureTypeSafety(eventSource.Settings, OpenXDAEvents.DefaultSourceSettings); + const queryParams: any = {}; + + // Time filter on the other side takes center time and a unit number + const time = ComputeTimeCenterAndSize(dataSet, 'hours'); + queryParams['time'] = time.Center.format(encodedTimeFormat); + queryParams['date'] = time.Center.format(encodedDateFormat); + queryParams['windowSize'] = time.Size; + queryParams['timeWindowUnits'] = 3; // hours + + function processArray(array: number[], name: string) { + if (array.length > 0 && array.length < 100) array.forEach((arg, index) => queryParams[name + index] = arg); + } + processArray(dataSetSettings.Types, 'types'); + processArray(dataSetSettings.IDs, dataSetSettings.By === 'Meter' ? 'meters' : 'assets'); + + // Handle Curve Filter + if (dataSetSettings.CurveID != null) + queryParams['curveID'] = dataSetSettings.CurveID; + queryParams['curveInside'] = dataSetSettings.CurveInside; + queryParams['curveOutside'] = !dataSetSettings.CurveInside; + + // Handle types + queryParams['sagType'] = dataSetSettings.SagType; + queryParams['swellType'] = dataSetSettings.SwellType; + queryParams['transientType'] = dataSetSettings.TransientType; + + // Handle ranges + if (dataSetSettings.DurationMin != null) queryParams['durationMin'] = dataSetSettings.DurationMin; + if (dataSetSettings.DurationMax != null) queryParams['durationMax'] = dataSetSettings.DurationMax; + if (dataSetSettings.TransientMin != null) queryParams['transientMin'] = dataSetSettings.TransientMin; + if (dataSetSettings.TransientMax != null) queryParams['transientMax'] = dataSetSettings.TransientMax; + if (dataSetSettings.SagMin != null) queryParams['sagMin'] = dataSetSettings.SagMin; + if (dataSetSettings.SagMax != null) queryParams['sagMax'] = dataSetSettings.SagMax; + if (dataSetSettings.SwellMax != null) queryParams['swellMax'] = dataSetSettings.SwellMax; + if (dataSetSettings.SwellMin != null) queryParams['swellMin'] = dataSetSettings.SwellMin; + + queryParams['PhaseAN'] = dataSetSettings.LegacyPhases.AN; + queryParams['PhaseBN'] = dataSetSettings.LegacyPhases.BN; + queryParams['PhaseCN'] = dataSetSettings.LegacyPhases.CN; + queryParams['PhaseAB'] = dataSetSettings.LegacyPhases.AB; + queryParams['PhaseBC'] = dataSetSettings.LegacyPhases.BC; + queryParams['PhaseCA'] = dataSetSettings.LegacyPhases.CA; + queryParams['PhaseABG'] = dataSetSettings.LegacyPhases.ABG; + queryParams['PhaseBCG'] = dataSetSettings.LegacyPhases.BCG; + queryParams['PhaseABC'] = dataSetSettings.LegacyPhases.ABC; + queryParams['PhaseABCG'] = dataSetSettings.LegacyPhases.ABCG; + + const queryUrl = queryString.stringify(queryParams, "&", "=", { encodeURIComponent: queryString.escape }); + // Regex removes trailing / + return `${sourceSettings.PQBrowserUrl.replace(/[\/]$/, '')}/eventsearch?${queryUrl}`; + }, + TestAuth: function (eventSource: EventSourceTypes.IEventSourceView): Promise { + return new Promise((resolve, reject) => { + $.ajax({ + type: "GET", + url: `${homePath}api/EventSource/TestAuth/${eventSource.ID}`, + contentType: "application/json; charset=utf-8", + cache: true, + async: true + }).done((data: string) => { + if (data === "1") resolve(true); + else { + console.error(data); + resolve(false); + } + }).fail(() => { + reject("Unable to resolve auth test."); + }); + }); + } +} +export default OpenXDAEvents; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/RandomEvents.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/RandomEvents.tsx new file mode 100644 index 00000000..bf386238 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Implementations/RandomEvents.tsx @@ -0,0 +1,59 @@ +//****************************************************************************************************** +// RandomEvents.tsx - Gbtc +// +// Copyright © 2024, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import _ from 'lodash'; +import { EventSourceTypes, IEventSource } from '../Interface'; +import { TrenDAP } from '../../../global'; +import { Input } from '@gpa-gemstone/react-forms'; +import { ComputeTimeEnds } from '../../DataSets/HelperFunctions' + +interface ISetting { Title: string } +interface IDatasetSetting { Number: number } + +const RandomEvents: IEventSource = { + DataSetUI: (props) => Record={props.Settings} Field="Number" Setter={props.SetSettings} Valid={() => true} />, + ConfigUI: (props) => Record={props.Settings} Field="Title" Setter={props.SetSettings} Valid={() => true} />, + Load: (eventSource: EventSourceTypes.IEventSourceView, dataSet: TrenDAP.iDataSet, dataConn: EventSourceTypes.IEventSourceDataSet) => { + const time = ComputeTimeEnds(dataSet); + const startTimeValue = time.Start.valueOf(); + const result: TrenDAP.IEvent[] = []; + let n = 0; + const t = (time.End.valueOf() - startTimeValue) / dataConn.Settings.Number; + while (n < dataConn.Settings.Number) { + result.push( { + Time: n * t + startTimeValue, + Description: 'Test', + Title: eventSource.Settings.Title, + Duration: 0.5*t + } ) + n = n + 1; + } + return Promise.resolve(result) + }, + TestAuth: () => Promise.resolve(true), + DefaultSourceSettings: { Title: 'test' }, + DefaultDataSetSettings: { Number: 1 }, + Name: 'Random', +} +export default RandomEvents; \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/Interface.tsx b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Interface.tsx new file mode 100644 index 00000000..29c289f7 --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Interface.tsx @@ -0,0 +1,90 @@ +//****************************************************************************************************** +// Interface.tsx - Gbtc +// +// Copyright © 2024, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** +import { cloneDeep } from 'lodash'; +import { TrenDAP } from '../../global' +// The intrefaces for Event Datasources +// Interfaces = connection points to other pieces in the architecture + +export namespace EventSourceTypes { + // The following are how event sources are stored in DB + export interface IEventSourceView { + ID: number, + Name: string, + Type: string, + URL: string, + RegistrationKey: string, + APIToken: string, + Public: boolean, + User: string, + Settings: any + } + + export interface IEventSourceDataSet { + ID: number, + EventSourceName: string, + EventSourceID: number, + DataSetName: string, + DataSetID: number, + Settings: any + } + + // Eventsource as tsx needs them + export interface IEventSourceDataSetProps { + // Event Source from DB + EventSource: IEventSourceView, + // Data Set From DB + DataSet: TrenDAP.iDataSet, + // Additional DataSet Settings parsed from dataset connection + Settings: U, + SetSettings: (newDataSetSettings: U) => void, + SetErrors: (errors: string[]) => void + } +} + +/* Helper Function to ensure type safety on settings objects + {T} => Default Settings Objects, Unintiated Fields Match this Default +*/ +export function EnsureTypeSafety(settingsObj: any, defaultSettings: T): T { + const s = cloneDeep(defaultSettings); + for (const [k] of Object.entries(defaultSettings)) { + if (settingsObj.hasOwnProperty(k)) + s[k] = cloneDeep(settingsObj[k]); + } + return s; +} + +/* + Interface that needs to be implemented by an EventSource + {T} => Settings Associated with this Eventsource + {U} => Settings associated with the speicific Eventsource and Dataset +*/ +export interface IEventSource { + DataSetUI: React.FC>, + ConfigUI: React.FC>, + Load: (eventSource: EventSourceTypes.IEventSourceView, dataSet: TrenDAP.iDataSet, dataConn: EventSourceTypes.IEventSourceDataSet) => Promise, + QuickView?: (eventSource: EventSourceTypes.IEventSourceView, dataSet: TrenDAP.iDataSet, dataConn: EventSourceTypes.IEventSourceDataSet) => string, + TestAuth: (eventSource: EventSourceTypes.IEventSourceView) => Promise, + DefaultSourceSettings: T, + DefaultDataSetSettings: U, + Name: string, +} \ No newline at end of file diff --git a/TrenDAP/wwwroot/TypeScript/Features/EventSources/Slices/EventSourcesSlice.ts b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Slices/EventSourcesSlice.ts new file mode 100644 index 00000000..cbfbd19a --- /dev/null +++ b/TrenDAP/wwwroot/TypeScript/Features/EventSources/Slices/EventSourcesSlice.ts @@ -0,0 +1,188 @@ +//****************************************************************************************************** +// EventSourcesSlice.ts - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 04/29/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { Redux } from '../../../global'; +import { EventSourceTypes } from '../Interface'; +import _ from 'lodash'; +import { ajax } from 'jquery'; + +// #region [ Thunks ] +export const FetchEventSources = createAsyncThunk('EventSources/FetchEventSources', async (_, { dispatch }) => { + return await GetEventSources(); +}); + +export const AddEventSource = createAsyncThunk('EventSources/AddEventSource', async (EventSource: EventSourceTypes.IEventSourceView) => { + return await PostEventSource(EventSource); +}); + +export const RemoveEventSource = createAsyncThunk('EventSources/RemoveEventSource', async (EventSource: EventSourceTypes.IEventSourceView, { dispatch }) => { + return await DeleteEventSource(EventSource); +}); + +export const UpdateEventSource = createAsyncThunk('EventSources/UpdateEventSource', async (EventSource: EventSourceTypes.IEventSourceView, { dispatch }) => { + return await PatchEventSource(EventSource); +}); +// #endregion + +// #region [ Slice ] +export const EventSourcesSlice = createSlice({ + name: 'EventSources', + initialState: { + Status: 'unitiated', + Data: [], + Error: null, + SortField: 'Name', + Ascending: true + } as Redux.State, + reducers: { + Sort: (state, action) => { + if(state.SortField === action.payload.SortField) + state.Ascending = !action.payload.Ascending; + else + state.SortField = action.payload.SortField; + + const sorted = _.orderBy(state.Data, [state.SortField], [state.Ascending ? "asc" : "desc"]) + state.Data = sorted; + } + }, + extraReducers: (builder) => { + + builder.addCase(FetchEventSources.fulfilled, (state, action) => { + state.Status = 'idle'; + state.Error = null; + + const sorted = _.orderBy(action.payload, [state.SortField], [state.Ascending ? "asc" : "desc"]) + state.Data = sorted; + + }); + builder.addCase(FetchEventSources.pending, (state, action) => { + state.Status = 'loading'; + }); + builder.addCase(FetchEventSources.rejected, (state, action) => { + state.Status = 'error'; + state.Error = action.error.message; + + }); + builder.addCase(AddEventSource.pending, (state, action) => { + state.Status = 'loading'; + }); + builder.addCase(AddEventSource.rejected, (state, action) => { + state.Status = 'error'; + state.Error = action.error.message; + + }); + builder.addCase(AddEventSource.fulfilled, (state, action) => { + state.Status = 'changed'; + state.Error = null; + }); + builder.addCase(RemoveEventSource.pending, (state, action) => { + state.Status = 'loading'; + }); + builder.addCase(RemoveEventSource.rejected, (state, action) => { + state.Status = 'error'; + state.Error = action.error.message; + + }); + builder.addCase(RemoveEventSource.fulfilled, (state, action) => { + state.Status = 'changed'; + state.Error = null; + }); + builder.addCase(UpdateEventSource.pending, (state, action) => { + state.Status = 'loading'; + }); + builder.addCase(UpdateEventSource.rejected, (state, action) => { + state.Status = 'error'; + state.Error = action.error.message; + + }); + builder.addCase(UpdateEventSource.fulfilled, (state, action) => { + state.Status = 'changed'; + state.Error = null; + }); + + } + +}); + +export const {Sort} = EventSourcesSlice.actions; +export default EventSourcesSlice.reducer; +// #endregion + +// #region [ Selectors ] +export const SelectEventSources = (state: Redux.StoreState) => state.EventSources.Data; +export const SelectEventSourcesStatus = (state: Redux.StoreState) => state.EventSources.Status; +export const SelectEventSourcesSortField = (state: Redux.StoreState) => state.EventSources.SortField; +export const SelectEventSourcesAscending = (state: Redux.StoreState) => state.EventSources.Ascending; + +// #endregion + +// #region [ Async Functions ] + +function GetEventSources(): JQuery.jqXHR { + return ajax({ + type: "GET", + url: `${homePath}api/EventSource`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); +} + +function PostEventSource(EventSource: EventSourceTypes.IEventSourceView): JQuery.jqXHR { + return ajax({ + type: "POST", + url: `${homePath}api/EventSource`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify({ ...EventSource, User: userName }), + cache: false, + async: true + }); +} + +function DeleteEventSource(EventSource: EventSourceTypes.IEventSourceView): JQuery.jqXHR { + return ajax({ + type: "DELETE", + url: `${homePath}api/EventSource`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify(EventSource), + cache: false, + async: true + }); +} + +function PatchEventSource(EventSource: EventSourceTypes.IEventSourceView): JQuery.jqXHR { + return ajax({ + type: "PATCH", + url: `${homePath}api/EventSource`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify(EventSource), + cache: false, + async: true + }); +} + +// #endregion diff --git a/TrenDAP/wwwroot/TypeScript/Features/OpenHistorian/OpenHistorianSlice.ts b/TrenDAP/wwwroot/TypeScript/Features/OpenHistorian/OpenHistorianSlice.ts index 90cfc024..cce1a136 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/OpenHistorian/OpenHistorianSlice.ts +++ b/TrenDAP/wwwroot/TypeScript/Features/OpenHistorian/OpenHistorianSlice.ts @@ -103,7 +103,7 @@ function GetOpenHistorian(dataSourceID: number): Promise<{ MetaData: any, Instan return new Promise(async (res, rej) => { let instances = await $.ajax({ type: "GET", - url: `${homePath}api/TrenDAPDB/${dataSourceID}/GetInstances`, + url: `${homePath}api/OpenHistorian/${dataSourceID}/GetInstances`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, @@ -112,7 +112,7 @@ function GetOpenHistorian(dataSourceID: number): Promise<{ MetaData: any, Instan let table = await $.ajax({ type: "GET", - url: `${homePath}api/TrenDAPDB/${dataSourceID}/GetMetaData`, + url: `${homePath}api/OpenHistorian/${dataSourceID}/GetMetaData`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, diff --git a/TrenDAP/wwwroot/TypeScript/Features/OpenXDA/OpenXDASlice.ts b/TrenDAP/wwwroot/TypeScript/Features/OpenXDA/OpenXDASlice.ts index 42d6200a..9d8c1ab2 100644 --- a/TrenDAP/wwwroot/TypeScript/Features/OpenXDA/OpenXDASlice.ts +++ b/TrenDAP/wwwroot/TypeScript/Features/OpenXDA/OpenXDASlice.ts @@ -27,12 +27,19 @@ import { Redux, TrenDAP } from '../../global'; import $ from 'jquery'; import { Search } from '@gpa-gemstone/react-interactive'; -export const FetchOpenXDA = createAsyncThunk('OpenXDA/FetchOpenXDA', async (ds ,{ dispatch }) => { - return await GetOpenXDA(ds.dataSourceID, ds.table) +export const FetchOpenXDA = createAsyncThunk('OpenXDA/FetchOpenXDA', async (ds ,{ dispatch }) => { + return await GetOpenXDA(ds.dataSourceID, ds.sourceType ?? 'data', ds.table) }); -export const SearchOpenXDA = createAsyncThunk[] }, {}>('OpenXDA/SearchOpenXDA', async (ds, { dispatch }) => { - return await PostOpenXDA(ds.dataSourceID, ds.table, ds.filter) +export const SearchOpenXDA = createAsyncThunk[], sourceType?: 'event' | 'data' }, {}>('OpenXDA/SearchOpenXDA', async (ds, { dispatch }) => { + return await PostOpenXDA(ds.dataSourceID, ds.sourceType ?? 'data', ds.table, ds.filter) +}); + +const getNewTable = () => ({ + Status: 'unitiated' as TrenDAP.Status, + Data: [] as any, + SearchStatus: 'unitiated' as TrenDAP.Status, + SearchData: [] as any }); export const OpenXDASlice = createSlice({ @@ -45,109 +52,87 @@ export const OpenXDASlice = createSlice({ extraReducers: (builder) => { builder.addCase(FetchOpenXDA.fulfilled, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'idle'; - state[action.meta.arg.dataSourceID][action.meta.arg.table].Error = null; + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); + + state[key][action.meta.arg.table].Status = 'idle'; if (typeof (action.payload) === "string") - state[action.meta.arg.dataSourceID][action.meta.arg.table].Data.push(...JSON.parse(action.payload)); + state[key][action.meta.arg.table].Data.push(...JSON.parse(action.payload)); else if (typeof (action.payload) === "object") - state[action.meta.arg.dataSourceID][action.meta.arg.table].Data = action.payload as any[]; + state[key][action.meta.arg.table].Data = action.payload as any[]; }); builder.addCase(FetchOpenXDA.pending, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'loading'; + state[key][action.meta.arg.table].Status = 'loading'; }); builder.addCase(FetchOpenXDA.rejected, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'error'; - state[action.meta.arg.dataSourceID][action.meta.arg.table].Error = action.error.message; + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); + + state[key][action.meta.arg.table].Status = 'error'; + console.error(action.error.message); }); builder.addCase(SearchOpenXDA.fulfilled, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'idle'; - state[action.meta.arg.dataSourceID][action.meta.arg.table].Error = null; + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); + + state[key][action.meta.arg.table].SearchStatus = 'idle'; if (typeof (action.payload) === "string") - state[action.meta.arg.dataSourceID][action.meta.arg.table].Data.push(...JSON.parse(action.payload)); + state[key][action.meta.arg.table].SearchData.push(...JSON.parse(action.payload)); else if (typeof (action.payload) === "object") - state[action.meta.arg.dataSourceID][action.meta.arg.table].Data = action.payload as any[]; + state[key][action.meta.arg.table].SearchData = action.payload as any[]; }); builder.addCase(SearchOpenXDA.pending, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'loading'; + state[key][action.meta.arg.table].SearchStatus = 'loading'; }); builder.addCase(SearchOpenXDA.rejected, (state, action) => { - if (state[action.meta.arg.dataSourceID] === undefined) { - state[action.meta.arg.dataSourceID] = {}; - } + const key = action.meta.arg.dataSourceID + (action.meta.arg.sourceType ?? 'data'); - if (state[action.meta.arg.dataSourceID][action.meta.arg.table] === undefined) { - state[action.meta.arg.dataSourceID][action.meta.arg.table] = { - Status: 'unitiated' as TrenDAP.Status, - Data: [] as any, - Error: null - }; + if (state[key] === undefined) { + state[key] = {}; } - state[action.meta.arg.dataSourceID][action.meta.arg.table].Status = 'error'; - state[action.meta.arg.dataSourceID][action.meta.arg.table].Error = action.error.message; + if (state[key][action.meta.arg.table] === undefined) + state[key][action.meta.arg.table] = getNewTable(); + + state[key][action.meta.arg.table].SearchStatus = 'error'; + console.error(action.error.message); }); } @@ -156,23 +141,25 @@ export const OpenXDASlice = createSlice({ export const { } = OpenXDASlice.actions; export default OpenXDASlice.reducer; -export const SelectOpenXDA = (state: Redux.StoreState, dsid: number, table: string) => (state.OpenXDA[dsid] ? state.OpenXDA[dsid][table].Data : [] ) ; -export const SelectOpenXDAStatus = (state: Redux.StoreState, dsid: number, table: string) => (state.OpenXDA[dsid] ? state.OpenXDA[dsid][table]?.Status ?? 'unitiated' : 'unitiated') as TrenDAP.Status +export const SelectOpenXDA = (state: Redux.StoreState, dsid: number, table: string, type?: 'event' | 'data') => (state.OpenXDA[dsid + (type ?? 'data')] ? state.OpenXDA[dsid + (type ?? 'data')][table].Data : [] ) ; +export const SelectOpenXDAStatus = (state: Redux.StoreState, dsid: number, table: string, type?: 'event' | 'data') => (state.OpenXDA[dsid + (type ?? 'data')] ? state.OpenXDA[dsid + (type ?? 'data')][table]?.Status ?? 'unitiated' : 'unitiated') as TrenDAP.Status +export const SelectSearchOpenXDA = (state: Redux.StoreState, dsid: number, table: string, type?: 'event' | 'data') => (state.OpenXDA[dsid + (type ?? 'data')] ? state.OpenXDA[dsid + (type ?? 'data')][table].SearchData : []); +export const SelectSearchOpenXDAStatus = (state: Redux.StoreState, dsid: number, table: string, type?: 'event' | 'data') => (state.OpenXDA[dsid + (type ?? 'data')] ? state.OpenXDA[dsid + (type ?? 'data')][table]?.SearchStatus ?? 'unitiated' : 'unitiated') as TrenDAP.Status -function GetOpenXDA(dataSourceID: number, table: string): JQuery.jqXHR { +function GetOpenXDA(sourceID: number, type: 'event' | 'data', table: string): JQuery.jqXHR { return $.ajax({ type: "GET", - url: `${homePath}api/TrenDAPDB/${dataSourceID}/${table}`, + url: `${homePath}api/${type === 'data' ? 'TrenDAPDB' : 'OpenXDA'}/${sourceID}/${table}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, async: true }); } -function PostOpenXDA(dataSourceID: number, table: string, filters: Search.IFilter[]): JQuery.jqXHR { +function PostOpenXDA(sourceID: number, type: 'event' | 'data', table: string, filters: Search.IFilter[]): JQuery.jqXHR { return $.ajax({ type: "Post", - url: `${homePath}api/TrenDAPDB/${dataSourceID}/${table}`, + url: `${homePath}api/${type === 'data' ? 'TrenDAPDB' : 'OpenXDA'}/${sourceID}/${table}`, contentType: "application/json; charset=utf-8", dataType: 'json', // Todo: If there is no ID col, this won't work. Every single one does, but this should still be more resilient diff --git a/TrenDAP/wwwroot/TypeScript/Store/Store.ts b/TrenDAP/wwwroot/TypeScript/Store/Store.ts index 58bdcc0f..7da853cf 100644 --- a/TrenDAP/wwwroot/TypeScript/Store/Store.ts +++ b/TrenDAP/wwwroot/TypeScript/Store/Store.ts @@ -23,8 +23,7 @@ import { configureStore } from '@reduxjs/toolkit'; import DataSourcesReducuer from '../Features/DataSources/DataSourcesSlice'; -import DataSourceDataSetReducer from '../Features/DataSources/DataSourceDataSetSlice'; -import DataSourceTypesReducer from '../Features/DataSourceTypes/DataSourceTypesSlice'; +import EventSourcesReducuer from '../Features/EventSources/Slices/EventSourcesSlice'; import WorkSpaceReducer from '../Features/WorkSpaces/WorkSpacesSlice'; import DataSetReducer from '../Features/DataSets/DataSetsSlice'; import OpenXDAReducer from '../Features/OpenXDA/OpenXDASlice'; @@ -34,16 +33,14 @@ import SapphireReducer from '../Features/Sapphire/SapphireSlice'; //Dispatch and Selector Typed export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType - const reducer = { DataSets: DataSetReducer, WorkSpaces: WorkSpaceReducer, DataSources: DataSourcesReducuer, - DataSourceDataSets: DataSourceDataSetReducer, - DataSourceTypes: DataSourceTypesReducer, OpenXDA: OpenXDAReducer, OpenHistorian: OpenHistorianReducer, - Sapphire: SapphireReducer + Sapphire: SapphireReducer, + EventSources: EventSourcesReducuer } const store = configureStore({ reducer }); diff --git a/TrenDAP/wwwroot/TypeScript/TrenDAP.tsx b/TrenDAP/wwwroot/TypeScript/TrenDAP.tsx index 12cf52bd..dade333a 100644 --- a/TrenDAP/wwwroot/TypeScript/TrenDAP.tsx +++ b/TrenDAP/wwwroot/TypeScript/TrenDAP.tsx @@ -34,10 +34,10 @@ import { useAppSelector } from './hooks'; import { SelectWorkSpacesForUser } from './Features/WorkSpaces/WorkSpacesSlice'; const DataSources = React.lazy(() => import(/* webpackChunkName: "DataSources" */ './Features/DataSources/DataSources')); +const ByEventSources = React.lazy(() => import(/* webpackChunkName: "EventSources" */ './Features/EventSources/ByEventSources')); const DataSets = React.lazy(() => import(/* webpackChunkName: "DataSets" */ './Features/DataSets/DataSets')); const WorkSpaces = React.lazy(() => import(/* webpackChunkName: "WorkSpaces" */ './Features/WorkSpaces/WorkSpaces')); const EditDataSet = React.lazy(() => import(/* webpackChunkName: "EditDataSet" */ './Features/DataSets/EditDataSet')); -const AddNewDataSet = React.lazy(() => import(/* webpackChunkName: "AddNewDataSet" */ './Features/DataSets/AddNewDataSet')); const WorkSpaceEditor = React.lazy(() => import(/* webpackChunkName: "WorkSpaceEditor" */ './Features/WorkSpaces/WorkSpaceEditor')); const ViewDataSet = React.lazy(() => import(/* webpackChunkName: "ViewDataSet" */ './Features/DataSets/ViewDataSet/ViewDataSet')); @@ -63,15 +63,15 @@ const TrenDAP: React.FunctionComponent = (props: {}) => { + + + - - - diff --git a/TrenDAP/wwwroot/TypeScript/global.d.ts b/TrenDAP/wwwroot/TypeScript/global.d.ts index ec058adb..27ecdb0e 100644 --- a/TrenDAP/wwwroot/TypeScript/global.d.ts +++ b/TrenDAP/wwwroot/TypeScript/global.d.ts @@ -20,7 +20,7 @@ // Generated original version of source code. // //****************************************************************************************************** -import {OpenXDA, OpenHistorian } from '@gpa-gemstone/application-typings'; +import { OpenXDA, OpenHistorian } from '@gpa-gemstone/application-typings'; export { }; declare module '*.scss'; @@ -39,12 +39,11 @@ export namespace Redux { interface StoreState { DataSets: State, DataSources: State, - DataSourceDataSets: State, - DataSourceTypes: State, + EventSources: State, WorkSpaces: State, OpenHistorian: { ID: number, State: OpenHistorianState }[], Sapphire: { [instance: number]: { [table: string]: Redux.SapphireTableSlice } }, - OpenXDA: { [instance: number]: { [table: string]: Redux.OpenXDATableSlice } }, + OpenXDA: { [instance: string]: { [table: string]: Redux.OpenXDATableSlice } }, } interface State { Status: TrenDAP.Status, @@ -64,8 +63,9 @@ export namespace Redux { interface OpenXDATableSlice { Status: TrenDAP.Status, - Error: string, - Data: any[] + Data: any[], + SearchStatus: TrenDAP.Status, + SearchData: any[] } interface SapphireTableSlice { @@ -87,14 +87,13 @@ export namespace OpenXDAExt { export namespace DataSourceTypes { // The following are how datasources are stored in DB type DataSourceType = 'TrenDAPDB' | 'OpenHistorian' | 'None' | 'Sapphire'; - interface IDataSourceType { ID: number, Name: DataSourceType } interface IDataSourceView { ID: number, Name: string, - DataSourceTypeID: number, + Type: string, URL: string, RegistrationKey: string, - Expires: string | null, + APIToken: string, Public: boolean, User: string, Settings: any @@ -102,7 +101,9 @@ export namespace DataSourceTypes { interface IDataSourceDataSet { ID: number, + DataSourceName: string, DataSourceID: number, + DataSetName: string, DataSetID: number, Settings: any } @@ -113,32 +114,11 @@ export namespace DataSourceTypes { DataSource: IDataSourceView, // Data Set From DB DataSet: TrenDAP.iDataSet, - // Additional Source Settings parsed form source view - DataSourceSettings: T, // Additional DataSet Settings parsed from dataset DataSetSettings: U, SetDataSetSettings: (newDataSetSettings: U) => void, SetErrors: (errors: string[]) => void } - - interface IConfigProps { - Settings: T, - SetSettings: (settings: T) => void, - SetErrors: (errors: string[]) => void - } - - // Datasource coding interface, uses props to get the datasource - interface IDataSource { - DataSetUI: React.FC>, - ConfigUI: React.FC>, - LoadDataSetMeta: (dataSource: IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: IDataSourceDataSet) => Promise, - LoadDataSet: (dataSource: IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: IDataSourceDataSet) => Promise, - QuickViewDataSet?: (dataSource: IDataSourceView, dataSet: TrenDAP.iDataSet, dataConn: IDataSourceDataSet) => string, - TestAuth: (dataSource: IDataSourceView) => Promise, - DefaultSourceSettings: T, - DefaultDataSetSettings: U, - Name: string, - } } export namespace DataSetTypes { @@ -161,7 +141,7 @@ export namespace DataSetTypes { } } -export namespace TrenDAP{ +export namespace TrenDAP { type Status = 'loading' | 'idle' | 'error' | 'changed' | 'unitiated'; type WidgetType = 'Histogram' | 'Profile' | 'Stats' | 'Table' | 'Text' | 'Trend' | 'XvsY'; type WidgetClass = iHistogram | iTrend | iProfile | iStats | iTable | iText | iXvsY; @@ -173,18 +153,33 @@ export namespace TrenDAP{ type TemplateBy = 'Meter' | 'Asset' | 'Device'; type iTrendDataPoint = iXDATrendDataPoint | iOpenHistorianAggregationPoint | iSapphireTrendDataPoint; // TrenDAP - interface iWorkSpace { ID: number, Type: WorkSpaceType, Name: string, User: string, DataSetID: number, JSON: string, JSONString: string, Public: boolean, UpdatedOn: string, Open: boolean } - interface iDataSet { ID: number, Name: string, Context: 'Relative' | 'Fixed Dates', RelativeValue: number, RelativeWindow: 'Day' | 'Week' | 'Month' | 'Year',From: string, To: string, Hours: number, Days: number, Weeks: number, Months: number, User: string, Public: boolean, UpdatedOn: string, Data?: { Status: Status, Error?: string } } + interface iWorkSpace { ID: number, Type: WorkSpaceType, Name: string, User: string, DataSetID: number, JSON: string, JSONString: string, Public: boolean, UpdatedOn: string, Open: boolean } + interface iDataSet { ID: number, Name: string, Context: 'Relative' | 'Fixed Dates', RelativeValue: number, RelativeWindow: 'Day' | 'Week' | 'Month' | 'Year', From: string, To: string, Hours: number, Days: number, Weeks: number, Months: number, User: string, Public: boolean, UpdatedOn: string, Data?: { Status: Status, Error?: string } } interface iDataSetSource { ID: number, Name: string, DataSourceTypeID: number, JSON: object } - interface iDataSetReturn { Data: T[], DataSource: { ID: number, Name: string, Type: DataSourceType, OpenSEE?: string}, From: string, To: string } + interface iDataSetReturn { Data: T[], DataSource: { ID: number, Name: string, Type: DataSourceType, OpenSEE?: string }, From: string, To: string } + + // Sources + interface ISourceConfig { + Settings: T, + SetSettings: (settings: T) => void, + SetErrors: (errors: string[]) => void + } + + // Events + interface IEvent { + Title: string, + Time: number, + Duration: number, + Description: string + } // XDA interface iXDADataSet { By: 'Asset' | 'Meter', IDs: number[], Phases: number[], Groups: number[], ChannelIDs: number[], Aggregate: '' | '1h' | '1d' | '1w' } interface iXDADataSource { PQBrowserUrl: string } interface iXDAReturn { ID: number, Meter: string, Name: string, Station: string, Phase: OpenXDA.Types.PhaseName, Type: OpenXDA.Types.MeasurementTypeName, Harmonic: number, Latitude: number, Longitude: number, Asset: string, Characteristic: OpenXDA.Types.MeasurementCharacteristicName, Unit: string } interface iXDAReturnWithDataSource extends iXDAReturnData { DataSourceID: number, DataSource: string } - interface iXDAReturnData extends iXDAReturn { Data: iXDATrendDataPoint[], Events: {ID: number, ChannelID: number, StartTime: string}[] } - interface iXDATrendDataPoint { Tag: string, Minimum: number, Maximum: number, Average: number, Timestamp: string, QualityFlags: number} + interface iXDAReturnData extends iXDAReturn { Data: iXDATrendDataPoint[], Events: { ID: number, ChannelID: number, StartTime: string }[] } + interface iXDATrendDataPoint { Tag: string, Minimum: number, Maximum: number, Average: number, Timestamp: string, QualityFlags: number } type iXDATrendDataPointField = 'Minimum' | 'Maximum' | 'Average'; // openHistorian @@ -193,16 +188,16 @@ export namespace TrenDAP{ interface iOpenHistorianAggregationPoint extends iXDATrendDataPoint { } // Sapphire - interface iSapphireDataSet { IDs: number[], Phases: number[], Types: number[], Aggregate: string, Harmonics: string} + interface iSapphireDataSet { IDs: number[], Phases: number[], Types: number[], Aggregate: string, Harmonics: string } interface iSapphireReturn { ID: number, Meter: string, Name: string, Station: string, Phase: string, Type: string, Harmonic: number, Latitude: number, Longitude: number, Asset: string, Characteristic: string, Unit: string } interface iSapphireReturnWithDataSource extends iSapphireReturnData { DataSourceID: number, DataSource: string } interface iSapphireReturnData extends iSapphireReturn { Data: iSapphireTrendDataPoint[], Events: { ID: number, ChannelID: number, StartTime: string }[] } interface iSapphireTrendDataPoint extends iXDATrendDataPoint { } - type iSapphireTrendDataPointField = iXDATrendDataPointField ; + type iSapphireTrendDataPointField = iXDATrendDataPointField; // Widget JSON interfaces interface WorkSpaceJSON { Rows: iRow[] | iTemplatableRow[], By?: TemplateBy, Type?: DataSourceType } - interface WorkSpaceJSONTrenDAPDB extends WorkSpaceJSON { By: 'Meter' | 'Asset' , Type: 'TrenDAPDB'} + interface WorkSpaceJSONTrenDAPDB extends WorkSpaceJSON { By: 'Meter' | 'Asset', Type: 'TrenDAPDB' } interface WorkSpaceJSONOpenHistorian extends WorkSpaceJSON { By: 'Device', Type: 'OpenHistorian' } // Workspace @@ -210,22 +205,22 @@ export namespace TrenDAP{ interface iTemplatableRow extends iRow { By: TrenDAP.TemplateBy, Device: string, Widgets: iTemplatableWidget[] } // Generic Widget - interface iWidget { WorkSpace?: iWorkSpace, Data?: iDataSetReturn[], Height: number, Width: number, Type: WidgetType, Label: string, JSON: T, Update?: (widget: iWidget) => void, Remove?: () => void, AddSeries?: (id: number, dataSourceID: number, label?: string) => void } + interface iWidget { WorkSpace?: iWorkSpace, Data?: iDataSetReturn[], Height: number, Width: number, Type: WidgetType, Label: string, JSON: T, Update?: (widget: iWidget) => void, Remove?: () => void, AddSeries?: (id: number, dataSourceID: number, label?: string) => void } interface iTemplatableWidget { WorkSpace?: iWorkSpace, By: TemplateBy, Device: string, Data?: iDataSetReturn[], Height: number, Width: number, Type: WidgetType, Label: string, JSON: T, Update?: (widget: iTemplatableWidget) => void, Remove?: () => void } interface iSeries { DataSourceID: number, ID: string, Field: iXDATrendDataPointField } interface iTemplateSeries { DataSourceID: number, Field: iXDATrendDataPointField } interface iTemplateSeriesXDA extends iTemplateSeries { Phase: OpenXDA.Types.PhaseName, Characteristic: OpenXDA.Types.MeasurementCharacteristicName, Type: OpenXDA.Types.MeasurementTypeName } - interface iTemplateSeriesSapphire extends iTemplateSeries { Phase: string, Measurement: string, Harmonic: number} + interface iTemplateSeriesSapphire extends iTemplateSeries { Phase: string, Measurement: string, Harmonic: number } interface iTemplateSeriesOpenHistorian extends iTemplateSeries { Phase: OpenHistorian.Types.Phase, Type: OpenHistorian.Types.SignalType } interface iAxis { Min: number, Max: number, Units: string } interface iYAxis extends iAxis { Position: 'left' | 'right' } // Histogram Specific - interface iHistogram { Min: number, Max: number, Units: string, BinCount: number, Series: iHistogramSeries[] } + interface iHistogram { Min: number, Max: number, Units: string, BinCount: number, Series: iHistogramSeries[] } interface iHistogramSeries extends iSeries { Color: string, Profile: boolean, ProfileColor: string } - interface iTemplatableHistogram { Min: number, Max: number, Units: string, BinCount: number, Series: iTemplatableHistogramSeriesXDA[] | iTemplatableHistogramSeriesOpenHistorian[] | iTemplatableHistogramSeriesSapphire[]} + interface iTemplatableHistogram { Min: number, Max: number, Units: string, BinCount: number, Series: iTemplatableHistogramSeriesXDA[] | iTemplatableHistogramSeriesOpenHistorian[] | iTemplatableHistogramSeriesSapphire[] } interface iTemplatableHistogramSeries extends iTemplateSeries { Color: string, Profile: boolean, ProfileColor: string } interface iTemplatableHistogramSeriesSapphire extends iTemplatableHistogramSeries { Phase: string, Measurement: string, Harmonic: number } interface iTemplatableHistogramSeriesXDA extends iTemplatableHistogramSeries { Phase: OpenXDA.Types.PhaseName, Characteristic: OpenXDA.Types.MeasurementCharacteristicName, Type: OpenXDA.Types.MeasurementTypeName } @@ -244,7 +239,7 @@ export namespace TrenDAP{ interface iTemplatableTable { Series: iTemplateSeriesXDA | iTemplateSeriesOpenHistorian | iTemplateSeriesSapphire, Precision: number } // Text - interface iText {Text: string } + interface iText { Text: string } interface iTemplatableText { Text: string } // Trend