@@ -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)} />
+ >
);
}
-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
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ 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 (
);
}
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 (
+
+ );
+}
+
+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} 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} />
+
+
+
+
+
+ );
+
+ },
+ 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