From c68a44bbfba643b0e963924a9c902d6093416758 Mon Sep 17 00:00:00 2001 From: Vadym <30234255+vadkor@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:01:18 +1100 Subject: [PATCH 1/2] Added AEMO Status & Outages --- src/components/Page.js | 5 + .../data/discovery/AEMODiscoveryInfo.js | 128 ++++++++++++++++++ src/store/aemo_discovery/actions.js | 70 ++++++++++ src/store/aemo_discovery/index.js | 2 + src/store/aemo_discovery/reducer.js | 28 ++++ src/store/index.js | 2 + 6 files changed, 235 insertions(+) create mode 100644 src/components/data/discovery/AEMODiscoveryInfo.js create mode 100644 src/store/aemo_discovery/actions.js create mode 100644 src/store/aemo_discovery/index.js create mode 100644 src/store/aemo_discovery/reducer.js diff --git a/src/components/Page.js b/src/components/Page.js index 33c20ba..52e2ae4 100644 --- a/src/components/Page.js +++ b/src/components/Page.js @@ -12,6 +12,7 @@ import AppBar from '@material-ui/core/AppBar' import Tabs from '@material-ui/core/Tabs' import Tab from '@material-ui/core/Tab' import DiscoveryInfo from './data/discovery/DiscoveryInfo' +import AEMODiscoveryInfo from './data/discovery/AEMODiscoveryInfo' const useStyles = makeStyles(theme => ({ hidden: { @@ -37,6 +38,7 @@ function Page() { +
@@ -50,6 +52,9 @@ function Page() {
+
+ +
); } diff --git a/src/components/data/discovery/AEMODiscoveryInfo.js b/src/components/data/discovery/AEMODiscoveryInfo.js new file mode 100644 index 0000000..b4ac302 --- /dev/null +++ b/src/components/data/discovery/AEMODiscoveryInfo.js @@ -0,0 +1,128 @@ +import React from 'react' +import Accordion from '@material-ui/core/Accordion' +import AccordionSummary from '@material-ui/core/AccordionSummary' +import AccordionActions from '@material-ui/core/AccordionActions' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import SubjectIcon from '@material-ui/icons/Subject' +import Typography from '@material-ui/core/Typography' +import Divider from '@material-ui/core/Divider' +import RefreshIcon from '@material-ui/icons/Refresh' +import Fab from '@material-ui/core/Fab' +import Tooltip from '@material-ui/core/Tooltip' +import { makeStyles } from '@material-ui/core/styles' +import { fade } from '@material-ui/core/styles/colorManipulator' +import Duration from '../Duration' +import DateTime from '../DateTime' +import { connect } from 'react-redux' +import { retrieveStatus, retrieveOutages } from '../../../store/aemo_discovery' +import { translateDiscoveryStatus } from '../../../utils/dict' + +const useStyles = makeStyles(theme => ({ + container: { + marginLeft: theme.typography.pxToRem(20), + marginRight: theme.typography.pxToRem(20) + }, + panel: { + backgroundColor: fade('#fff', 0.9) + }, + heading: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + fontSize: theme.typography.pxToRem(20), + }, + details: { + maxWidth:'95%', + marginLeft: 'auto', + marginRight: 'auto', + marginBottom: 20 + } +})) + +const AEMODiscoveryInfo = (props) => { + + const classes = useStyles() + const [expanded, setExpanded] = React.useState(true) + const {statusDetails, outagesDetails} = props.data + + const toggleExpansion = (event, newExpanded) => { + setExpanded(newExpanded) + } + + const refreshStatusOutages = () => { + props.retrieveStatus() + props.retrieveOutages() + } + + React.useEffect(() => { + refreshStatusOutages() + // eslint-disable-next-line + }, []) + + return ( + + } + aria-controls='panel1c-content' + > +
+ Status & Outages +
+
+
+

Secondary Data Holders provide data to Data Holders via CDR requests, who in turn provide the data to ADRs. Such data is called Shared Responsibility Data (SR data).

+

Currently, only the energy sector has a designated secondary data holder: AEMO. This page lists the status and outages for AEMO.

+ +
AEMO
+ {!!statusDetails && + <> +

Status

+
+
Status: {translateDiscoveryStatus(statusDetails.status)}{!!statusDetails.explanation && - {statusDetails.explanation}}
+ {!!statusDetails.detectionTime &&
Detection Time:
} + {!!statusDetails.expectedResolutionTime &&
Expected Resolution Time:
} + {!!statusDetails.updateTime &&
Update Time:
} +
+ + } + + {!!outagesDetails && !!outagesDetails.outages && !!outagesDetails.outages.length && + <> +

Scheduled Outages

+
    + {outagesDetails.outages.map((outage, index) => +
  • +
    Outage Time:
    + {!!outage.duration &&
    Planned Duration:
    } + {!!outage.isPartial &&
    Partial: {outage.isPartial}
    } +
    «{outage.explanation}»
    +
  • + )} +
+ + } +
+ + + + + + + + + + +
+ ) +} + +const mapStateToProps = state => ({ + data: state.aemoDiscovery +}) + +const mapDispatchToProps = { + retrieveStatus, + retrieveOutages +} + +export default connect(mapStateToProps, mapDispatchToProps)(AEMODiscoveryInfo) \ No newline at end of file diff --git a/src/store/aemo_discovery/actions.js b/src/store/aemo_discovery/actions.js new file mode 100644 index 0000000..f69a03e --- /dev/null +++ b/src/store/aemo_discovery/actions.js @@ -0,0 +1,70 @@ +import {conoutInfo, conoutHtmlError, conoutError} from '../conout/actions' + +const AEMO_URL = 'https://api.aemo.com.au/NEMRetail/cds-au/v1' + +export const RETRIEVE_AEMO_STATUS = 'RETRIEVE_AEMO_STATUS' +export const RETRIEVE_AEMO_OUTAGES = 'RETRIEVE_AEMO_OUTAGES' + +const headers = { + 'Accept': 'application/json', + 'x-v': 1 +} + +function createConoutError(error, url) { + return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? + ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) +} + +export const retrieveStatus = () => dispatch => { + const fullUrl = AEMO_URL + '/discovery/status' + const request = new Request(fullUrl, {headers}) + dispatch(conoutInfo('Requesting retrieveStatus(): ' + fullUrl)) + dispatch({ + type: RETRIEVE_AEMO_STATUS, + payload: fetch(request) + .then(response => { + if (response.ok) { + return response.json() + } + throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) + }) + .then(obj => { + dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj)) + return obj + }) + .catch(error => { + dispatch(createConoutError(error, fullUrl)) + }) + }) +} + +export const retrieveOutages = () => dispatch => { + const fullUrl = AEMO_URL + '/discovery/outages' + const request = new Request(fullUrl, {headers}) + dispatch(conoutInfo('Requesting retrieveOutages(): ' + fullUrl)) + dispatch({ + type: RETRIEVE_AEMO_OUTAGES, + payload: fetch(request) + .then(response => { + if (response.ok) { + if (!response.headers['x-v']) { + const msg = `Response for ${fullUrl}: doesn't expose header x-v: possibly caused by incomplete ` + const corsSupport = 'CORS support' + dispatch(conoutHtmlError( + msg + corsSupport, + `${msg}${corsSupport}` + )) + } + return response.json() + } + throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) + }) + .then(obj => { + dispatch(conoutInfo(`Received response for ${fullUrl}:`, obj)) + return obj + }) + .catch(error => { + dispatch(createConoutError(error, fullUrl)) + }) + }) +} diff --git a/src/store/aemo_discovery/index.js b/src/store/aemo_discovery/index.js new file mode 100644 index 0000000..0a77bab --- /dev/null +++ b/src/store/aemo_discovery/index.js @@ -0,0 +1,2 @@ +export * from './actions' +export * from './reducer' diff --git a/src/store/aemo_discovery/reducer.js b/src/store/aemo_discovery/reducer.js new file mode 100644 index 0000000..c1c1d86 --- /dev/null +++ b/src/store/aemo_discovery/reducer.js @@ -0,0 +1,28 @@ +import { + RETRIEVE_AEMO_STATUS, + RETRIEVE_AEMO_OUTAGES +} from './actions' +import {fulfilled} from '../../utils/async-actions' + +export default function discovery(state = {}, action) { + switch (action.type) { + case fulfilled(RETRIEVE_AEMO_STATUS): { + const s = {...state} + if (action.payload) { + const response = action.payload + s.statusDetails = response ? response.data : null + } + return s + } + case fulfilled(RETRIEVE_AEMO_OUTAGES): { + const s = {...state} + if (action.payload) { + const response = action.payload + s.outagesDetails = response ? response.data : null + } + return s + } + default: + return state + } +} diff --git a/src/store/index.js b/src/store/index.js index 0572799..a553bf6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -9,6 +9,7 @@ import banking from './banking/data/reducer' import energy from './energy/data/reducer' import energySelection from './energy/selection/reducer' import discovery from './discovery/reducer' +import aemoDiscovery from './aemo_discovery/reducer' import bankingSelection from './banking/selection/reducer' import bankingComparison from './banking/comparison/reducer' import energyComparison from './energy/comparison/reducer' @@ -20,6 +21,7 @@ const rootReducer = combineReducers({ banking, energy, discovery, + aemoDiscovery, bankingSelection, energySelection, bankingComparison, From 3024265a1c21bd1e811d49a8d449ed45356d3944 Mon Sep 17 00:00:00 2001 From: Vadym <30234255+vadkor@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:02:17 +1100 Subject: [PATCH 2/2] Generalised CORS messaging, reused StatusOutages --- .../data/discovery/AEMODiscoveryInfo.js | 32 ++----------------- .../data/discovery/DiscoveryInfo.js | 24 ++++++++++---- .../data/discovery/StatusOutages.js | 19 +---------- src/store/aemo_discovery/actions.js | 18 +++-------- src/store/banking/data/actions.js | 17 ++-------- src/store/discovery/actions.js | 18 +++-------- src/store/energy/data/actions.js | 17 ++-------- src/utils/cors.js | 17 ++++++++++ 8 files changed, 52 insertions(+), 110 deletions(-) create mode 100644 src/utils/cors.js diff --git a/src/components/data/discovery/AEMODiscoveryInfo.js b/src/components/data/discovery/AEMODiscoveryInfo.js index b4ac302..2717e25 100644 --- a/src/components/data/discovery/AEMODiscoveryInfo.js +++ b/src/components/data/discovery/AEMODiscoveryInfo.js @@ -11,11 +11,9 @@ import Fab from '@material-ui/core/Fab' import Tooltip from '@material-ui/core/Tooltip' import { makeStyles } from '@material-ui/core/styles' import { fade } from '@material-ui/core/styles/colorManipulator' -import Duration from '../Duration' -import DateTime from '../DateTime' +import StatusOutages from './StatusOutages' import { connect } from 'react-redux' import { retrieveStatus, retrieveOutages } from '../../../store/aemo_discovery' -import { translateDiscoveryStatus } from '../../../utils/dict' const useStyles = makeStyles(theme => ({ container: { @@ -74,33 +72,7 @@ const AEMODiscoveryInfo = (props) => {

Currently, only the energy sector has a designated secondary data holder: AEMO. This page lists the status and outages for AEMO.

AEMO
- {!!statusDetails && - <> -

Status

-
-
Status: {translateDiscoveryStatus(statusDetails.status)}{!!statusDetails.explanation && - {statusDetails.explanation}}
- {!!statusDetails.detectionTime &&
Detection Time:
} - {!!statusDetails.expectedResolutionTime &&
Expected Resolution Time:
} - {!!statusDetails.updateTime &&
Update Time:
} -
- - } - - {!!outagesDetails && !!outagesDetails.outages && !!outagesDetails.outages.length && - <> -

Scheduled Outages

-
    - {outagesDetails.outages.map((outage, index) => -
  • -
    Outage Time:
    - {!!outage.duration &&
    Planned Duration:
    } - {!!outage.isPartial &&
    Partial: {outage.isPartial}
    } -
    «{outage.explanation}»
    -
  • - )} -
- - } +
diff --git a/src/components/data/discovery/DiscoveryInfo.js b/src/components/data/discovery/DiscoveryInfo.js index 55369b5..3651193 100644 --- a/src/components/data/discovery/DiscoveryInfo.js +++ b/src/components/data/discovery/DiscoveryInfo.js @@ -49,6 +49,11 @@ const DiscoveryInfo = (props) => { setExpanded(newExpanded) } + React.useEffect(() => { + refreshStatusOutages() + // eslint-disable-next-line + }, [props.dataSources]) + const getWidth = (dataSourceCount, min) => { return Math.max(12 / dataSourceCount, min) } @@ -78,8 +83,13 @@ const DiscoveryInfo = (props) => { { savedDataSourcesCount > 0 && - {dataSources.map((dataSource, index) => ( - !dataSource.unsaved && dataSource.enabled && !dataSource.deleted && + {dataSources.map((dataSource, index) => { + const data = props.data[index] + if (!data) { + return false + } + const {statusDetails, outagesDetails} = data + return (!dataSource.unsaved && dataSource.enabled && !dataSource.deleted && { xl={getWidth(savedDataSourcesCount, 3)} >
{!!dataSource.icon && }

{dataSource.name}

- +
- ))} + ) + })}
} @@ -108,10 +119,11 @@ const DiscoveryInfo = (props) => { ) } -const mapStateToProps = state=>({ +const mapStateToProps = state => ({ dataSources : state.dataSources, savedDataSourcesCount: state.dataSources.filter(dataSource => !dataSource.unsaved && !dataSource.deleted && dataSource.enabled).length, - versionInfo: state.versionInfo.vHeaders + versionInfo: state.versionInfo.vHeaders, + data: state.discovery }) const mapDispatchToProps = { diff --git a/src/components/data/discovery/StatusOutages.js b/src/components/data/discovery/StatusOutages.js index 8154624..76c13ef 100644 --- a/src/components/data/discovery/StatusOutages.js +++ b/src/components/data/discovery/StatusOutages.js @@ -2,25 +2,12 @@ import React from 'react' import DateTime from '../DateTime' import Duration from '../Duration' import { connect } from 'react-redux' -import { normalise } from '../../../utils/url' -import { retrieveStatus, retrieveOutages } from '../../../store/discovery' import { translateDiscoveryStatus } from '../../../utils/dict' class StatusOutages extends React.Component { - componentDidMount() { - const { dataSourceIndex, dataSource, versionInfo } = this.props - const url = normalise(dataSource.url) - this.props.retrieveStatus(dataSourceIndex, url, versionInfo.xV, versionInfo.xMinV) - this.props.retrieveOutages(dataSourceIndex, url, versionInfo.xV, versionInfo.xMinV) - } - render() { - let data = this.props.data[this.props.dataSourceIndex] - if (!data) { - return false - } - const {statusDetails, outagesDetails} = data + const { statusDetails, outagesDetails } = this.props return ( <> {!!statusDetails && @@ -63,13 +50,9 @@ const Outage = props => { } const mapStateToProps = state => ({ - versionInfo: state.versionInfo.vHeaders, - data: state.discovery }) const mapDispatchToProps = { - retrieveStatus, - retrieveOutages } export default connect(mapStateToProps, mapDispatchToProps)(StatusOutages) \ No newline at end of file diff --git a/src/store/aemo_discovery/actions.js b/src/store/aemo_discovery/actions.js index f69a03e..ad010e6 100644 --- a/src/store/aemo_discovery/actions.js +++ b/src/store/aemo_discovery/actions.js @@ -1,4 +1,5 @@ -import {conoutInfo, conoutHtmlError, conoutError} from '../conout/actions' +import {conoutInfo} from '../conout/actions' +import {createConoutError, checkExposedHeaders} from '../../utils/cors' const AEMO_URL = 'https://api.aemo.com.au/NEMRetail/cds-au/v1' @@ -10,11 +11,6 @@ const headers = { 'x-v': 1 } -function createConoutError(error, url) { - return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? - ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) -} - export const retrieveStatus = () => dispatch => { const fullUrl = AEMO_URL + '/discovery/status' const request = new Request(fullUrl, {headers}) @@ -24,6 +20,7 @@ export const retrieveStatus = () => dispatch => { payload: fetch(request) .then(response => { if (response.ok) { + checkExposedHeaders(response, fullUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) @@ -47,14 +44,7 @@ export const retrieveOutages = () => dispatch => { payload: fetch(request) .then(response => { if (response.ok) { - if (!response.headers['x-v']) { - const msg = `Response for ${fullUrl}: doesn't expose header x-v: possibly caused by incomplete ` - const corsSupport = 'CORS support' - dispatch(conoutHtmlError( - msg + corsSupport, - `${msg}${corsSupport}` - )) - } + checkExposedHeaders(response, fullUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) diff --git a/src/store/banking/data/actions.js b/src/store/banking/data/actions.js index 787660b..a73f560 100644 --- a/src/store/banking/data/actions.js +++ b/src/store/banking/data/actions.js @@ -1,4 +1,5 @@ -import {conoutInfo, conoutError, conoutHtmlError, conoutWarn} from '../../conout/actions' +import {conoutInfo, conoutError, conoutWarn} from '../../conout/actions' +import {createConoutError, checkExposedHeaders} from '../../../utils/cors' import {encodeRFC3986URIComponent} from '../../../utils/url' export const START_RETRIEVE_PRODUCT_LIST = 'START_RETRIEVE_PRODUCT_LIST' @@ -17,11 +18,6 @@ const headers = { 'Accept': 'application/json' } -function createConoutError(error, url) { - return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? - ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) -} - export const retrieveProductList = (dataSourceIdx, baseUrl, productListUrl, xV, xMinV) => (dispatch) => { const request = new Request(productListUrl, {headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})}) @@ -31,14 +27,7 @@ export const retrieveProductList = (dataSourceIdx, baseUrl, productListUrl, xV, payload: fetch(request) .then(response => { if (response.ok) { - if (!response.headers['x-v']) { - const msg = `Response for ${productListUrl}: doesn't expose header x-v: possibly caused by incomplete ` - const corsSupport = 'CORS support' - dispatch(conoutHtmlError( - msg + corsSupport, - `${msg}${corsSupport}` - )) - } + checkExposedHeaders(response, productListUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) diff --git a/src/store/discovery/actions.js b/src/store/discovery/actions.js index 545d424..1971132 100644 --- a/src/store/discovery/actions.js +++ b/src/store/discovery/actions.js @@ -1,4 +1,5 @@ -import {conoutInfo, conoutHtmlError, conoutError} from '../conout/actions' +import {conoutInfo} from '../conout/actions' +import {createConoutError, checkExposedHeaders} from '../../utils/cors' export const RETRIEVE_STATUS = 'RETRIEVE_STATUS' export const RETRIEVE_OUTAGES = 'RETRIEVE_OUTAGES' @@ -7,11 +8,6 @@ const headers = { 'Accept': 'application/json' } -function createConoutError(error, url) { - return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? - ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) -} - export const retrieveStatus = (dataSourceIdx, url, xV, xMinV) => dispatch => { const fullUrl = url + '/discovery/status' const request = new Request(fullUrl, { @@ -23,6 +19,7 @@ export const retrieveStatus = (dataSourceIdx, url, xV, xMinV) => dispatch => { payload: fetch(request) .then(response => { if (response.ok) { + checkExposedHeaders(response, fullUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) @@ -48,14 +45,7 @@ export const retrieveOutages = (dataSourceIdx, url, xV, xMinV) => dispatch => { payload: fetch(request) .then(response => { if (response.ok) { - if (!response.headers['x-v']) { - const msg = `Response for ${fullUrl}: doesn't expose header x-v: possibly caused by incomplete ` - const corsSupport = 'CORS support' - dispatch(conoutHtmlError( - msg + corsSupport, - `${msg}${corsSupport}` - )) - } + checkExposedHeaders(response, fullUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) diff --git a/src/store/energy/data/actions.js b/src/store/energy/data/actions.js index f158c0a..797a99c 100644 --- a/src/store/energy/data/actions.js +++ b/src/store/energy/data/actions.js @@ -1,4 +1,5 @@ -import {conoutInfo, conoutHtmlError, conoutError} from '../../conout/actions' +import {conoutInfo, conoutError} from '../../conout/actions' +import {createConoutError, checkExposedHeaders} from '../../../utils/cors' import {encodeRFC3986URIComponent} from '../../../utils/url' export const START_RETRIEVE_PLAN_LIST = 'START_RETRIEVE_PLAN_LIST' @@ -16,11 +17,6 @@ const headers = { 'Accept': 'application/json' } -function createConoutError(error, url) { - return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? - ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) -} - export const retrievePlanList = (dataSourceIdx, baseUrl, planListUrl, xV, xMinV, effective, fuelType) => (dispatch) => { const request = new Request(planListUrl, {headers: new Headers({...headers, 'x-v': xV, 'x-min-v': xMinV})}) @@ -30,14 +26,7 @@ export const retrievePlanList = (dataSourceIdx, baseUrl, planListUrl, xV, xMinV, payload: fetch(request) .then(response => { if (response.ok) { - if (!response.headers['x-v']) { - const msg = `Response for ${planListUrl}: doesn't expose header x-v: possibly caused by incomplete ` - const corsSupport = 'CORS support' - dispatch(conoutHtmlError( - msg + corsSupport, - `${msg}${corsSupport}` - )) - } + checkExposedHeaders(response, planListUrl, dispatch) return response.json() } throw new Error(`Response not OK. Status: ${response.status} (${response.statusText})`) diff --git a/src/utils/cors.js b/src/utils/cors.js new file mode 100644 index 0000000..58a6a0a --- /dev/null +++ b/src/utils/cors.js @@ -0,0 +1,17 @@ +import {conoutHtmlError, conoutError} from '../store/conout/actions' + +export function createConoutError(error, url) { + return conoutError('Caught ' + error + ' while requesting ' + url + (error.name === 'TypeError' ? + ' Possibly caused by the endpoint not supporting Cross-Origin Requests (CORS)' : '')) +} + +export function checkExposedHeaders(response, fullUrl, dispatch) { + if (!response.headers['x-v']) { + const msg = `Response for ${fullUrl}: doesn't expose header x-v: possibly caused by incomplete ` + const corsSupport = 'CORS support' + dispatch(conoutHtmlError( + msg + corsSupport, + `${msg}${corsSupport}` + )) + } +}