diff --git a/.gitattributes b/.gitattributes index 7973fabe8edf96baab4547c0d274c2a01735efba..803896696012fed33d039cf2bf56830c365ef839 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1850,6 +1850,9 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.test.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/api_configuration.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaErrorDetails.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaView.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/FillHeight.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.scss -text @@ -1875,6 +1878,9 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.css -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.scss -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.scss -text @@ -1888,10 +1894,12 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/RTSMPage.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationTestPage.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/antennaOverviewPageActions.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/appInitDataActions.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/landingPageActions.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/mainFiltersActions.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/stationOverviewPageActions.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/antennaOverviewPageReducers.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/appInitDataReducers.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/index.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/landingPageReducers.js -text diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json b/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json index bce306cf061e0de8f8ccc62e0ae452dc73d1b6ba..a4b265d0ed88957d46673e0264bf645c02271674 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json @@ -39,11 +39,12 @@ "react-sticky": "^6.0.3", "react-table": "^6.8.6", "react-table-container": "^2.0.1", + "react-treeview": "^0.4.7", "react-vega-lite": "^2.0.2", "reactstrap": "^6.3.1", "redux": "^4.0.1", "redux-thunk": "^2.3.0", - "vega": "^4.3.0", + "vega": "^4.4.0", "vega-lite": "^2.6.0", "vega-tooltip": "^0.13.0" }, diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js index 584a61eb5158a326e8e55743d1afc0d76cf1855d..5c4c238b5b2a2972d80a129fb1112b7519637c69 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js @@ -33,7 +33,7 @@ class AppC extends Component { <Switch> <Route exact path="/" component={LandingPage}/> <Route path="/station_overview/:name?" component={StationOverviewPage}/> - <Route exact path="/tiles" component={TilesPage}/> + <Route exact path="/tiles/:antenna?" component={TilesPage}/> <Route exact path="/details" component={DetailsPage}/> </Switch> </div> diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaErrorDetails.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaErrorDetails.js new file mode 100644 index 0000000000000000000000000000000000000000..c0766214a0e77550525a9f0c3d37b1c9cde89ca5 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaErrorDetails.js @@ -0,0 +1,248 @@ +import React, { + Component +} from 'react'; +import TreeView from 'react-treeview'; +import 'react-treeview/react-treeview.css'; +import moment from 'moment'; +import { + Button, + Modal, + ModalHeader, + ModalBody, + Card, + CardSubtitle, + CardBody, + Container, + Row, + Col +} from 'reactstrap'; +import { IoMdCloseCircleOutline as CloseIcon} from 'react-icons/io'; +import {pinAntennaError, selectAntennaError} from '../redux/actions/antennaOverviewPageActions'; +import {LOFARTESTS} from '../utils/LOFARDefinitions'; +import {datetime_format} from '../utils/constants'; +import {connect} from "react-redux"; + + +class ModalPicture extends Component { + + state = { + modal: false, + modalUrl: '' + }; + + toggleModal = (e) => { + this.setState({ + modal: !this.state.modal, + modalUrl: e.currentTarget.src + }); + }; + + onImgError = (e) => { + const img = e.currentTarget; + img.alt = 'Reloading in 2 sec..'; + // let the window figure out if the timeout id is still valid + if(this.timeout){ + clearTimeout(this.timeout); + } + this.timeout = setTimeout(() => { img.src = img.src; }, 2000 ); + }; + + clearTimeouts() { + clearTimeout(this.timeout); + this.timeout = null; + } + + render(){ + this.clearTimeouts(); + return ( + <React.Fragment> + <img src={this.props.url} onClick={this.toggleModal} onError={this.onImgError} title="Click to enlarge" alt="Not present"/> + <Modal isOpen={this.state.modal} fade={false} size="lg" toggle={this.toggle} className={this.props.className}> + <ModalHeader toggle={this.toggleModal}></ModalHeader> + <ModalBody> + <img style={{width: '100%'}} src={this.state.modalUrl} alt="Not present" /> + </ModalBody> + </Modal> + </React.Fragment> + ); + } +} + +class AntennaErrorDetailsC extends Component { + + composeTitle(){ + const test_type = LOFARTESTS[this.props.test_type]; + let title = "Error details"; + if(this.props.test_type !== undefined){ + title = `${test_type}: ${this.props.error_type} Antenna: ${this.props.antennaId} Date: ${moment(this.props.start_date).format(datetime_format)}`; + if(this.props.element_id) title += `\t Element: ${this.props.element_id}`; + } + return title + } + + static renderDetails(details){ + if(details === undefined) return; + + const details_lines = []; + if(details === Object(details)){ + for(let entity_name of Object.keys(details)){ + if(details.hasOwnProperty(entity_name)){ + const element = details[entity_name]; + const children = this.renderDetails(details[entity_name]); + + if(element === Object(element)){ + details_lines.push(<TreeView key={entity_name} nodeLabel={entity_name}>{children}</TreeView>) + }else{ + details_lines.push(<div key={entity_name}> {entity_name}: <em>{children}</em></div>) + } + } + } + return ( + <React.Fragment> + {details_lines.length? details_lines: undefined} + </React.Fragment> + ); + }else{ + return ( + <React.Fragment> + {details} + </React.Fragment> + ); + } + } + + unpinPanel = () => { + this.props.pinAntennaError(!this.props.isPinned); + this.props.selectAntennaError({}); + }; + + renderElementErrors(){ + if(this.props.content.element_errors === undefined) return; + + const element_errors = this.props.content.element_errors; + const rendered_element_errors = []; + for(let element_id of Object.keys(element_errors)){ + const element_error = ( + <TreeView key={element_id} nodeLabel={element_id}>{AntennaErrorDetailsC.renderDetails(element_errors[element_id])}</TreeView> + ); + rendered_element_errors.push(element_error) + } + return ( + <React.Fragment> + {rendered_element_errors} + </React.Fragment> + ); + } + + renderRTSMPolarizationDetails(polarization){ + const RTSMPerPolarization = this.props.content[polarization]; + if(RTSMPerPolarization === undefined) return; + return ( + <Card> + <CardBody> + <CardSubtitle className="stcv-header"> + RCU {RTSMPerPolarization.rcu} - {polarization} + </CardSubtitle> + + <Container> + <Row> + <Col> + <Row> + <div>percentage: {RTSMPerPolarization.percentage.toFixed(2)} %</div> + </Row> + <Row> + <div>mode: {RTSMPerPolarization.mode}</div> + </Row> + </Col> + <Col> + <div style={{width: "20em"}}><ModalPicture url={RTSMPerPolarization.url} /></div> + </Col> + </Row> + </Container> + </CardBody> + </Card> + ); + } + + renderRTSMDetails(){ + if (this.props.hasOwnProperty('test_type') && this.props.test_type === 'S') return; + return ( + <React.Fragment> + {this.renderRTSMPolarizationDetails('X')} + {this.renderRTSMPolarizationDetails('Y')} + </React.Fragment> + ) + + } + + renderSTDetails(){ + if (this.props.hasOwnProperty('test_type') && this.props.test_type === 'R') return; + const detailsFragment = ( + <TreeView nodeLabel="details"> + {AntennaErrorDetailsC.renderDetails(this.props.content.details)} + </TreeView> + ); + const elementsFragment = ( + <TreeView nodeLabel="elements"> + {this.renderElementErrors()} + </TreeView> + ); + const elementErrors = this.props.content.element_errors; + return ( + <React.Fragment> + {this.props.content.hasOwnProperty('details')? detailsFragment: undefined} + {elementErrors !== undefined && Object.keys(elementErrors).length > 0? elementsFragment: undefined} + </React.Fragment> + ); + } + + static renderEmptyContent(){ + return ( + <div> + <i>Hover the mouse over an error to view the details. Right-click on the error to pin it on this panel.</i> + </div>); + } + + renderContent(){ + if(this.props.test_type === undefined) return AntennaErrorDetailsC.renderEmptyContent(); + let jsx; + switch(this.props.test_type){ + case 'S': + jsx = this.renderSTDetails(); + break; + case 'R': + jsx = this.renderRTSMDetails(); + break; + } + return ( + <React.Fragment> + {jsx} + </React.Fragment> + ); + } + render(){ + const title = this.composeTitle(); + let unpinButton = ""; + if (this.props.isPinned) { + unpinButton = <Button title="Click to unpin the error details" color="info" size="xs" style={{float:'right'}} onClick={this.unpinPanel}> + <CloseIcon/> unpin + </Button> + } + return ( + <div className="stcv"> + <div className="stcv-header">{title} {unpinButton}</div> + {this.renderContent()} + </div>) + } +} + +const AntennaErrorDetails = connect(state => { + return { + ...state.antenna_page.child_panel, + ...state.antenna_page.main_panel.antenna_error_data, + ...state.mainFilters + }; +},{pinAntennaError, selectAntennaError})(AntennaErrorDetailsC); + + +export default AntennaErrorDetails; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaView.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaView.js new file mode 100644 index 0000000000000000000000000000000000000000..9fe8768a0c929773dda40cf8ad24fb8f23a9e4ef --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/AntennaView.js @@ -0,0 +1,352 @@ +import React, { + Component +} from 'react'; + +import ReactTableContainer from 'react-table-container'; +import AutoLoadWrapper from '../utils/autoLoader.js'; +import {connect} from 'react-redux'; +import {componentErrorTypes, datetime_format} from "../utils/constants.js"; +import moment from 'moment'; +import { IoMdArrowDropdown as DropDownIcon} from 'react-icons/io'; + +import {selectAntennaError, pinAntennaError} from '../redux/actions/antennaOverviewPageActions'; +import {renderDateRange} from '../utils/utils'; + +import classnames from 'classnames'; + +// CSS +import './StationOverview.scss'; +import './StationTestView.scss'; +import FillHeight from './FillHeight.js'; + + +class GenericStatusC extends Component { + + mouseOver = () => { + if (this.props.data !== undefined && this.props.isPinned === false) { + this.props.selectAntennaError(this.props.data); + } + } + + mouseOut = () => { + if (this.props.data !== undefined && this.props.isPinned === false) { + this.props.selectAntennaError({}); + } + } + + renderStationTest() { + const label = this.props.isGood ? '' : 'X'; + return (<React.Fragment>{label}</React.Fragment>); + } + + renderRTSM() { + const xPolStyle = { + float: 'left', + position: 'relative', + top: '-0.3rem' + }; + const yPolStyle = { + float: 'right', + position: 'relative', + right: '0.rem', + bottom: '-.2rem' + }; + + + const xPol = ( + <div style={xPolStyle} key={'X'}>X</div> + ); + const yPol = ( + <div style={yPolStyle} key={'Y'}>Y</div> + ); + let presentErrors = []; + const errorsPerPolarization = this.props.data.content; + if (errorsPerPolarization.hasOwnProperty('X')) presentErrors.push(xPol); + if (errorsPerPolarization.hasOwnProperty('Y')) presentErrors.push(yPol); + + return <div>{presentErrors}</div>; + } + + renderError() { + switch (this.props.data.test_type) { + case 'R': + return this.renderRTSM(); + case 'S': + return this.renderStationTest(); + default: + return + } + } + + onContextMenu = (e) => { + e.preventDefault(); + if (this.props.data !== undefined) { + this.props.selectAntennaError(this.props.data); + this.props.pinAntennaError(true); + } + } + + render() { + + const color = this.props.isGood ? "so-good" : "so-serious"; + const label = this.props.isGood ? "" : this.renderError(); + return ( + <td className={"stv-component-status " + color} + onContextMenu={this.onContextMenu} + onMouseOver={this.mouseOver} + onMouseOut={this.mouseOut}> + {label} + </td>); + } +} + +// TestLine is connected to Redux store +const GenericStatus = connect(state => { + return { + ...state.antenna_page.main_panel, + ...state.antenna_page.child_panel + }; +}, { + selectAntennaError, + pinAntennaError +})(GenericStatusC); + + +class AntennaErrorLineC extends Component { + + state = { + isCollapsed: true + } + + static composeHeaderLine(data) { + return (<React.Fragment> + <th>{data.test_type}</th> + <th>{moment(data.start_date).format(datetime_format)}</th> + </React.Fragment>); + } + + + + composeTestLine(data) { + let line = []; + let element = {}; + let component_errors; + component_errors = data.component_errors; + + for (let i = 0; i < this.props.errorTypes.length; i++) { + const error_type = this.props.errorTypes[i]; + if (component_errors && component_errors.hasOwnProperty(error_type)) { + const component_errors_per_type = component_errors[error_type]; + + if (component_errors_per_type.hasOwnProperty('element_errors')) { + for (let element_id of Object.keys(component_errors_per_type.element_errors)) { + if (!element.hasOwnProperty(element_id)) element[element_id] = []; + element[element_id].push({ + 'error type': error_type, + 'content': component_errors_per_type.element_errors[element_id] + }) + } + } + line.push(<GenericStatus isGood={false} + key={i} data={{ + test_type: data.test_type, + start_date: data.start_date, + end_date: data.end_date, + + error_type: error_type, + content: component_errors_per_type + }}/>) + } else { + line.push(<GenericStatus key={i} isGood={true}/>) + } + + + } + line.push(<td key="space"></td>); + for (let i = 1; i <= 16; i++) { + if (element.hasOwnProperty(i)) { + const errorType = element[i].length > 1 ? 'Multiple': element[i][0]['error type']; + const elementErrors = {}; + elementErrors['details'] = {}; + if(element[i].length > 1){ + for(const key in element[i]){ + elementErrors.details[`Error #${key}`] = element[i][key] + } + }else{ + elementErrors.details = {...element[i][0]} + } + + line.push(<GenericStatus key={`element${i}`} isGood={false} + data={ + { + test_type: data.test_type, + start_date: data.start_date, + end_date: data.end_date, + error_type: errorType, + element_id: i, + content: elementErrors + } + }/>); + + } else if (this.props.data.test_type === 'S') { + line.push(<GenericStatus key={`element${i}`} isGood={true}/>); + } + } + + return line + } + + dropdownClick = () => { + this.setState({isCollapsed: !this.state.isCollapsed}) + this.props.update() + } + + renderCollapsedLine() { + const errorTypes = this.props.errorTypes; + const errors = this.props.data; + const totalNumberOfErrors = errors.length; + const timeSpan = renderDateRange(errors); + const summary = {}; + for (const type of errorTypes) { + for (const error of errors) { + if (error.component_errors.hasOwnProperty(type)) { + if (!summary.hasOwnProperty(type)) summary[type] = 0.; + summary[type] += 1.; + } + } + } + const summaryItems = errorTypes.map(item => { + if (summary.hasOwnProperty(item)) { + return <td key={`element-${item}`} + className="stv-rtsm-summary-badge">{(summary[item] * 100 / totalNumberOfErrors).toFixed(0)}%</td> + } else { + return <td key={`element-${item}`}></td> + } + }); + const dropdownClass = classnames('dropdownbutton', {'dropdownbutton-up': !this.state.isCollapsed}); + return <tr className={'stv-rtsm-summary-row'}> + <td >R</td> + <td onClick={this.dropdownClick}>{timeSpan} + <DropDownIcon className={dropdownClass}/> + </td> + {summaryItems}</tr> + } + + renderAllLines() { + return this.props.data.map((test, id) => + <tr key={id}>{AntennaErrorLineC.composeHeaderLine(test)}{this.composeTestLine(test)}</tr>); + + } + + renderMultilineSummary() { + + const summaryLine = this.renderCollapsedLine(); + let additionalLines; + if (!this.state.isCollapsed) { + additionalLines = this.renderAllLines(); + } + return <React.Fragment>{summaryLine}{additionalLines}</React.Fragment> + } + + render() { + const data = this.props.data; + if (data.hasOwnProperty('test_type')) { + return <React.Fragment> + <tr>{AntennaErrorLineC.composeHeaderLine(data)}{this.composeTestLine(data)}</tr> + </React.Fragment>; + } else { + return <React.Fragment>{this.renderMultilineSummary()}</React.Fragment>; + } + } +} + +const AntennaErrorLine = connect(state => { + return { + ...state.mainFilters, + ...state.appInitData + }; +})(AntennaErrorLineC); + + +class AntennaViewC extends Component { + + renderHeader() { + const errors_columns = this.props.errorTypes.map((error_type, key) => { + return (<td key={key} title={error_type}>{componentErrorTypes[error_type]}</td>) + }); + + const element_ids = []; + for (let i = 1; i <= 16; i++) element_ids.push(<td key={i}>{i}</td>); + return <tr> + <th title="Test type">T</th> + <th style={{width: "10em"}}>Date</th> + {errors_columns} + <th></th> + {element_ids} + </tr>; + }; + + static groupData(data) { + let grouped = []; + let group = []; + for (let test of data) { + + if (test.test_type !== 'R') { + if (group.length > 0) { + grouped.push(group); + group = []; + } + grouped.push(test) + } else { + group.push(test) + } + } + if (group.length > 0) grouped.push(group); + + return grouped + } + + updateIfContentChanges = () => { + this.setState({state:this.state}) + } + + renderBody() { + if (this.props.data.hasOwnProperty('errors')) { + let groupedData = AntennaViewC.groupData(this.props.data.errors); + return groupedData.map((test, id) => <AntennaErrorLine key={id} data={test} update={this.updateIfContentChanges}/>); + } + + + } + + render() { + + const jsx = ( + <ReactTableContainer width="100%" height={this.props.height+'px'}> + <table className="stv-table table-sm table-hover table-bordered"> + <thead className={"stv-tableheader"} >{this.renderHeader()}</thead> + <tbody>{this.renderBody()}</tbody> + </table> + </ReactTableContainer> + ); + return jsx; + } +} + +const AntennaViewContent = function(props){ + return ( + <FillHeight className="border-right"> + { ({height}) => <AntennaViewC {...props} height={height} />} + </FillHeight> + ) +} + +const AntennaViewController = connect(state => { + return { + ...state.mainFilters, + ...state.appInitData + }; +})(AntennaViewContent); +const AntennaView = AutoLoadWrapper(AntennaViewController); + +export default AntennaView; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/FillHeight.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/FillHeight.js new file mode 100644 index 0000000000000000000000000000000000000000..f36a94934af6d9cc28eaf3c7986d37549a94afbb --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/FillHeight.js @@ -0,0 +1,88 @@ +import React from "react"; + +/* + * FillHeight: Creates a div that fills the remaining height of the viewport. + * Window resize is monitored to scale the div accordingly. + * + * This component uses the 'render prop' pattern where the 'children' prop is used + * as the render function. The height of the div (in an object) is passed to + * the function for use by subcomponents. E.g.: + * + * <FillHeight> + * { (props) => <MyComponent hgt={props.height} /> } + * </FillHeight> + */ +class FillHeight extends React.Component { + + static defaultProps = { + className: 'fill-height-container', // classname for the container div + gutterBottom: 10, // px, leave some space at the bottom + minHeight: 200 // px, minimum height + }; + + state = { + height: 500 // px, height of the container div + }; + + node = null; // internal ref to the DOM node (div) + mounted = false; // bool + resizeTimeout = null; // throttling of render during resize + + + componentDidMount() { + this.mounted = true; + + window.addEventListener("resize", () => { + window.clearTimeout(this.resizeTimeout); + this.resizeTimeout = window.setTimeout(this.onWindowResize, 200); + }); + + // force a render now that it has been mounted + // (the offset is known at this point) + this.onWindowResize(); + } + + componentWillUnmount() { + this.mounted = false; + window.removeEventListener("resize", this.onWindowResize); + } + + onWindowResize = () => { + if (!this.mounted || !this.node) { + return; + } + + let offset = this.node.getBoundingClientRect().top; + let height = window.innerHeight - offset - this.props.gutterBottom; + if (height < this.props.minHeight) { + height = this.props.minHeight; + } + + this.setState({ + height + }); + }; + + getNodeRef = (node) => { + this.node = node; + } + + render() { + // Create a simple div for measuring the viewport offset when it mounts, + // then re-render. + if (!this.mounted) { + return ( + <div ref={this.getNodeRef} /> + ); + } + + // Render prop pattern; pass height to child render function + return ( + <div ref={this.getNodeRef} className={this.props.className} style={{height: this.state.height+'px'}}> + { this.props.children({height: this.state.height}) } + </div> + ); + } +}; + +export default FillHeight; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.css index 3ec2c0158b7b87ed251af31b361753b754e2e02c..5621f63474414e48e745aa3863172fe5abd38c11 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.css @@ -4,7 +4,7 @@ /* font color */ /* Data colors */ .hoverable:hover { - background-color: #bdbdbd; } + background-color: #b6b6ba; } .table-wrapper { width: 10em; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js index ddebb174766ae830b7a371eadc6cddabf54f3b67..75e0d715aadf3c486551d3a3f7065aa0842c246d 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js @@ -10,18 +10,19 @@ class SelectableOption extends Component { const selectMark = this.props.isSelected ? <IsSelectIcon style={{width:'1rem'}} /> : <div style={{paddingRight:'1rem'}}/>; const jsx = ( <DropdownItem onClick={this.selectedItem} > + <table><tbody> <tr style={{position:'relative', left: '-1rem'}}> <td>{selectMark}</td> - <td>{this.props.children}</td></tr> + <td>{this.props.children}</td></tr></tbody></table> </DropdownItem> - ) + ); return jsx } } export class MultiSelectDropdown extends Component{ constructor(props){ - super(props) + super(props); this.state = { isOpen: false, @@ -79,7 +80,7 @@ export class MultiSelectDropdown extends Component{ isItemSelected = (e) => { if(this.state.selectedItems.hasOwnProperty(e)) - return this.state.selectedItems[e] + return this.state.selectedItems[e]; return false } @@ -89,7 +90,7 @@ export class MultiSelectDropdown extends Component{ if(this.props.selectedItems === undefined) return for(let item of this.props.selectedItems){ if(!selectedItems.hasOwnProperty(item) || !selectedItems[item] ){ - selectedItems[item] = true + selectedItems[item] = true; update = true } } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css index d76b043672b29789d9e5849176b2f0e51358b4c5..b7172e7d24fd1e8a071985ffb4592b35bc7f0e36 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css @@ -7,13 +7,18 @@ position: relative; display: inline-block; } +.react-autosuggest__container svg { + font-size: 1.2rem; } + +.react-autosuggest__container button.btn.btn-info { + padding: .25rem .3rem; } + /* .react-autosuggest__input { } */ -.react-autosuggest__container--open .react-autosuggest__input { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } +.react-autosuggest__container .react-autosuggest__input { + border-radius: 0; } .react-autosuggest__suggestions-container { display: none; } @@ -56,4 +61,17 @@ background-repeat: no-repeat; } .react-autosuggest__suggestion--highlighted { - background-color: #8d8d8d; } + background-color: #86868a; } + +.react-autosuggest__container .btn-clear { + position: absolute; + right: 4px; + top: 0; + bottom: 0; + height: 100%; + margin: auto; + cursor: pointer; + color: #b6b6ba; } + +.react-autosuggest__container .btn-clear:hover { + color: #86868a; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js index 383544a188ea96feeb0a3ebd69c8e9abec27139a..b8def694ce7dd4d59935ce50205f8c53ffc93d28 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js @@ -1,12 +1,23 @@ import React, {Component} from 'react'; import {connect} from "react-redux"; import Autosuggest from 'react-autosuggest'; +import { Input, InputGroup, InputGroupAddon, Button } from 'reactstrap'; +import { + IoMdCloseCircleOutline as CloseIcon, + IoIosArrowBack as BackIcon, + IoIosArrowForward as ForwardIcon +} from 'react-icons/io'; -import './StationAutoComplete.css' +// History handling +import { push } from 'connected-react-router'; +import { store } from "../redux/store.js"; + +// CSS +import './StationAutoComplete.scss' /** - * StationAutoCompleteC; class to render an input field for station name with auto-completion. + * AutoCompleteC; class to render an input field for station name with auto-completion. * * The parent component is notified about a new station name (through the onChange callback) * when the user presses 'Enter' in the input field or when an item from the list of @@ -17,9 +28,15 @@ import './StationAutoComplete.css' * selected station was changed outside this component. In a new instance the state.value is * set to the selectedStation prop. * - * Usage: <StationAutoComplete key={station} selectedStation={station} onChange={onchange} /> + * Usage: <AutoComplete key={station} onChange={onchange} stations={stations} selectedStation={selectedStation}/> */ -class StationAutoCompleteC extends Component { +class AutoComplete extends Component { + + // The DOM node of the input + inputRef = null; + + // Timeout for station loading when back/forward button is used + timeoutId = null; // Autosuggest is a controlled component. // However the input value is decoupled from Redux state but gets its @@ -34,26 +51,19 @@ class StationAutoCompleteC extends Component { const inputValue = value.trim().toLowerCase(); const inputLength = inputValue.length; - return inputLength === 0 ? this.props.stations : this.props.stations.filter(lang => - //lang.name.toLowerCase().slice(0, inputLength) === inputValue - lang.name.toLowerCase().indexOf(inputValue) > -1 + return inputLength === 0 ? this.props.stations : this.props.stations.filter(obj => + //obj.name.toLowerCase().slice(0, inputLength) === inputValue + obj.name.toLowerCase().indexOf(inputValue) > -1 ); }; // Get value to show in the input field when a suggestion is chosen getSuggestionValue = suggestion => suggestion.name; - // Render a suggestion - renderSuggestion = suggestion => ( - <div> - {suggestion.name} - </div> - ); - // Change handler for input onChange = (event, { newValue, method }) => { this.setState({ - value: newValue + value: newValue.toUpperCase() }); }; @@ -89,6 +99,75 @@ class StationAutoCompleteC extends Component { return true; }; + getInput = (input) => { + this.inputRef = input + } + + // Clear button handler for input + clearInput = () => { + this.setState( + { value: "" }, + () => this.inputRef.focus() + ); + }; + + inputNotification = (bgcolor, text) => { + this.inputRef.style.backgroundColor = bgcolor; + this.inputRef.value = text; + window.setTimeout(() => { + if (this.inputRef) { + this.inputRef.style.backgroundColor = null; + this.inputRef.value = this.state.value; + } + }, 300) + + } + + backForward = (e) => { + const mode = e.currentTarget.id; + const v = this.state.value; + // Find index of selected station in array + let idx = this.props.stations.findIndex(obj => obj.name === v); + if (idx === -1) { + this.inputNotification('#f9abab', 'NOT FOUND'); + return; + } + idx = (mode === "btn-forward" ? idx+1 : idx-1); + if (idx < 0 || idx >= this.props.stations.length) { + // At first station (mode=back) or at last station (mode=forward) + this.inputNotification('#a2d6a2', mode==="btn-forward" ? 'LAST' : 'FIRST'); + return; + } + // Update display value and reload after 500ms (prevents reloading on fast clicking) + window.clearTimeout(this.timeoutId); + this.setState({ + value: this.props.stations[idx].name + }); + this.timeoutId = window.setTimeout(() => this.props.onChange(this.props.stations[idx].name), 500); + } + + // Render a suggestion + renderSuggestion = suggestion => ( + <div> + {suggestion.name} + </div> + ); + + renderInputComponent = inputProps => ( + <InputGroup size="sm"> + <InputGroupAddon addonType="prepend"> + <Button id="btn-back" color="info" onClick={this.backForward}><BackIcon /></Button> + </InputGroupAddon> + <div className="btn-group"> + <Input innerRef={this.getInput} {...inputProps} /> + <CloseIcon onClick={this.clearInput} className="btn-clear"/> + </div> + <InputGroupAddon addonType="append"> + <Button id="btn-forward" color="info" onClick={this.backForward}><ForwardIcon /></Button> + </InputGroupAddon> + </InputGroup> + ); + render() { @@ -110,17 +189,42 @@ class StationAutoCompleteC extends Component { shouldRenderSuggestions={this.shouldRenderSuggestions} getSuggestionValue={this.getSuggestionValue} renderSuggestion={this.renderSuggestion} + renderInputComponent={this.renderInputComponent} inputProps={inputProps} /> ); } } + + +class StationAutoCompleteC extends Component { + + + // Callback for StationAutoComplete + onStationChange = (station) => { + store.dispatch(push(`?station=${station}`)) + } + + render() { + // The key on AutoComplete is important, see the desc of AutoComplete + // That's the main reason why this wrapper exists. + return( + <div className="toolbar-ctrl"> + <AutoComplete key={this.props.selectedStation} + stations={this.props.stations} + selectedStation={this.props.selectedStation} + onChange={this.onStationChange}/> + </div> + ); + } +} + // Get full list of stations from redux const mapStateToPropsToolBar = state => { return { - //stations: [{name:'cs001c'},{name:'cs002c'},{name:'cs003c'}] - stations: state.appInitData.stations + stations: state.appInitData.stations, + selectedStation: state.mainFilters.selectedStation }; }; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss index a9d65f862b64fc5016539333edf885267e0d4afe..4b7f44a72a7e1696fe817bb016af9a41a53b7e6b 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss @@ -6,14 +6,19 @@ display: inline-block; } +.react-autosuggest__container svg { + font-size: 1.2rem; +} +.react-autosuggest__container button.btn.btn-info { + padding: .25rem .3rem; +} /* .react-autosuggest__input { } */ -.react-autosuggest__container--open .react-autosuggest__input { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; +.react-autosuggest__container .react-autosuggest__input { + border-radius: 0; } .react-autosuggest__suggestions-container { @@ -66,3 +71,18 @@ .react-autosuggest__suggestion--highlighted { background-color: $secondary-dark; } + +// Clear icon in input +.react-autosuggest__container .btn-clear { + position: absolute; + right: 4px; + top: 0; + bottom: 0; + height: 100%; + margin: auto; + cursor: pointer; + color: $secondary; +} +.react-autosuggest__container .btn-clear:hover { + color: $secondary-dark; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.css index 32a3e3b713e9c855476633cf3ed361a829825d31..11a0240c2c57a99f62ab7ef9cb93dbed336b962b 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.css @@ -48,8 +48,8 @@ float: left; height: 1.4rem; line-height: 1.4rem; - background: #e1e1e1; - border: 1px solid #8d8d8d; + background: #e8e8ec; + border: 1px solid #86868a; border-radius: .2rem; padding: 0 0.5em; text-align: left; @@ -63,8 +63,8 @@ .so-stationtestbadge:hover, .so-badge:hover { color: #fff; - background-color: #8d8d8d; - border-color: #8d8d8d; } + background-color: #86868a; + border-color: #86868a; } .so-pill { display: block; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js index 2e423eefd723ecc722ef4b353da603532eacd754..d61e96a4f824764cc59971de04c8cf6e6699f172 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js @@ -42,14 +42,14 @@ const SOPopover = ({target, isOpen, togglePopover, title, lines}) => { return null; } - return (<Popover placement="auto" isOpen={isOpen} target={target} toggle={togglePopover}> + return (<Popover placement="auto" isOpen={isOpen} target={target} toggle={togglePopover} key={title}> <PopoverHeader> {title} </PopoverHeader> <PopoverBody> <Table borderless size="sm"> <tbody> - {lines.map( (obj, id) => <tr> + {lines.map( (obj, id) => <tr key={id}> <th>{obj.th}</th> <td>{obj.td}</td> </tr>) diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js index 5ff27fd80e34ac18dc30480b725b0975d8a5c0dc..9fd2a6e3d046d0f0f83c7cf4f3b27f657d57458d 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js @@ -86,7 +86,7 @@ class StationStatisticsC extends Component { histogramType: 'per_error_type' }; this.ref = React.createRef(); - this.getErrorsPerTypeSpec.bind(this); + StationStatisticsC.getErrorsPerTypeSpec.bind(this); } getErrorsPerStation() { if (this.props.data.errors_per_station !== undefined) { @@ -99,8 +99,7 @@ class StationStatisticsC extends Component { return {values: this.props.data.errors_per_type}; } } - - getBaseSpec() { + static getBaseSpec() { return { "$schema": "https://vega.github.io/schema/vega-lite/v2.json", "mark": { @@ -110,9 +109,12 @@ class StationStatisticsC extends Component { "type": "fit", "contains": "padding" }, + "selection":{ + "highlight": {"type":"single", "empty":"none","on": "mouseover"}, + }, "config": { "legend": { - "columns": 2 + "columns": 2, } }, "encoding": { @@ -128,14 +130,19 @@ class StationStatisticsC extends Component { "type": "quantitative", "axis": { "title": "number of errors" - } + }, + + }, + opacity: { + "condition": {"selection": "highlight", "value": .6}, + value: 1 } - } + }, } } - getErrorsPerTypeSpec() { - let schema = this.getBaseSpec(); + static getErrorsPerTypeSpec() { + let schema = StationStatisticsC.getBaseSpec(); schema.encoding["color"] = { "field": "error_type", "type": "nominal" @@ -152,12 +159,12 @@ class StationStatisticsC extends Component { return schema; } - getErrorsPerStationSpec() { - let schema = this.getBaseSpec(); + static getErrorsPerStationSpec() { + let schema = StationStatisticsC.getBaseSpec(); schema.encoding["color"] = { "field": "station_name", "type": "nominal" - } + }; schema.encoding["tooltip"] = [ { field: "n_errors", @@ -166,19 +173,19 @@ class StationStatisticsC extends Component { field: "station_name", type: "nominal" } - ] - schema.config.legend.columns = 3 + ]; + schema.config.legend.columns = 3; return schema; } getSpecData(histogram_type) { switch (histogram_type) { case "per_error_type": - return {spec: this.getErrorsPerTypeSpec(), data: this.getErrorsPerType()}; + return {spec: StationStatisticsC.getErrorsPerTypeSpec(), data: this.getErrorsPerType()}; case "per_station": - return {spec: this.getErrorsPerStationSpec(), data: this.getErrorsPerStation()}; + return {spec: StationStatisticsC.getErrorsPerStationSpec(), data: this.getErrorsPerStation()}; default: - return {spec: this.getErrorsPerTypeSpec(), data: this.getErrorsPerType()}; + return {spec: StationStatisticsC.getErrorsPerTypeSpec(), data: this.getErrorsPerType()}; } } @@ -195,8 +202,8 @@ class StationStatisticsC extends Component { render() { const {spec, data} = this.getSpecData(this.state.histogramType); if (this.ref.current !== null) { - const width = this.ref.current.clientWidth - const height = this.ref.current.clientHeight + const width = this.ref.current.clientWidth; + const height = this.ref.current.clientHeight; spec.width = width spec.height = height diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css index b4c07fae728766e11eeb124422f6164c121221e9..5dc5cc855b3ac5b93a00c83e21d2f5335ee67476 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css @@ -5,7 +5,7 @@ /* Data colors */ .stcv-header { padding: .5rem 0; - color: #8d8d8d; } + color: #86868a; } .stcv img { cursor: pointer; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js index c5323ec642d6cf42b2401468cb17847a1ae23c26..679263de7c633c9e8028d472726985f9ae18794c 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js @@ -1,48 +1,107 @@ -import React, { - Component -} from 'react'; +import React, {Component} from 'react'; import {connect} from "react-redux"; import {unpinChildPanelData} from '../redux/actions/stationOverviewPageActions' -import { - Button, - Table, - Badge, - Modal, - ModalHeader, - ModalBody -} from 'reactstrap'; -import { IoMdCloseCircleOutline as CloseIcon} from 'react-icons/io'; +import {Badge, Button, Modal, ModalBody, ModalHeader, Table} from 'reactstrap'; +import {IoMdCloseCircleOutline as CloseIcon} from 'react-icons/io'; import './StationOverview.css' import './StationTestView.css' import '../themes/lofar-styles.css' - // CSS import './StationTestChildView.css'; - class StationTestChildViewC extends Component { state = { modal: false, - modal_url: "" - } + modalUrl: "" + }; + + imgLoadTimeout = {}; toggleModal = (e) => { this.setState({ modal: !this.state.modal, - modal_url: e.currentTarget.src + modalUrl: e.currentTarget.src }); - } + }; unpinPanel = () => { this.props.unpinChildPanelData(); - } + }; + + onImgError = (e) => { + let img = e.currentTarget; + img.alt = "Reloading in 2 sec.."; + // let the window figure out if the timeout id is still valid + if (this.imgLoadTimeout[img.id]) { + clearTimeout(this.imgLoadTimeout[img.id]); + } + this.imgLoadTimeout[img.id] = setTimeout(() => { img.src = img.src; }, 2000 ); + }; + + clearTimeouts() { + let key; + for (key in this.imgLoadTimeout) { + clearTimeout(this.imgLoadTimeout[key]); + } + this.imgLoadTimeout = {}; + }; + + renderErrorDetails(pdata, key){ + + let pol = pdata['polarization']; + let rcuId = pdata['rcu_id']; + let err_items = [], + ignore = { + start_frequency: 1, + stop_frequency: 1, + polarization: 1, + rcu_id: 1, + antenna_id: 1, + url: 1 + }, + img = ""; + + // first process frequency range and url + if (pdata.details.hasOwnProperty("start_frequency")) { + err_items.push(<li key="freq">frequency-range: {pdata.details.start_frequency}-{pdata.details.stop_frequency} MHz</li>); + } + if (pdata.details.url) { + img = <img id={'img-'+pol+'-'+key} src={pdata.details.url} onClick={this.toggleModal} onError={this.onImgError} title="Click to enlarge" alt="Not present"/>; + } + + // process remaining items + Object.keys(pdata.details).forEach((parameter, key) =>{ + if (! ignore.hasOwnProperty(parameter)) { + const parameter_value = pdata.details[parameter]; + const rendered_parameter = parameter === 'percentage'? parameter_value.toFixed(2) + '%' : parameter_value; + err_items.push(<li key={parameter}>{parameter}: {rendered_parameter}</li>); + }}); + + if (err_items.length === 0) { + err_items = <li><em>See element error.</em></li>; + } + const imgHeader = (<th scope="row">RCU {rcuId} ({pol}){img}</th>); + return ( + <tr key={key}> + {rcuId ? imgHeader : undefined} + <td> + <ul style={{listStyleType: 'none', paddingLeft: '0.5em'}}> + <li><Badge className='error-type-badge' color="danger">{pdata.error_type}</Badge></li> + {err_items} + </ul> + </td> + </tr> + ); + }; render() { let rows, title; + this.clearTimeouts(); + // defaults when data is null rows = <tr><td><i>Hover the mouse over an error to view the details. Right-click on the error to pin it on this panel.</i></td></tr>; title = "Error details"; @@ -51,56 +110,20 @@ class StationTestChildViewC extends Component { rows = []; title = `${this.props.data.component_type}, Antenna ${this.props.data.component_id}, Test ${this.props.data.datetime}`; - for (let pol of ['X', 'Y']){ - let pdata = this.props.data[pol]; - let rcuId = this.props.data[pol+"_rcu_id"]; - - pdata.forEach((item, key) => { - let err_items = [], - ignore = { - start_frequency: 1, - stop_frequency: 1, - url: 1 - }, - img = ""; - - // first process frequency range and url - if (item.details.hasOwnProperty("start_frequency")) { - err_items.push(<li key="freq">frequency-range: {item.details.start_frequency}-{item.details.stop_frequency} MHz</li>); - } - if (item.details.url) { - img = <img src={item.details.url} onClick={this.toggleModal} title="Click to enlarge" alt="Not present"/>; - } - - // process remaining items - Object.keys(item.details).forEach((parameter, key) =>{ - if (! ignore.hasOwnProperty(parameter)) { - const parameter_value = item.details[parameter]; - const rendered_parameter = parameter === 'percentage'? parameter_value.toFixed(2) + '%' : parameter_value; - err_items.push(<li key={parameter}>{parameter}: {rendered_parameter}</li>); + const data = this.props.data.errors; + for (const error_type of Object.keys(data)){ + let pdata = data[error_type]; + const key = error_type; + if(pdata.hasOwnProperty('X') || pdata.hasOwnProperty('Y')){ + for(const pol of ['X', 'Y']){ + if(!pdata.hasOwnProperty(pol)){ + continue; } - }); - - if (err_items.length === 0) { - err_items = <li><em>See element error.</em></li>; + rows.push(this.renderErrorDetails(pdata[pol], key + pol)) } - - rows.push( - <tr key={pol}> - <th scope="row">RCU {rcuId} ({pol}) - {img} - </th> - <td> - <ul style={{listStyleType: 'none', paddingLeft: '0.5em'}}> - <li><Badge className='error-type-badge' color="danger">{item.error_type}</Badge></li> - { err_items } - </ul> - </td> - </tr> - ); - - pol = ""; - }); + }else{ + rows.push(this.renderErrorDetails(pdata, key)) + } } } @@ -121,14 +144,12 @@ class StationTestChildViewC extends Component { <Modal isOpen={this.state.modal} fade={false} size="lg" toggle={this.toggle} className={this.props.className}> <ModalHeader toggle={this.toggleModal}></ModalHeader> <ModalBody> - <img style={{width: '100%'}} src={this.state.modal_url} alt="Not present" /> + <img style={{width: '100%'}} src={this.state.modalUrl} alt="Not present" /> </ModalBody> </Modal> </div>; } } - - /* Add some magic; use the AutoLoadWrapper to create a HOC that handles the auto-loading of the data for StationOverviewC. */ diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css index 395189869afb3d2d326302e77f9eed609541458e..aefc047c97d4cbd290e08d3f3cbf7705b5d5904f 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css @@ -24,7 +24,7 @@ .stv-component-status.highlight, .stv-testline.highlight { - background-color: #8d8d8d !important; + background-color: #86868a !important; color: white; } .stv-testline.highlight td:first-child::before { @@ -36,11 +36,12 @@ .stv-component-status:hover { color: #fff; - background-color: #8d8d8d; - border-color: #8d8d8d; } + background-color: #86868a; + border-color: #86868a; } .stv-rtsm-summary-badge { - background-color: #ffcd74; + background-color: #fbfb83; + color: black; text-shadow: 1px 2px white; font-size: 80%; text-align: center; } @@ -58,14 +59,14 @@ .clickable-tab-active { color: white !important; - background-color: #a7689d; } + background-color: #4a6889; } .clickable-tab { cursor: pointer; - color: #490f44; } + color: #001832; } .clickable-tab:hover { - color: #a7689d; } + color: #4a6889; } @keyframes animation-open { from { @@ -79,7 +80,7 @@ to { transform: rotate(0deg); } } -.stv-rtsm-summary-row .row-header-dropdownbutton { +.stv-rtsm-summary-row .dropdownbutton { display: inline; float: right; animation: animation-close; @@ -87,7 +88,7 @@ animation-iteration-count: 1; animation-timing-function: linear; } -.stv-rtsm-summary-row .row-header-dropdownbutton-up { +.stv-rtsm-summary-row .dropdownbutton-up { transform: rotate(180deg); animation: animation-open; animation-duration: 100ms; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js index 1553575993b913bc42f1e354b9a7c400114de703..60824482bf264510ed02871cc4be2b72fc9c4068 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js @@ -3,7 +3,6 @@ import React, { } from 'react'; import {connect} from "react-redux"; import {setChildPanelData} from '../redux/actions/stationOverviewPageActions' - import { Badge, NavItem, @@ -12,65 +11,30 @@ import { TabContent, Nav } from 'reactstrap'; -import { IoMdArrowDropdown as DropDownIcon} from 'react-icons/io'; +import {IoMdArrowDropdown as DropDownIcon} from 'react-icons/io'; import AutoLoadWrapper from '../utils/autoLoader.js' import * as LOFARDefinitions from '../utils/LOFARDefinitions' -import { datetime_format } from '../utils/constants' +import {datetime_format} from '../utils/constants' +import {renderDateRange} from '../utils/utils' import ReactTableContainer from "react-table-container"; import moment from 'moment'; +import {withRouter} from "react-router"; +import classNames from "classnames"; +import FillHeight from '../components/FillHeight.js'; -// CSS -import '../themes/lofar-styles.css' -import './StationOverview.css' -import './StationTestView.css' - -function antennaNumberFromRCUID(rcuID, componentType) { - if (componentType === "LBH" || componentType === "HBA") { - const result = {} - result.polarization = rcuID % 2 === 0 - ? 'X' - : 'Y' - result.id = Math.floor(rcuID / 2) - return result - } else if (componentType === "LBL") { - const result = {} - result.polarization = rcuID % 2 === 1 - ? 'X' - : 'Y' - result.id = Math.floor(rcuID / 2) + 48 - return result - } else { - return {id: rcuID} - } -} -function RCUIDFromAntennaNumberPolarization(antennaNumber, polarization, componentType) { - if (componentType === "LBH" || componentType === "HBA") { - const result = {} - const polarization_id = polarization === 'X' - ? 0 - : 1 - result.id = antennaNumber * 2 + polarization_id - return result - } else if (componentType === "LBL") { - const result = {} - const polarization_id = polarization === 'X' - ? 1 - : 0 - result.id = (antennaNumber - 48) * 2 + polarization_id - return result - } else { - return {id: antennaNumber} - } -} +// CSS +import '../themes/lofar-styles.css'; +import './StationOverview.css'; +import './StationTestView.css'; -class GenericStatus extends Component { +class GenericStatusTdC extends Component { - getClass(){ + getClass() { let cls = 'stv-component-status'; if (this.props.doHighlight) { - cls += " highlight"; + cls += " highlight"; } if (this.props.n_errors > 0) { cls += ' so-serious' @@ -83,9 +47,9 @@ class GenericStatus extends Component { onMouseOver = (e) => { e.stopPropagation(); if (this.props.n_errors > 0) { - this.props.onSelect(this.props.errors_per_polarization); + this.props.onSelect(this.props.data); } - } + }; onMouseOut = (e) => { e.stopPropagation(); @@ -93,34 +57,37 @@ class GenericStatus extends Component { this.props.onSelect(null); } - } + }; // left-click with mouse onClick = () => { - console.log('click (TODO, switch to tiles page)'); - } + this.props.history.push(`/tiles?antenna_id=${this.props.antenna_id}&antenna_type=${this.props.antenna_type}&station=${this.props.station_name}`); + }; // right-click with mouse onContextMenu = (e) => { e.preventDefault(); if (this.props.n_errors > 0) { - this.props.onSelect(this.props.errors_per_polarization, true); + this.props.onSelect(this.props.data, true); } - } + }; - render(){ - const jsx = <td className={this.getClass()} - onContextMenu={this.onContextMenu} - onClick={this.onClick} - onMouseOver={this.onMouseOver} - onMouseOut={this.onMouseOut}> - {this.props.n_errors === 0 ? ' ' : this.props.n_errors} - </td>; + render() { + const content = (this.props.n_errors===0 ? ' ' : this.props.n_errors); - return jsx; + return ( + <td className={this.getClass()} + onContextMenu={this.onContextMenu} + onClick={this.onClick} + onMouseOver={this.onMouseOver} + onMouseOut={this.onMouseOut}> + {content} + </td> + ); } } +const GenericStatusTd = withRouter(GenericStatusTdC); /** * TestLine: renders a table row with the results of one station test or RTSM run. @@ -129,23 +96,23 @@ class TestLineC extends Component { doHighlight = false; - formatDate(date){ + static formatDate(date) { return moment(date).format(datetime_format); } shouldHighlight() { const props = this.props, - data = props.highlightData; + errorData = props.highlightData; - return data !== null && - data.component_type === props.component_type && - data.test_type === props.test_type && - data.datetime === this.formatDate(props.data.end_date); + return errorData !== null && + errorData.component_type === props.component_type && + errorData.test_type === props.test_type && + errorData.datetime === TestLineC.formatDate(props.data.start_date); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState, nextContext) { // this.doHighlight will only be true for the previously and currently selected row - if (nextProps.highlightData !== this.props.highlightData && ! this.doHighlight) { + if (nextProps.highlightData !== this.props.highlightData && !this.doHighlight) { return false; } return true; @@ -156,55 +123,56 @@ class TestLineC extends Component { this.doHighlight = true; } this.props.setChildPanelData(data, doPin); - } + }; renderComponentErrors() { - const component_errors = this.props.data.component_errors; - const component_type = this.props.component_type; - const rendered_component_errors = this.props.ordered_component_ids.map((item, key) => { - let component_errors_count = 0 - const errorsPerPolarization = { - X: [], - Y: [], - // add some items for the child panel - X_rcu_id: RCUIDFromAntennaNumberPolarization(item, 'X', component_type).id, - Y_rcu_id: RCUIDFromAntennaNumberPolarization(item, 'Y', component_type).id, - test_type: this.props.test_type, - component_type: component_type, - component_id: item, - datetime: this.formatDate(this.props.data.end_date) - } - for (let polarization of['X', 'Y']) { - let rcu_id = RCUIDFromAntennaNumberPolarization(item, polarization, component_type).id - if (component_errors.hasOwnProperty(rcu_id)) { - component_errors_count += component_errors[rcu_id].length; - errorsPerPolarization[polarization].push(...component_errors[rcu_id]) - } + const componentErrors = this.props.data.component_errors; + + const renderedComponentErrors = this.props.ordered_component_ids.map((component_id, key) => { + let nErrors = 0, + errors = [], + errorData = {}; + + if (componentErrors.hasOwnProperty(component_id)) { + errors = componentErrors[component_id]; + nErrors = Object.keys(errors).length; + + // Data for child panel and checking if an antenna item must be highlighted permanently (see shouldHighlight) + errorData.errors = errors; + errorData.datetime= TestLineC.formatDate(this.props.data.start_date); + errorData.test_type = this.props.test_type; + errorData.component_id = component_id; + errorData.component_type = this.props.component_type; } - return <GenericStatus - doHighlight={this.doHighlight && item === this.props.highlightData.component_id} - key={key} - errors_per_polarization={errorsPerPolarization} - n_errors={component_errors_count} - onSelect={this.onSelect} - /> + return <GenericStatusTd + doHighlight={this.doHighlight && component_id === this.props.highlightData.component_id} + key={key} + data={errorData} + antenna_id={component_id} + antenna_type={this.props.component_type} + station_name={this.props.station_name} + n_errors={nErrors} + onSelect={this.onSelect} /> }); - return rendered_component_errors; + + return renderedComponentErrors; } render() { - const date = this.formatDate(this.props.data.end_date); + const date = TestLineC.formatDate(this.props.data.start_date); // Determine if this row needs to be highlighted this.doHighlight = this.shouldHighlight(); const cls = this.doHighlight ? "stv-testline highlight" : "stv-testline"; - return <tr className={cls}> - <td className="stv-testline-header">{this.props.test_type} {date}</td> - {this.renderComponentErrors()} - </tr> + return ( + <tr className={cls}> + <td className="stv-testline-header">{this.props.test_type} {date}</td> + {this.renderComponentErrors()} + </tr> + ); } } @@ -218,104 +186,99 @@ const TestLine = connect(state => { })(TestLineC); +/* + * RTSMSummaryLine: create one table row with percentages of errors per antenna. + */ +function RTSMSummaryLine(props) { + const data = props.data; -class RTSMSummaryLine extends Component { + const cols = props.ordered_component_ids.map((item, key) => { + if (data[item] > 0) { + let perc = Math.ceil(data[item]); + return (<td key={key} className={'stv-rtsm-summary-badge'}>{perc + '%'} </td>); + } else { + return <td key={key}></td>; + } + }); + + const dropdownAdditionStyles = classNames( + 'dropdownbutton', {'dropdownbutton-up': props.isExpanded}); + + return ( + <tr className='stv-rtsm-summary-row'> + <td className="row-header" onClick={props.onClick}> + RT {props.dateRange} + <DropDownIcon className={"row-header-dropdownbutton " + dropdownAdditionStyles} color="black"/> + </td> + {cols} + </tr> + ); +} - constructor(props){ - super(props); - this.state = {displaySingleTests: false} - } +/* + * RTSMLines: create summary line + expandable data rows + */ +class RTSMLines extends Component { - renderTestLine(key, data, component_id_list){ - return (<TestLine className="collapse open" key={key} ordered_component_ids={component_id_list} test_type="RT" component_type={this.props.component_type} station_type={this.props.station_type} data={data}/>) + state = { + displaySingleTests: false } - renderTestSummaryLine(data, component_id_list){ - const result = component_id_list.map((item, key) => { - if(data[item] > 0){ - let perc = Math.ceil(data[item]); - return (<td key={key} className={'stv-rtsm-summary-badge'}>{perc + '%'} </td> ); - } else { - return <td key={key}></td>; - } - }) - return result - } - - computeSummary(){ - let summary = {} + computeSummary() { + let summary = {}; let n_tests = this.props.data.length; - const component_id_list = this.props.ordered_component_ids - component_id_list.forEach(component_id => summary[component_id] = 0) + const component_id_list = this.props.ordered_component_ids; + component_id_list.forEach(component_id => summary[component_id] = 0); this.props.data.forEach((item, key) => { - Object.keys(item.component_errors).forEach((item) => - { - const id = antennaNumberFromRCUID(item, this.props.component_type).id - summary[id] += 1 - }) - }) + Object.keys(item.component_errors).forEach((component_id) => { + summary[component_id] += 1 + }) + }); - Object.keys(summary).forEach(item => summary[item] /= n_tests/50. ) + Object.keys(summary).forEach(item => summary[item] /= n_tests / 100.); return summary } - renderDateRange(data){ - const lastTest = data[0] - const firstTest = data[data.length - 1] - const duration = moment.duration(moment(lastTest.end_date)-moment(firstTest.start_date)) - const years = duration.years() - const days = duration.days() - const months = duration.months() - const hours = duration.hours() - let string = `${data.length} obs ` - if (years > 0) string += `${years}Y` - if (months > 0) string += `${months}M` - if (days > 0) string += `${days}d` - if (hours > 0) string += `${hours}h` - - return string - } - toggleDisplaySingleTests = (e) => { this.setState({ - displaySingleTests:!this.state.displaySingleTests + displaySingleTests: !this.state.displaySingleTests }); - } + this.props.update(); + }; - render(){ - let summary = this.computeSummary() - const component_id_list = this.props.ordered_component_ids - const summaryLine = this.renderTestSummaryLine(summary, component_id_list) - var jsx = undefined - var all_rtsm = undefined + render() { + let summaryData = this.computeSummary(); - if (this.state.displaySingleTests){ - all_rtsm = this.props.data.map((item, key) => this.renderTestLine(key, item, component_id_list)) - } + // RTSM data rows, only shown when expanded + let all_rtsm = this.state.displaySingleTests ? this.props.data : []; - let dropdownAdditionStyles = "" - if(this.state.displaySingleTests) dropdownAdditionStyles += "row-header-dropdownbutton-up" - jsx = ( - <React.Fragment> - <tr className='stv-rtsm-summary-row'> - <td className="row-header" onClick={this.toggleDisplaySingleTests}> - RT {this.renderDateRange(this.props.data)} - <DropDownIcon className={"row-header-dropdownbutton " + dropdownAdditionStyles} color="black" /> - </td> - {summaryLine} - </tr> - {all_rtsm} - </React.Fragment> - ) - - - return jsx; + return ( + <React.Fragment> + <RTSMSummaryLine onClick={this.toggleDisplaySingleTests} + isExpanded={this.state.displaySingleTests} + data={summaryData} + ordered_component_ids={this.props.ordered_component_ids} + dateRange={renderDateRange(this.props.data)} /> + { // All RTSM lines in this block (expanded or folded) + all_rtsm.map((item, key) => + <TestLine className="collapse open" + key={key} + test_type="RT" + ordered_component_ids={this.props.ordered_component_ids} + component_type={this.props.component_type} + station_name={this.props.station_name} + station_type={this.props.station_type} + data={item}/> + ) + } + </React.Fragment> + ); } } /** - * ComponentClass; renders a table of station tests and rtsm data for a component (HBA, RSP, LBH, etc.) + * ComponentClass; renders a table of station tests and rtsm data for one component (HBA, RSP, LBH, etc.) * * Props: * station_type: C, R or I @@ -324,156 +287,181 @@ class RTSMSummaryLine extends Component { */ class ComponentClass extends Component { - computeComponentIDList(componentType){ + computeComponentIDList(componentType) { let componentIDSet = new Set(); - let toComponentID = item => {return antennaNumberFromRCUID(item, componentType).id} - - this.props.data.forEach(test => {Object.keys(test.component_errors).forEach(item => componentIDSet.add(toComponentID(item)))}) - // Javascript only sorts strings :/ - // a comparing function is therefore needed + this.props.data.forEach(test => { + Object.keys(test.component_errors).forEach(item => componentIDSet.add(item)) + }); + // Numerical sort return Array.from(componentIDSet).sort((a, b) => a - b); } - renderHeader(){ - const components_id = this.computeComponentIDList(this.props.type) - - const result = components_id.map((item, key) => <th scope="col" key={key}>{item}</th>); - const jsx = ( - <thead className="stv-tableheader"> - <tr><th style={{textAlign:"left"}} scope="col"><Badge color="info">{this.props.type}</Badge></th>{result}</tr> - </thead> - ) - return jsx + renderStationTestLine(key, data, component_ids) { + return (<TestLine key={key} + test_type="ST" + ordered_component_ids={component_ids} + component_type={this.props.type} + station_name={this.props.station_name} + station_type={this.props.station_type} + data={data}/>) } - renderStationTestLine(key, data, component_id_list){ - return (<TestLine key={key} ordered_component_ids={component_id_list} test_type="ST" component_type={this.props.type} station_type={this.props.station_type} data={data}/>) + renderRTSMLines(key, data, component_ids) { + return (<RTSMLines key={key} + ordered_component_ids={component_ids} + component_type={this.props.type} + station_name={this.props.station_name} + station_type={this.props.station_type} + data={data} + update={this.updateIfComponentChanges} />) } - renderRTSMSummaryLine(key, data, component_id_list){ - return (<RTSMSummaryLine key={key} ordered_component_ids={component_id_list} component_type={this.props.type} station_type={this.props.station_type} data={data}/>) - } + renderTestLines(data, component_ids) { + const rows = []; + let tmp_rtsm_set = [], + num_items = data.length; + + for (let i = 0; i < num_items; i++) { + const current_item = data[i], + next_item = (i===num_items-1 ? null : data[i + 1]); - renderTestLines(data){ - const component_id_list = this.computeComponentIDList(this.props.type) - const rows = [] - var tmp_rtsm_set = [] + // Temporarily store RTSM lines + if (current_item.test_type === 'R') { + tmp_rtsm_set.push(current_item) - for (let i=0; i<data.length - 1; i++){ - const next_item = data[i + 1] - const current_item = data[i] - if (current_item.test_type === 'R' ){ - tmp_rtsm_set.push(current_item) + // Push lines when next item is a station test or when we are at the last item + if (next_item === null || next_item.test_type === 'S') { + rows.push(this.renderRTSMLines(rows.length, tmp_rtsm_set, component_ids)) + tmp_rtsm_set = [] + } } - if (current_item.test_type === 'R' && next_item.test_type === 'S'){ - rows.push(this.renderRTSMSummaryLine(rows.length, tmp_rtsm_set, component_id_list)) - tmp_rtsm_set = [] - } else if (current_item.test_type === 'S'){ - rows.push(this.renderStationTestLine(rows.length, current_item, component_id_list)); + else if (current_item.test_type === 'S') { + rows.push(this.renderStationTestLine(rows.length, current_item, component_ids)); } } - const lastTest = data[data.length - 1] - if (lastTest.test_type === 'R'){ - tmp_rtsm_set.push(lastTest) - } else { - rows.push(this.renderStationTestLine(rows.length, lastTest, component_id_list)); - } - if (tmp_rtsm_set.length > 0) { - rows.push(this.renderRTSMSummaryLine(rows.length, tmp_rtsm_set, component_id_list)) - tmp_rtsm_set = [] - } return rows } + updateIfComponentChanges = () => { + this.setState({ state: this.state }); + } + render() { - const rows = this.renderTestLines(this.props.data) + const comp_ids = this.computeComponentIDList(this.props.type); - const jsx = ( - <ReactTableContainer width="100%" height="79vh"> + return ( + <ReactTableContainer width="100%" height={this.props.height+'px'}> <table className="stv-table table-sm table-hover table-bordered"> - {this.renderHeader()} - <tbody> - {rows} - </tbody> + <thead className="stv-tableheader"> + <tr> + <th style={{textAlign: "left"}} scope="col"> + <Badge color="info">{this.props.type}</Badge> + </th> + {comp_ids.map((item, key) => + <th scope="col" key={key}>{item}</th>) + } + </tr> + </thead> + <tbody> + {this.renderTestLines(this.props.data, comp_ids)} + </tbody> </table> </ReactTableContainer> - ) - return jsx; + ); } } + +/* + * Render a Tab item + */ +function Tab({label, onClick, isActive}) { + const cls = isActive ? 'clickable-tab-active' : 'clickable-tab-unactive'; + + return ( + <NavItem className="clickable-tab"> + <NavLink className={cls} onClick={onClick}> + {label} + </NavLink> + </NavItem> + ); +} + /** * StationTestView class. */ class StationTestViewC extends Component { - constructor(props) { - super(props); - this.toggle = this.toggle.bind(this); - this.state = { - activeTab: undefined - }; + state = { + activeTab: undefined } - toggle(tab) { - if (this.state.activeTab !== tab) { - this.setState({ - activeTab: tab - }); - } - } + // Set the activeTab to the first component if it wasn't set yet or when + // the station changed and doesn't have the active component + static getDerivedStateFromProps(props, state) { + let componentTypes = Object.keys(props.data).sort(), + currentComponent = state.activeTab; - componentDidUpdate(){ - this.setDefaultTabIfUndefined() - } + if (componentTypes.length === 0) { + return null; + } - setDefaultTabIfUndefined(){ - const firstComponent = Object.keys(this.props.data)[0] - if(this.state !== undefined && this.state.activeTab === undefined && firstComponent !== undefined){ - this.setState({ - activeTab: firstComponent - }) + if (! currentComponent || componentTypes.findIndex((c) => currentComponent === c) === -1) { + return { + activeTab: componentTypes[0] + }; } + return null; } - isActiveClass = (componentType) => { - const className = this.state.activeTab === componentType ? 'active' : 'unactive' - return className + // Do not (re)render when data is loading (performance improvement) + shouldComponentUpdate(nextProps, nextState, nextContext) { + return nextProps.isLoading ? false : true; } - // Do not (re)render when data is loading (performance improvement) - shouldComponentUpdate(nextProps, nextState) { - if (nextProps.isLoading) { - return false; + toggleTab = (e) => { + let tab = e.currentTarget.innerHTML; + if (this.state.activeTab !== tab) { + this.setState({ + activeTab: tab + }); } - return true; } render() { - //console.log("render main"); const stationType = LOFARDefinitions.stationTypeFromName(this.props.selectedStation); + const componentTypes = Object.keys(this.props.data).sort(); - const navBar = Object.keys(this.props.data).map((componentType, key) => - (<NavItem key={key} className="clickable-tab"> - <NavLink className={'clickable-tab-' + this.isActiveClass(componentType)} - onClick={() => this.toggle(componentType)}>{componentType}</NavLink> - </NavItem>) - ); - - const componentListTabs = Object.keys(this.props.data).map((componentType, key) => - <TabPane key={key} tabId={componentType}> - <ComponentClass key={componentType} station_type={stationType} type={componentType} data={this.props.data[componentType]}/> - </TabPane> - ); + if (this.props.isLoading) { + return null; + } return ( <div> <Nav tabs className="component-type-selector"> - {navBar} + { + componentTypes.map((componentType, key) => + <Tab key={key} onClick={this.toggleTab} isActive={this.state.activeTab===componentType} label={componentType} /> + ) + } </Nav> - <TabContent activeTab={this.state.activeTab}> - {componentListTabs} - </TabContent> + <FillHeight className="border-right"> + { ({height}) => ( + <TabContent activeTab={this.state.activeTab}> + {componentTypes.map((componentType, key) => + <TabPane key={key} tabId={componentType}> + <ComponentClass key={componentType} + station_type={stationType} + type={componentType} + station_name={this.props.selectedStation} + data={this.props.data[componentType]} + height={height} + /> + </TabPane> + )} + </TabContent> + )} + </FillHeight> </div>); } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss index e155759eb20ef4e8191db1c27c5651ebc9b3efa6..31c17f5985150fdcb3c2d6d3642e3db82565db34 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss @@ -45,7 +45,8 @@ } .stv-rtsm-summary-badge{ - background-color: $alarming; + background-color: $warning; + color: $warning-fontcolor; text-shadow: 1px 2px white; font-size: 80%; text-align: center; @@ -101,7 +102,7 @@ } } -.stv-rtsm-summary-row .row-header-dropdownbutton { +.stv-rtsm-summary-row .dropdownbutton { display: inline; float: right; animation: animation-close; @@ -110,7 +111,7 @@ animation-timing-function: linear; } -.stv-rtsm-summary-row .row-header-dropdownbutton-up { +.stv-rtsm-summary-row .dropdownbutton-up { transform: rotate(180deg); animation: animation-open; animation-duration: 100ms; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.css new file mode 100644 index 0000000000000000000000000000000000000000..ad6dc8f2444266c9f8b7fd79ad3da630df61288d --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.css @@ -0,0 +1,42 @@ +/* COLORS */ +/* Color palette interface (created with https://material.io/tools/color/) */ +/* font color */ +/* font color */ +/* Data colors */ +.toolbar-top { + background-color: #4a6889; + color: #ffffff; + padding: 5px 10px; + /* Note: same padding as ResponsiveGridLayout */ + width: 100%; + overflow: visible; } + +.toolbar-top { + font-weight: 500; } + +.toolbar-top .btn-info:not(:disabled):not(.disabled).active, +.sts-toolbar .btn-info:not(:disabled):not(.disabled).active, +.toolbar-top .btn-info:not(:disabled):not(.disabled):active, +.toolbar-top .show > .btn-info.dropdown-toggle { + color: white; + background-color: #86868a; + border-color: #86868a; } + +.toolbar-top .btn-info, +.sts-toolbar .btn-info { + color: white; + background-color: #b6b6ba; + border-color: #b6b6ba; } + +.toolbar-top .btn-info:hover, +.sts-toolbar .btn-info:hover { + color: white; + background-color: #86868a; + border-color: #86868a; } + +.react-datepicker-popper { + z-index: 2 !important; } + +.toolbar-ctrl { + display: inline-block; + margin-right: 1em; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.js new file mode 100644 index 0000000000000000000000000000000000000000..0ffb52a22ab4ae8ffd5ebed86e0b3cdf08d0d938 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.js @@ -0,0 +1,417 @@ +import React, {Component} from 'react'; +import {connect} from "react-redux"; +import {Button, ButtonGroup, Input} from 'reactstrap'; +import { + setDateRange, + setTestType, + setErrorTypes, + setStationGroup, + setErrorsOnly, + setPeriod, setAntennaID, + setAntennaType +} from "../redux/actions/mainFiltersActions"; +import {stationTypeFromName} from "../utils/LOFARDefinitions"; +// History handling +import { push } from 'connected-react-router'; +import { store } from "../redux/store.js"; + +import DatePicker from 'react-datepicker'; +import moment from 'moment'; +import MultiSelectDropdown from '../components/MultiSelectDropdown.js' +import {AntennaIdsPerTypeStationType} from '../utils/LOFARDefinitions.js' +// CSS +import './Toolbar.css' +import 'react-datepicker/dist/react-datepicker.css'; + + +/** + * Class to display a secondary header for selecting data filters. + * The state is managed by the LandingPage class. + */ +class DateRangeSelectorC extends Component { + + handleChange(obj) { + var startDate = obj.startDate, + endDate = obj.endDate; + + startDate = startDate || this.props.startDate; + endDate = endDate || this.props.endDate; + + if (startDate.isAfter(endDate)) { + endDate = startDate; + } + + this.props.setDateRange({ + startDate: startDate, + endDate: endDate + }); + }; + + handleChangeStart = (startDate) => { + return this.handleChange({ + startDate: startDate + }); + }; + + handleChangeEnd = (endDate) => { + return this.handleChange({ + endDate: endDate + }); + }; + + onPeriodClick = (i) => { + this.handleChange({ + startDate: moment().subtract(i, 'days'), + endDate: moment() + }); + + } + + periodIsActive(ndays) { + let now = moment().format("YYYY-MMM-DD"), + past = moment().subtract(ndays, 'days').format("YYYY-MMM-DD"); + if (this.props.endDate.format("YYYY-MMM-DD") === now && this.props.startDate.format("YYYY-MMM-DD") === past) { + return true; + } + return false; + } + + render() { + return (<div className="toolbar-ctrl"> + <label>Period </label> + <ButtonGroup size="sm"> + <Button color="info" onClick={() => this.onPeriodClick(7)} active={this.periodIsActive(7)}>1 wk</Button> + <Button color="info" onClick={() => this.onPeriodClick(14)} active={this.periodIsActive(14)}>2 wk</Button> + <Button color="info" onClick={() => this.onPeriodClick(28)} active={this.periodIsActive(28)}>4 wk</Button> + </ButtonGroup> + + <div style={{display: 'inline-block', width: '7em'}}> + <DatePicker + selected={this.props.startDate} + selectsStart + dateFormat="YYYY-MMM-DD" + className='form-control form-control-sm' + startDate={this.props.startDate} + endDate={this.props.endDate} + onChange={this.handleChangeStart} + /> + </div> + + <div style={{display: 'inline-block', width: '7em'}}> + <DatePicker + selected={this.props.endDate} + selectsEnd + dateFormat="YYYY-MMM-DD" + className='form-control form-control-sm' + startDate={this.props.startDate} + endDate={this.props.endDate} + onChange={this.handleChangeEnd} + /> + </div> + </div>); + } +} + +const mapStateDateRangeSelector = state => { + return { + startDate: state.mainFilters.startDate, + endDate: state.mainFilters.endDate + }; +}; + +const mapDispatchDateRangeSelector = { + setDateRange +}; + +export const DateRangeSelector = connect(mapStateDateRangeSelector, mapDispatchDateRangeSelector)(DateRangeSelectorC); + + + +/** + * Radio buttons for Test Type + */ +class TestTypeSelectorC extends Component { + + onTestTypeClick(type) { + this.props.setTestType(type); + } + + render() { + return (<div className="toolbar-ctrl"> + <label>Type </label> + <ButtonGroup size="sm"> + <Button color="info" onClick={() => this.onTestTypeClick('B')} active={this.props.testType === 'B'}>BOTH</Button> + <Button color="info" onClick={() => this.onTestTypeClick('S')} active={this.props.testType === 'S'}>ST-TEST</Button> + <Button color="info" onClick={() => this.onTestTypeClick('R')} active={this.props.testType === 'R'}>RTSM</Button> + </ButtonGroup> + </div>); + } +} + +const mapStateTestTypeSelector = state => { + return { + testType: state.mainFilters.testType + }; +}; + +const mapDispatchTestTypeSelector = { + setTestType +}; + +export const TestTypeSelector = connect(mapStateTestTypeSelector, mapDispatchTestTypeSelector)(TestTypeSelectorC); + + +/** + * Radio buttons for Antenna Type + */ +class AntennaTypeSelectorC extends Component { + + onAntennaTypeClick = (e) => { + let type = e.currentTarget.innerHTML; + //this.props.setAntennaType(type); + + store.dispatch(push(`?antenna_type=${type}`)) + } + + render() { + return (<div className="toolbar-ctrl"> + <label>Type </label> + <ButtonGroup size="sm"> + <Button color="info" onClick={this.onAntennaTypeClick} active={this.props.antennaType === 'HBA'}>HBA</Button> + <Button color="info" onClick={this.onAntennaTypeClick} active={this.props.antennaType === 'LBH'}>LBH</Button> + <Button color="info" onClick={this.onAntennaTypeClick} active={this.props.antennaType === 'LBL'}>LBL</Button> + </ButtonGroup> + </div>); + } +} + +const mapStateAntennaTypeSelector = state => { + return { + antennaType: state.mainFilters.antennaType + }; +}; + +const mapDispatchAntennaTypeSelector = { + setAntennaType +}; + +export const AntennaTypeSelector = connect(mapStateAntennaTypeSelector, mapDispatchAntennaTypeSelector)(AntennaTypeSelectorC); + + +/** + * Radio buttons for Antenna Id + */ +class AntennaIdSelectorC extends Component { + + antennaIdList(type, stationType){ + const antennaIdRange = AntennaIdsPerTypeStationType[stationType][type]; + let options = []; + for(let i=antennaIdRange.start; i<antennaIdRange.end; i++){ + options.push(<option key={i} value={i}>{i}</option>); + } + return options + } + onSelectedAntenna = (e) => { + //this.props.setAntennaID(Number(e.target.value)); + store.dispatch(push(`?antenna_id=${e.target.value}`)) + }; + + render() { + const stationType = stationTypeFromName(this.props.selectedStation); + + const options = this.antennaIdList(this.props.antennaType, stationType); + + return ( + <div className="toolbar-ctrl"> + <label>Antenna id </label> + <Input type="select" + value={this.props.antennaId} + onChange={this.onSelectedAntenna} + bsSize="sm" + style={{display:"inline-block", width:"auto"}}> + {options} + </Input> + </div>); + } + +} + +const mapStateAntennaIdSelector = state => { + return { + selectedStation: state.mainFilters.selectedStation, + antennaId: state.mainFilters.antennaId, + antennaType: state.mainFilters.antennaType + }; +}; + +const mapDispatchAntennaIdSelector = { + setAntennaID +}; + +export const AntennaIdSelector = connect(mapStateAntennaIdSelector, mapDispatchAntennaIdSelector)(AntennaIdSelectorC); + + +/** + * Class to display a secondary header for selecting data filters. + * The state is managed by the LandingPage class. + */ +class ErrorTypesSelectorC extends Component { + + onSelectionErrorTypes = (errorTypes) => { + this.props.setErrorTypes(errorTypes) + } + + render() { + const errorTypes = this.props.errorTypes.map(item => ({value:item, label:item})) + + return (<div className="toolbar-ctrl"> + <label>Error type </label> + <MultiSelectDropdown + className="form-input" + placeHolder="All" + options={errorTypes} + selectedItems={this.props.selectedErrorTypes} + onSelectionChange={this.onSelectionErrorTypes} + /> + </div>); + } +} + +const mapStateErrorTypesSelector = state => { + return { + selectedErrorTypes: state.mainFilters.selectedErrorTypes, + errorTypes: state.appInitData.errorTypes + }; +}; + +const mapDispatchErrorTypesSelector = { + setErrorTypes +}; + +export const ErrorTypesSelector = connect(mapStateErrorTypesSelector, mapDispatchErrorTypesSelector)(ErrorTypesSelectorC); + + + +/* + * + */ +class StationGroupSelectorC extends Component { + + onStationGroupChange = (e) => { + this.props.setStationGroup(e.target.value); + } + + render() { + return (<div className="toolbar-ctrl"> + <label htmlFor="selected-group">Station group </label> + <select className="form-control custom-select custom-select-sm" id="selected-group" + value={this.props.selectedStationGroup} + onChange={this.onStationGroupChange} + style={{ width: 'auto' }}> + <option value="A">All stations</option> + <option value="C">Core stations</option> + <option value="R">Remote stations</option> + <option value="I">ILT stations</option> + </select> + </div>); + } +} + +const mapStateStationGroupSelector = state => { + return { + selectedStationGroup: state.mainFilters.selectedStationGroup + }; +}; + +const mapDispatchStationGroupSelector = { + setStationGroup +}; + +export const StationGroupSelector = connect(mapStateStationGroupSelector, mapDispatchStationGroupSelector)(StationGroupSelectorC); + + +/* + * Toggle button for "errorsOnly" + */ +class ErrorsOnlySelectorC extends Component { + + onErrorsOnlyClick = () => { + this.props.setErrorsOnly(!this.props.errorsOnly); + } + + render() { + return ( + <div className="toolbar-ctrl"> + <Button color="info" size="sm" onClick={this.onErrorsOnlyClick} active={this.props.errorsOnly}> + Errors only + </Button> + </div>); + } +} + +const mapStateErrorsOnlySelector = state => { + return { + errorsOnly: state.mainFilters.errorsOnly + }; +}; + +const mapDispatchErrorsOnlySelector = { + setErrorsOnly +}; + +export const ErrorsOnlySelector = connect(mapStateErrorsOnlySelector, mapDispatchErrorsOnlySelector)(ErrorsOnlySelectorC); + + + +/** + * Class to display a secondary header for selecting data filters. + * The state is managed by the LandingPage class. + */ +class PeriodSelectorC extends Component { + + onPeriodClick(i) { + this.props.setPeriod(i); + } + + render() { + + return (<div className="toolbar-ctrl"> + <label>Period </label> + <ButtonGroup size="sm"> + <Button color="info" onClick={() => this.onPeriodClick(7)} active={this.props.period === 7}>1 wk</Button> + <Button color="info" onClick={() => this.onPeriodClick(14)} active={this.props.period === 14}>2 wk</Button> + <Button color="info" onClick={() => this.onPeriodClick(21)} active={this.props.period === 21}>3 wk</Button> + <Button color="info" onClick={() => this.onPeriodClick(28)} active={this.props.period === 28}>4 wk</Button> + </ButtonGroup> + </div>); + } +} + +const mapStatePeriodSelector = state => { + return { + period: state.mainFilters.period + }; +}; + +const mapDispatchPeriodSelector = { + setPeriod +}; + +export const PeriodSelector = connect(mapStatePeriodSelector, mapDispatchPeriodSelector)(PeriodSelectorC); + + + +/* + * Toolbar: the wrapper for all toolbar components. Use as: + * <Toolbar> + * <StationAutoComplete /> + * <TestTypeSelector /> + * <..> + * </Toolbar> + */ +export function Toolbar(props) { + + return (<div className="toolbar-top"> + {props.children} + </div>); +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..050a6d5954be21965c598b77519a4a12954eda6a --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/Toolbar.scss @@ -0,0 +1,44 @@ +@import '../themes/lofar-variables.scss'; + + +.toolbar-top { + background-color: $primary-light; + color: $primary-color; + padding: 5px 10px; /* Note: same padding as ResponsiveGridLayout */ + width: 100%; + overflow: visible; +} + +.toolbar-top { + font-weight: 500; +} +.toolbar-top .btn-info:not(:disabled):not(.disabled).active, +.sts-toolbar .btn-info:not(:disabled):not(.disabled).active, +.toolbar-top .btn-info:not(:disabled):not(.disabled):active, +.toolbar-top .show>.btn-info.dropdown-toggle { + color: $secondary-color; + background-color: $secondary-dark; + border-color: $secondary-dark; +} + +.toolbar-top .btn-info, +.sts-toolbar .btn-info { + color: $secondary-color; + background-color: $secondary; + border-color: $secondary; +} + +.toolbar-top .btn-info:hover, +.sts-toolbar .btn-info:hover { + color: $secondary-color; + background-color: $secondary-dark; + border-color: $secondary-dark; +} +.react-datepicker-popper { + z-index: 2 !important; +} + +.toolbar-ctrl { + display: inline-block; + margin-right: 1em; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.css index 7af89cd86a7cd3bcc2903f84a096f640893668b3..504f1c3657d8fc1676916b406bac7dd4679edd07 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.css @@ -3,11 +3,16 @@ /* font color */ /* font color */ /* Data colors */ -.header { - background-color: #773b6f; - padding: 20px; +.header-navbar.navbar { + background-color: #1c3e5c; + padding: .3rem 1rem; color: white; } -.lofar { +.header-brand em { font-weight: bold; font-style: normal; } + +.header-brand span { + color: #86868a; + font-size: 1.5rem; + font-style: italic; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.js index 33aa07d52c2a20e68c69e6978e155e3ddca1ae7b..bc1daa61bf8fb50dd1da8380f4ee3d4d84b773b5 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import './header.css' import { Link } from 'react-router-dom'; import { Collapse, Nav, @@ -9,52 +8,54 @@ import { Collapse, NavItem, NavLink } from 'reactstrap'; +// CSS +import './header.scss' + class Header extends Component { - constructor(props){ - super(props); - this.state = { - isOpen: false - }; - } - - toggle(){ - this.setState({ - isOpen:!this.state.isOpen + + state = { + isOpen: false + } + + toggle = () => { + this.setState({ + isOpen:!this.state.isOpen }); - } + } + + check_status(item){ + //console.log(this.props.active_page.pathname) + if (this.props.active_page.pathname === item){ + return "active" + } + } - check_status(item){ - //console.log(this.props.active_page.pathname) - if (this.props.active_page.pathname === item){ - return "active" + render() { + return ( + <Navbar className="header-navbar" dark expand="lg"> + <NavbarBrand className='header-brand'><h1><em>LOFAR</em> Station monitor <span>v0.1</span></h1></NavbarBrand> + <NavbarToggler onClick={this.toggle} /> + <Collapse isOpen={this.state.isOpen} navbar> + <Nav className="ml-auto" navbar> + <NavItem className={this.check_status("/")}> + <NavLink tag={Link} to="/"><h3>Dashboard</h3></NavLink> + </NavItem> + <NavItem className={this.check_status("/station_overview")}> + <NavLink tag={Link} to="/station_overview"><h3>Station Overview</h3></NavLink> + </NavItem> + <NavItem className={this.check_status("/tiles")}> + <NavLink tag={Link} to="/tiles"><h3>Tiles</h3></NavLink> + </NavItem> + { /*} + <NavItem className={this.check_status("/details")}> + <NavLink tag={Link} to="/details"><h3>Details</h3></NavLink> + </NavItem> + */ } + </Nav> + </Collapse> + </Navbar>); } - } - - render() { - return ( - <Navbar className="header" dark expand="lg"> - <NavbarBrand><h1><em className='lofar'>LOFAR</em> Station monitor</h1></NavbarBrand> - <NavbarToggler onClick={() => this.toggle()} /> - <Collapse isOpen={this.state.isOpen} navbar> - <Nav className="ml-auto" navbar> - <NavItem className={this.check_status("/")}> - <NavLink tag={Link} to="/"><h3>Dashboard</h3></NavLink> - </NavItem> - <NavItem className={this.check_status("/station_overview")}> - <NavLink tag={Link} to="/station_overview"><h3>Station Overview</h3></NavLink> - </NavItem> - <NavItem className={this.check_status("/tiles")}> - <NavLink tag={Link} to="/tiles"><h3>Tiles</h3></NavLink> - </NavItem> - <NavItem className={this.check_status("/details")}> - <NavLink tag={Link} to="/details"><h3>Details</h3></NavLink></NavItem> - - </Nav> - </Collapse> - </Navbar> - ); - } } export default Header; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.scss index 941c3a6d21574df1e2cbd90638a11f6e56c4d66f..f0e366076c6e65a817143c04335f09578bab6f08 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.scss +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/header.scss @@ -1,12 +1,18 @@ @import '../themes/lofar-variables.scss'; -.header { +.header-navbar.navbar { background-color: $primary; - padding: 20px; + padding: .3rem 1rem; color: white; } -.lofar { +.header-brand em { font-weight: bold; font-style: normal; } + +.header-brand span { + color: $secondary-dark; + font-size: 1.5rem; + font-style: italic; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js index c0afbcd0c9d77392f0c351aef6c110b74cf64f5f..fab99a8ad74b55a98f47719114ace6126ae3ef7f 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js @@ -7,16 +7,14 @@ import LatestObservations from '../components/LatestObservations.js'; import StationStatistics from '../components/StationStatistics.js'; import {Responsive, WidthProvider} from 'react-grid-layout'; -import {Button, ButtonGroup, Form} from 'reactstrap'; import * as moment from 'moment'; -import {connect} from "react-redux"; +import { connect } from "react-redux"; import { setNewLayout } from "../redux/actions/landingPageActions"; -import { setPeriod, setStationGroup, setErrorsOnly, setErrorTypes } from "../redux/actions/mainFiltersActions"; import { composeQueryString } from '../utils/utils.js'; import { createGridPanel } from '../utils/grid.js'; +import { Toolbar, StationGroupSelector, ErrorTypesSelector, ErrorsOnlySelector, PeriodSelector } from '../components/Toolbar' -import MultiSelectDropdown from '../components/MultiSelectDropdown.js' import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import '../themes/lofar-styles.css'; @@ -24,87 +22,6 @@ import '../themes/lofar-styles.css'; const ResponsiveGridLayout = WidthProvider(Responsive); -/** - * Class to display a secondary header for selecting data filters. - * The state is managed by the LandingPage class. - */ -class ToolBarC extends Component { - - onErrorsOnlyClick(selected) { - this.props.setErrorsOnly(!this.props.errorsOnly); - } - - onStationGroupChange(e) { - //this.props.onChange('selectedStationGroup', e.target.value) - this.props.setStationGroup(e.target.value); - } - - onPeriodClick(i) { - this.props.setPeriod(i); - } - - onSelectionErrorTypes(errorTypes) { - this.props.setErrorTypes(errorTypes) - } - - render() { - - const errorTypes = this.props.errorTypes.map(item => ({value:item, label:item})) - - return (<div className="toolbar-top"> - <Form inline> - <label htmlFor="selected-group">Station group </label> - <select className="form-control custom-select custom-select-sm" id="selected-group" value={this.props.selectedStationGroup} onChange={(e) => this.onStationGroupChange(e)} style={{ - width: 'auto' - }}> - <option value="A">All stations</option> - <option value="C">Core stations</option> - <option value="R">Remote stations</option> - <option value="I">ILT stations</option> - </select> - { - this.props.errorsOnly - ? <Button color="info" size="sm" onClick={() => this.onErrorsOnlyClick()} active="active">Errors only</Button> - : <Button color="info" size="sm" onClick={() => this.onErrorsOnlyClick()}>Errors only</Button> - } - - <label>Period </label> - <ButtonGroup size="sm"> - <Button color="info" onClick={() => this.onPeriodClick(7)} active={this.props.period === 7}>1 wk</Button> - <Button color="info" onClick={() => this.onPeriodClick(14)} active={this.props.period === 14}>2 wk</Button> - <Button color="info" onClick={() => this.onPeriodClick(21)} active={this.props.period === 21}>3 wk</Button> - <Button color="info" onClick={() => this.onPeriodClick(28)} active={this.props.period === 28}>4 wk</Button> - </ButtonGroup> - - <label>Error type </label> - <MultiSelectDropdown - className="form-input" - placeHolder="All" - options={errorTypes} - selectedItems={this.props.selectedErrorTypes} - onSelectionChange={(e)=> this.onSelectionErrorTypes(e)}/> - </Form> - </div>); - } -} - -const mapStateToPropsToolBar = state => { - return { - ...state.mainFilters, - ...state.appInitData - }; -}; - -const mapDispatchToPropsToolBar = { - setStationGroup, - setErrorsOnly, - setPeriod, - setErrorTypes, -}; - -const ToolBar = connect(mapStateToPropsToolBar, mapDispatchToPropsToolBar)(ToolBarC); - - class LandingPageC extends Component { getStationOverviewURL() { @@ -178,10 +95,16 @@ class LandingPageC extends Component { return `${url}&${parametersString}`; } + render() { return (<div> <Header active_page={this.props.location}/> - <ToolBar/> + <Toolbar> + <StationGroupSelector /> + <ErrorsOnlySelector /> + <PeriodSelector /> + <ErrorTypesSelector /> + </Toolbar> <ResponsiveGridLayout className="layout" layouts={this.props.layout.panels} measureBeforeMount={true} breakpoints={this.props.layout.breakpoints} cols={this.props.layout.cols} onResizeStop={this.props.setNewLayout}> diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js index d4f9413032a72a22b52291e8154d3ebfdf3ec46f..36cdae18940b23258fb11bd201dd12cd685ed968 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js @@ -1,209 +1,98 @@ import React, { Component } from 'react'; import {connect} from "react-redux"; -import {Alert, Button, ButtonGroup, Container, Row, Col} from 'reactstrap'; -import { setDateRange, setTestType, setStation, setErrorTypes } from "../redux/actions/mainFiltersActions"; -import DatePicker from 'react-datepicker'; +import {Alert, Container, Row, Col} from 'reactstrap'; import moment from 'moment'; import Header from '../components/header.js' import StationAutoComplete from '../components/StationAutoComplete'; import StationTestView from '../components/StationTestView'; import StationTestChildView from '../components/StationTestChildView'; -import MultiSelectDropdown from '../components/MultiSelectDropdown.js' import { composeQueryString } from '../utils/utils.js'; +import { Toolbar, DateRangeSelector, TestTypeSelector, ErrorTypesSelector } from '../components/Toolbar' -// History handling -import { push } from 'connected-react-router'; -import { store } from "../redux/store.js"; - -// CSS -import 'react-datepicker/dist/react-datepicker.css'; - - -/** - * Class to display a secondary header for selecting data filters. - * The state is managed by the LandingPage class. +/* + * Display an Alert */ -class ToolBarC extends Component { +function ErrorAlert({doDisplay, message}){ - constructor(props) { - super(props); - this.handleChangeStart = this.handleChangeStart.bind(this); - this.handleChangeEnd = this.handleChangeEnd.bind(this); - this.onStationChange = this.onStationChange.bind(this); + if (! doDisplay) { + return null; } - handleChange(obj) { - var startDate = obj.startDate, - endDate = obj.endDate; - - startDate = startDate || this.props.startDate; - endDate = endDate || this.props.endDate; - - if (startDate.isAfter(endDate)) { - endDate = startDate; - } - - this.props.setDateRange({ - startDate: startDate, - endDate: endDate - }); - }; - - handleChangeStart(startDate) { - return this.handleChange({ - startDate: startDate - }); - }; - - handleChangeEnd(endDate) { - return this.handleChange({ - endDate: endDate - }); - }; - - onPeriodClick(i) { - this.handleChange({ - startDate: moment().subtract(i, 'days'), - endDate: moment() - }); - - } - - onTestTypeClick(type) { - this.props.setTestType(type); - } + // The 10px is the margin that ResponsiveGridLayout uses + return ( + <Alert style={{margin: '10px'}} color="warning"> + {message} + </Alert> + ); +} - // Callback for StationAutoComplete - onStationChange(station) { - //this.props.setStation(station); - store.dispatch(push(`?station=${station}`)) - } +/* + * Display the page body (below the header) + */ +function PageBody({doDisplay, url}) { - onSelectionErrorTypes(errorTypes) { - this.props.setErrorTypes(errorTypes) + if (! doDisplay) { + return null; } - render() { - const errorTypes = this.props.errorTypes.map(item => ({value:item, label:item})) - // The key on StationAutoComplete is important, see the desc of StationAutoComplete - return (<div className="toolbar-top"> - <StationAutoComplete key={this.props.selectedStation} selectedStation={this.props.selectedStation} onChange={this.onStationChange}/> - - <label>Type </label> - <ButtonGroup size="sm"> - <Button color="info" onClick={() => this.onTestTypeClick('B')} active={this.props.testType === 'B'}>BOTH</Button> - <Button color="info" onClick={() => this.onTestTypeClick('S')} active={this.props.testType === 'S'}>ST-TEST</Button> - <Button color="info" onClick={() => this.onTestTypeClick('R')} active={this.props.testType === 'R'}>RTSM</Button> - </ButtonGroup> - - <label>Period </label> - <ButtonGroup size="sm"> - <Button color="info" onClick={() => this.onPeriodClick(7)}>1 wk</Button> - <Button color="info" onClick={() => this.onPeriodClick(14)}>2 wk</Button> - <Button color="info" onClick={() => this.onPeriodClick(28)}>4 wk</Button> - </ButtonGroup> - - <div style={{display: 'inline-block', width: '7em'}}> - <DatePicker - selected={this.props.startDate} - selectsStart - dateFormat="YYYY-MMM-DD" - className='form-control form-control-sm' - startDate={this.props.startDate} - endDate={this.props.endDate} - onChange={this.handleChangeStart} - /> - </div> - - <div style={{display: 'inline-block', width: '7em'}}> - <DatePicker - selected={this.props.endDate} - selectsEnd - dateFormat="YYYY-MMM-DD" - className='form-control form-control-sm' - startDate={this.props.startDate} - endDate={this.props.endDate} - onChange={this.handleChangeEnd} - /> - </div> - - <label>Error type </label> - <MultiSelectDropdown - className="form-input" - placeHolder="All" - options={errorTypes} - selectedItems={this.props.selectedErrorTypes} - onSelectionChange={(e)=> this.onSelectionErrorTypes(e)}/> - </div>); - } + return ( + <Container fluid={true}> + <Row> + <Col md="8" className="col-padding"> + <StationTestView url={url} /> + </Col> + <Col md="4" className="col-padding"> + <StationTestChildView /> + </Col> + </Row> + </Container> + ); } -const mapStateToPropsToolBar = state => { - return { - ...state.mainFilters, - ...state.appInitData - }; -}; - -const mapDispatchToPropsToolBar = { - setTestType, - setDateRange, - setStation, - setErrorTypes, -}; - -const ToolBar = connect(mapStateToPropsToolBar, mapDispatchToPropsToolBar)(ToolBarC); class StationOverviewPageC extends Component { - getStationSummaryURL(){ - const parameters = {}; + getStationSummaryURL() { if (this.isParameterMissing()) { - return ""; + return ''; } - parameters.station_name = this.props.selectedStation; - parameters.test_type = this.props.testType; - parameters.from_date = moment(this.props.startDate).format('YYYY-MM-DD'); - parameters.to_date = moment(this.props.endDate).format('YYYY-MM-DD'); - parameters.error_types = this.props.selectedErrorTypes - const baseURL = '/api/view/ctrl_station_component_errors?format=json'; + const parameters = { + format: 'json', + station_name: this.props.selectedStation, + test_type: this.props.testType, + from_date: moment(this.props.startDate).format('YYYY-MM-DD'), + to_date: moment(this.props.endDate).format('YYYY-MM-DD'), + error_types: this.props.selectedErrorTypes + }; + + const baseURL = '/api/view/ctrl_station_component_errors'; const queryString = composeQueryString(parameters); - return `${baseURL}&${queryString}` - } - isParameterMissing(){ - const stationName = this.props.selectedStation - return stationName === undefined || - stationName === "" + return `${baseURL}?${queryString}` } - renderErrorIfParameterMissing(){ - if (this.isParameterMissing()){ - // The 10px is the margin that ResponsiveGridLayout uses - return <Alert style={{margin: '10px'}} color="warning">Please select a station</Alert> - }else { - return "" - } + isParameterMissing(){ + const stationName = this.props.selectedStation; + return stationName === undefined || stationName === ""; } - setNewLayout() {} render() { + const parmMissing = this.isParameterMissing(); + return ( <React.Fragment> <Header active_page={this.props.location} /> - <ToolBar/> - {this.renderErrorIfParameterMissing()} - { this.isParameterMissing() ? "" : - <Container fluid={true} style={{padding: '10px'}}> - <Row> - <Col md="8"><StationTestView url={this.getStationSummaryURL()} /></Col> - <Col md="4"><StationTestChildView /></Col> - </Row> - </Container> - } + <Toolbar> + <StationAutoComplete /> + <TestTypeSelector /> + <DateRangeSelector /> + <ErrorTypesSelector /> + </Toolbar> + <ErrorAlert doDisplay={parmMissing} message="Please select a station" /> + <PageBody doDisplay={!parmMissing} url={this.getStationSummaryURL()} /> </React.Fragment> ); } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.js index c9c5e05acd99d7f9536ced9fe62f678c92a2067b..43eabbd06801579f4a781667db46c262d5461728 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.js @@ -1,16 +1,84 @@ import React, { Component } from 'react'; import Header from '../components/header.js' +import moment from 'moment' +import {connect} from "react-redux"; +import StationAutoComplete from '../components/StationAutoComplete'; +import { Toolbar, DateRangeSelector, TestTypeSelector, AntennaTypeSelector, AntennaIdSelector } from '../components/Toolbar' +import {Alert, Container, Col, Row} from "reactstrap"; +import { composeQueryString } from '../utils/utils.js'; +import AntennaErrorDetails from '../components/AntennaErrorDetails' +import AntennaView from '../components/AntennaView'; + + +// Component to display an alert +function StationAlert(props) { + if (props.show) { + return <Alert style={{margin: '10px'}} color="warning">Please select a station</Alert>; + } + return null; +} + + +class TilesPageC extends Component { + + isParameterMissing(){ + const stationName = this.props.selectedStation + return stationName === undefined || + stationName === "" + } + + getTilesDataURL () { + const url = '/api/view/ctrl_station_component_element_errors?format=json'; + const parameters = {}; + // ---- Mandatory parameters + parameters.station_name = this.props.selectedStation; + // ---- Optional parameters + parameters.from_date = moment(this.props.startDate).format('YYYY-MM-DD'); + parameters.to_date = moment(this.props.endDate).format('YYYY-MM-DD'); + parameters.component_type = this.props.antennaType; + parameters.antenna_id = this.props.antennaId; + parameters.test_type = this.props.testType === 'B' ? 'A' : this.props.testType; + const parametersString = composeQueryString(parameters); + + return `${url}&${parametersString}`; + } -class TilesPage extends Component { render() { + let parmMissing = this.isParameterMissing(), + url = this.getTilesDataURL(); + return ( <div> <Header active_page={this.props.location} /> - - <div>Tiles Overview</div> + <Toolbar> + <StationAutoComplete /> + <AntennaTypeSelector /> + <AntennaIdSelector /> + <TestTypeSelector /> + <DateRangeSelector /> + </Toolbar> + <StationAlert show={parmMissing} /> + { parmMissing ? null : + <Container fluid={true}> + <Row> + <Col md="8" className="col-padding"> + <AntennaView url={url}/> + </Col> + <Col md="4" className="col-padding"> + <AntennaErrorDetails/> + </Col> + </Row> + </Container> + } </div> ); } } +const TilesPage = connect(state => { + return { + ...state.mainFilters + }; +})(TilesPageC); + export default TilesPage; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/antennaOverviewPageActions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/antennaOverviewPageActions.js new file mode 100644 index 0000000000000000000000000000000000000000..fc03517f386bf672129c2d64801d01b1085c760a --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/antennaOverviewPageActions.js @@ -0,0 +1,12 @@ +export const SELECT_ANTENNA_ERROR = "SELECT_ANTENNA_ERROR"; +export const PIN_ANTENNA_ERROR = "PIN_ANTENNA_ERROR"; + +export const selectAntennaError = antenna_error_data => ({ + type: SELECT_ANTENNA_ERROR, + payload: { antenna_error_data } +}); + +export const pinAntennaError = is_pin => ({ + type: PIN_ANTENNA_ERROR, + payload: { is_pin } +}); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/appInitDataActions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/appInitDataActions.js index 674e6fc9a4e89beebaffeac447bee549594c82b7..5c546aeb7513e972c6a1e6619c007d3d60a2631d 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/appInitDataActions.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/appInitDataActions.js @@ -38,7 +38,7 @@ export function fetchErrorTypes() { }) .catch(error => { console.log("Error fetching error types: "+error); - dispatch(fetchErrorTypesFailure(error)) + dispatch(fetchErrorTypesFailure(error)); // Try again in 30s setTimeout(() => dispatch(fetchErrorTypes()), 10000); }); @@ -67,7 +67,6 @@ export const fetchStationsFailure = error => ({ export function fetchStations() { return dispatch => { // Not used: dispatch(fetchstationsBegin()); - return axios.get(stationsURL) .then(res => { let stations = res.data.results ? res.data.results : []; @@ -76,7 +75,7 @@ export function fetchStations() { }) .catch(error => { console.log("Error fetching error types: "+error); - dispatch(fetchStationsFailure(error)) + dispatch(fetchStationsFailure(error)); // Try again in 30s setTimeout(() => dispatch(fetchStations()), 10000); }); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/mainFiltersActions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/mainFiltersActions.js index c6bdd70742fac2f84e369d47e91ea19ce60c4aaf..bcc42b5f4c6ef1a25b35718db6f017aebc3d5003 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/mainFiltersActions.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/mainFiltersActions.js @@ -1,4 +1,3 @@ - export const SET_STATION_GROUP = "SET_STATION_GROUP"; export const SET_STATION = "SET_STATION"; export const SET_ERRORS_ONLY = "SET_ERRORS_ONLY"; @@ -6,6 +5,18 @@ export const SET_PERIOD = "SET_PERIOD"; export const SET_DATE_RANGE = "SET_DATE_RANGE"; export const SET_TEST_TYPE = "SET_TEST_TYPE"; export const SET_ERROR_TYPES = "SET_ERROR_TYPES"; +export const SET_ANTENNA_TYPE = "SET_ANTENNA_TYPE"; +export const SET_ANTENNA_ID = "SET_ANTENNA_ID"; + +export const setAntennaID = antennaId => ({ + type: SET_ANTENNA_ID, + payload: { antennaId } +}); + +export const setAntennaType = antennaType => ({ + type: SET_ANTENNA_TYPE, + payload: { antennaType } +}); export const setStationGroup = stationGroup => ({ type: SET_STATION_GROUP, diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/antennaOverviewPageReducers.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/antennaOverviewPageReducers.js new file mode 100644 index 0000000000000000000000000000000000000000..2854320b7225121e1f4b1bc23b79d5f21b388ec7 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/antennaOverviewPageReducers.js @@ -0,0 +1,25 @@ +import { SELECT_ANTENNA_ERROR, + PIN_ANTENNA_ERROR } from '../actions/antennaOverviewPageActions' + +const initialState = { + main_panel : {}, + child_panel : {isPinned: false} +}; + + +export default function(state = initialState, action) { + let newState = {...state}; + switch (action.type) { + + case SELECT_ANTENNA_ERROR: + newState.main_panel = action.payload; + return newState; + + case PIN_ANTENNA_ERROR: + newState.child_panel.isPinned = action.payload.is_pin; + return newState; + + default: + return newState + } +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/index.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/index.js index c877894fe8e09a03c9d9c61a170745bdd13df88b..8e93f1fb02b34c97ac319c0185ec2f8421a7bf14 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/index.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/index.js @@ -3,10 +3,11 @@ import appInitData from "./appInitDataReducers"; import mainFilters from "./mainFilters"; import landingPageReducers from "./landingPageReducers"; import stationOverviewPageReducers from "./stationOverviewPageReducers"; - +import antennaOverviePageReducers from "./antennaOverviewPageReducers"; export default combineReducers({ appInitData, mainFilters, landing_page: landingPageReducers, - station_page: stationOverviewPageReducers + station_page: stationOverviewPageReducers, + antenna_page: antennaOverviePageReducers }); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/mainFilters.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/mainFilters.js index ef52320124f903e15fc190f904b747a0614ea1c8..61be6786f9f436db6650d9ffc4fd7a63a23dde64 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/mainFilters.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/mainFilters.js @@ -11,24 +11,42 @@ const initialState = { period: 7, // days startDate: moment().subtract(7, 'days'), endDate: moment(), - selectedErrorTypes: [] + selectedErrorTypes: [], + antennaId: 0, + antennaType: 'HBA', }; export default function(state = initialState, action) { + switch (action.type) { case LOCATION_CHANGE: { // Location changed, check is a station parameter is present let newState = { ...state }; - const values = queryString.parse( action.payload.location.search); + const values = queryString.parse(action.payload.location.search); if ('station' in values) { newState.selectedStation = values.station; } + if ('antenna_id' in values){ + newState.antennaId = values.antenna_id; + } + if('antenna_type' in values){ + newState.antennaType = values.antenna_type; + } + return newState; + } + case a.SET_ANTENNA_ID: { + let newState = {...state}; + newState.antennaId = action.payload.antennaId; + return newState; + } + case a.SET_ANTENNA_TYPE: { + let newState = {...state}; + newState.antennaType = action.payload.antennaType; return newState; } - case a.SET_STATION_GROUP: { const { stationGroup } = action.payload; return { diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.css index f48fba4d3e0dd6e764adb80dd595bbfcca26117b..3a0c2bded0cb8179f7727639a6d37e2cfcd3716c 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.css @@ -4,14 +4,18 @@ /* font color */ /* font color */ /* Data colors */ +/* For alt text */ +img { + font-weight: 400 !important; } + .react-grid-item { background-color: white; - border: 1px solid #e1e1e1; + border: 1px solid #e8e8ec; border-radius: 0.25rem; } .react-grid-item-header { - color: #8d8d8d; - background-color: #e1e1e1; + color: #86868a; + background-color: #e8e8ec; font-weight: bold; padding: .3rem; /* Note: must be same as the Bootstrap table for nice alignment */ @@ -36,8 +40,8 @@ top: 2.25em; /* Check with height of .react-grid-item-header */ left: 0.25em; - border: 0.55rem solid #e1e1e1; - border-top: 0.55rem solid #a7689d; + border: 0.55rem solid #e8e8ec; + border-top: 0.55rem solid #4a6889; border-radius: 50%; width: 3rem; height: 3rem; @@ -66,41 +70,10 @@ text-align: center; font-weight: 700; } -.toolbar-top { - background-color: #a7689d; - color: #ffffff; - padding: 5px 10px; - /* Note: same padding as ResponsiveGridLayout */ - width: 100%; - overflow: visible; } - -.toolbar-top { - font-weight: 500; } - -.toolbar-top .btn-info:not(:disabled):not(.disabled).active, -.sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.toolbar-top .btn-info:not(:disabled):not(.disabled):active, -.toolbar-top .show > .btn-info.dropdown-toggle { - color: white; - background-color: #8d8d8d; - border-color: #8d8d8d; } - -.toolbar-top .btn-info, -.sts-toolbar .btn-info { - color: white; - background-color: #bdbdbd; - border-color: #bdbdbd; } - -.toolbar-top .btn-info:hover, -.sts-toolbar .btn-info:hover { - color: white; - background-color: #8d8d8d; - border-color: #8d8d8d; } - .tooltip > .tooltip-inner { background-color: white !important; color: black !important; - border: 1px solid #8d8d8d; } + border: 1px solid #86868a; } .react-select-container { min-width: 20rem; } @@ -115,4 +88,8 @@ border: none; } .form-input button:hover { - background-color: #e1e1e1 !important; } + background-color: #e8e8ec !important; } + +/* Padding on column in bootstrap grid */ +.row .col-padding { + padding-top: 15px; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.scss index ae80678c68f190303bffbcba143823077efa66c9..a06cfc16b5fc08b0b54b81b4d4e37317f9cd0a84 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.scss +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-styles.scss @@ -6,6 +6,11 @@ body { //background-color: #E1E2E1!important; } +/* For alt text */ +img { + font-weight: 400!important; +} + $griditem: $secondary-light; $griditem-color: $secondary-dark; @@ -74,40 +79,6 @@ $griditem-color: $secondary-dark; font-weight: 700; } -.toolbar-top { - background-color: $primary-light; - color: $primary-color; - padding: 5px 10px; /* Note: same padding as ResponsiveGridLayout */ - width: 100%; - overflow: visible; -} - -.toolbar-top { - font-weight: 500; -} -.toolbar-top .btn-info:not(:disabled):not(.disabled).active, -.sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.toolbar-top .btn-info:not(:disabled):not(.disabled):active, -.toolbar-top .show>.btn-info.dropdown-toggle { - color: $secondary-color; - background-color: $secondary-dark; - border-color: $secondary-dark; -} - -.toolbar-top .btn-info, -.sts-toolbar .btn-info { - color: $secondary-color; - background-color: $secondary; - border-color: $secondary; -} - -.toolbar-top .btn-info:hover, -.sts-toolbar .btn-info:hover { - color: $secondary-color; - background-color: $secondary-dark; - border-color: $secondary-dark; -} - .tooltip > .tooltip-inner { background-color: white !important; color: black !important; @@ -115,6 +86,7 @@ $griditem-color: $secondary-dark; border: 1px solid $secondary-dark; } + .react-select-container{ min-width: 20rem; } @@ -134,3 +106,9 @@ $griditem-color: $secondary-dark; .form-input button:hover { background-color: $secondary-light !important; } + + +/* Padding on column in bootstrap grid */ +.row .col-padding { + padding-top: 15px; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.scss index e868bb8726e852d3ae6fb804313824b8a3affa28..ce151724f088f43ac79938ed956baf1c4dd58cbb 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.scss +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.scss @@ -3,14 +3,14 @@ /* Color palette interface (created with https://material.io/tools/color/) */ -$primary: #773b6f; -$primary-light: #a7689d; -$primary-dark: #490f44; +$primary: #1c3e5c; //#773b6f; +$primary-light: #4a6889; // #a7689d; +$primary-dark: #001832; //#490f44; $primary-color: #ffffff; /* font color */ -$secondary: #bdbdbd; -$secondary-light: #e1e1e1; -$secondary-dark: #8d8d8d; +$secondary: #b6b6ba; //#bdbdbd; +$secondary-light: #e8e8ec; //#e1e1e1; +$secondary-dark: #86868a; //#8d8d8d; $secondary-color: white; /* font color */ diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css index a40b42bb57c95fc8e2eba5674461f641c72f07f1..aab891291b8281dacb619f0553f7465bc3fde7de 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css @@ -23,8 +23,8 @@ --white: #fff; --gray: #6c757d; --gray-dark: #343a40; - --primary: #773b6f; - --secondary: #bdbdbd; + --primary: #1c3e5c; + --secondary: #b6b6ba; --success: #28a745; --info: #17a2b8; --warning: #fbfb83; @@ -142,12 +142,12 @@ sup { top: -.5em; } a { - color: #773b6f; + color: #1c3e5c; text-decoration: none; background-color: transparent; -webkit-text-decoration-skip: objects; } a:hover { - color: #44223f; + color: #0a1621; text-decoration: underline; } a:not([href]):not([tabindex]) { @@ -1083,24 +1083,24 @@ pre { .table-primary, .table-primary > th, .table-primary > td { - background-color: #d9c8d7; } + background-color: #bfc9d1; } .table-hover .table-primary:hover { - background-color: #cfb9cc; } + background-color: #b0bcc6; } .table-hover .table-primary:hover > td, .table-hover .table-primary:hover > th { - background-color: #cfb9cc; } + background-color: #b0bcc6; } .table-secondary, .table-secondary > th, .table-secondary > td { - background-color: #ededed; } + background-color: #ebebec; } .table-hover .table-secondary:hover { - background-color: #e0e0e0; } + background-color: #dedee0; } .table-hover .table-secondary:hover > td, .table-hover .table-secondary:hover > th { - background-color: #e0e0e0; } + background-color: #dedee0; } .table-success, .table-success > th, @@ -1274,9 +1274,9 @@ pre { .form-control:focus { color: #495057; background-color: #fff; - border-color: #bb76b2; + border-color: #3a80be; outline: 0; - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .form-control::placeholder { color: #6c757d; opacity: 1; } @@ -1618,7 +1618,7 @@ textarea.form-control { text-decoration: none; } .btn:focus, .btn.focus { outline: 0; - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .btn.disabled, .btn:disabled { opacity: 0.65; } .btn:not(:disabled):not(.disabled) { @@ -1630,49 +1630,49 @@ fieldset:disabled a.btn { .btn-primary { color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .btn-primary:hover { color: #fff; - background-color: #5d2e57; - border-color: #552a4f; } + background-color: #132a3f; + border-color: #102435; } .btn-primary:focus, .btn-primary.focus { - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.5); } .btn-primary.disabled, .btn-primary:disabled { color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { color: #fff; - background-color: #552a4f; - border-color: #4c2647; } + background-color: #102435; + border-color: #0d1d2b; } .btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.5); } .btn-secondary { color: #212529; - background-color: #bdbdbd; - border-color: #bdbdbd; } + background-color: #b6b6ba; + border-color: #b6b6ba; } .btn-secondary:hover { color: #212529; - background-color: #aaaaaa; - border-color: #a4a4a4; } + background-color: #a2a2a7; + border-color: #9c9ca1; } .btn-secondary:focus, .btn-secondary.focus { - box-shadow: 0 0 0 0.2rem rgba(189, 189, 189, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(182, 182, 186, 0.5); } .btn-secondary.disabled, .btn-secondary:disabled { color: #212529; - background-color: #bdbdbd; - border-color: #bdbdbd; } + background-color: #b6b6ba; + border-color: #b6b6ba; } .btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { color: #212529; - background-color: #a4a4a4; - border-color: #9d9d9d; } + background-color: #9c9ca1; + border-color: #95959b; } .btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(189, 189, 189, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(182, 182, 186, 0.5); } .btn-success { color: #fff; @@ -1813,50 +1813,50 @@ fieldset:disabled a.btn { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-outline-primary { - color: #773b6f; + color: #1c3e5c; background-color: transparent; background-image: none; - border-color: #773b6f; } + border-color: #1c3e5c; } .btn-outline-primary:hover { color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .btn-outline-primary:focus, .btn-outline-primary.focus { - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.5); } .btn-outline-primary.disabled, .btn-outline-primary:disabled { - color: #773b6f; + color: #1c3e5c; background-color: transparent; } .btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, .show > .btn-outline-primary.dropdown-toggle { color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.5); } .btn-outline-secondary { - color: #bdbdbd; + color: #b6b6ba; background-color: transparent; background-image: none; - border-color: #bdbdbd; } + border-color: #b6b6ba; } .btn-outline-secondary:hover { color: #212529; - background-color: #bdbdbd; - border-color: #bdbdbd; } + background-color: #b6b6ba; + border-color: #b6b6ba; } .btn-outline-secondary:focus, .btn-outline-secondary.focus { - box-shadow: 0 0 0 0.2rem rgba(189, 189, 189, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(182, 182, 186, 0.5); } .btn-outline-secondary.disabled, .btn-outline-secondary:disabled { - color: #bdbdbd; + color: #b6b6ba; background-color: transparent; } .btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, .show > .btn-outline-secondary.dropdown-toggle { color: #212529; - background-color: #bdbdbd; - border-color: #bdbdbd; } + background-color: #b6b6ba; + border-color: #b6b6ba; } .btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(189, 189, 189, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(182, 182, 186, 0.5); } .btn-outline-success { color: #28a745; @@ -1998,10 +1998,10 @@ fieldset:disabled a.btn { .btn-link { font-weight: 400; - color: #773b6f; + color: #1c3e5c; background-color: transparent; } .btn-link:hover { - color: #44223f; + color: #0a1621; text-decoration: underline; background-color: transparent; border-color: transparent; } @@ -2209,7 +2209,7 @@ input[type="button"].btn-block { .dropdown-item.active, .dropdown-item:active { color: #fff; text-decoration: none; - background-color: #773b6f; } + background-color: #1c3e5c; } .dropdown-item.disabled, .dropdown-item:disabled { color: #6c757d; background-color: transparent; } @@ -2473,12 +2473,12 @@ input[type="button"].btn-block { opacity: 0; } .custom-control-input:checked ~ .custom-control-label::before { color: #fff; - background-color: #773b6f; } + background-color: #1c3e5c; } .custom-control-input:focus ~ .custom-control-label::before { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .custom-control-input:active ~ .custom-control-label::before { color: #fff; - background-color: #cc98c5; } + background-color: #5d99ce; } .custom-control-input:disabled ~ .custom-control-label { color: #6c757d; } .custom-control-input:disabled ~ .custom-control-label::before { @@ -2514,34 +2514,34 @@ input[type="button"].btn-block { border-radius: 0.25rem; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::before { - background-color: #773b6f; } + background-color: #1c3e5c; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { - background-color: #773b6f; } + background-color: #1c3e5c; } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); } .custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { - background-color: rgba(119, 59, 111, 0.5); } + background-color: rgba(28, 62, 92, 0.5); } .custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { - background-color: rgba(119, 59, 111, 0.5); } + background-color: rgba(28, 62, 92, 0.5); } .custom-radio .custom-control-label::before { border-radius: 50%; } .custom-radio .custom-control-input:checked ~ .custom-control-label::before { - background-color: #773b6f; } + background-color: #1c3e5c; } .custom-radio .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); } .custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { - background-color: rgba(119, 59, 111, 0.5); } + background-color: rgba(28, 62, 92, 0.5); } .custom-select { display: inline-block; @@ -2557,9 +2557,9 @@ input[type="button"].btn-block { border-radius: 0.25rem; appearance: none; } .custom-select:focus { - border-color: #bb76b2; + border-color: #3a80be; outline: 0; - box-shadow: 0 0 0 0.2rem rgba(187, 118, 178, 0.5); } + box-shadow: 0 0 0 0.2rem rgba(58, 128, 190, 0.5); } .custom-select:focus::-ms-value { color: #495057; background-color: #fff; } @@ -2600,10 +2600,10 @@ input[type="button"].btn-block { margin: 0; opacity: 0; } .custom-file-input:focus ~ .custom-file-label { - border-color: #bb76b2; - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + border-color: #3a80be; + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .custom-file-input:focus ~ .custom-file-label::after { - border-color: #bb76b2; } + border-color: #3a80be; } .custom-file-input:disabled ~ .custom-file-label { background-color: #e9ecef; } .custom-file-input:lang(en) ~ .custom-file-label::after { @@ -2646,18 +2646,18 @@ input[type="button"].btn-block { .custom-range:focus { outline: none; } .custom-range:focus::-webkit-slider-thumb { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .custom-range:focus::-moz-range-thumb { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .custom-range:focus::-ms-thumb { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .custom-range::-moz-focus-outer { border: 0; } .custom-range::-webkit-slider-thumb { width: 1rem; height: 1rem; margin-top: -0.25rem; - background-color: #773b6f; + background-color: #1c3e5c; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; @@ -2666,7 +2666,7 @@ input[type="button"].btn-block { .custom-range::-webkit-slider-thumb { transition: none; } } .custom-range::-webkit-slider-thumb:active { - background-color: #cc98c5; } + background-color: #5d99ce; } .custom-range::-webkit-slider-runnable-track { width: 100%; height: 0.5rem; @@ -2678,7 +2678,7 @@ input[type="button"].btn-block { .custom-range::-moz-range-thumb { width: 1rem; height: 1rem; - background-color: #773b6f; + background-color: #1c3e5c; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; @@ -2687,7 +2687,7 @@ input[type="button"].btn-block { .custom-range::-moz-range-thumb { transition: none; } } .custom-range::-moz-range-thumb:active { - background-color: #cc98c5; } + background-color: #5d99ce; } .custom-range::-moz-range-track { width: 100%; height: 0.5rem; @@ -2702,7 +2702,7 @@ input[type="button"].btn-block { margin-top: 0; margin-right: 0.2rem; margin-left: 0.2rem; - background-color: #773b6f; + background-color: #1c3e5c; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; @@ -2711,7 +2711,7 @@ input[type="button"].btn-block { .custom-range::-ms-thumb { transition: none; } } .custom-range::-ms-thumb:active { - background-color: #cc98c5; } + background-color: #5d99ce; } .custom-range::-ms-track { width: 100%; height: 0.5rem; @@ -2783,7 +2783,7 @@ input[type="button"].btn-block { .nav-pills .nav-link.active, .nav-pills .show > .nav-link { color: #fff; - background-color: #773b6f; } + background-color: #1c3e5c; } .nav-fill .nav-item { flex: 1 1 auto; @@ -3284,19 +3284,19 @@ input[type="button"].btn-block { padding: 0.5rem 0.75rem; margin-left: -1px; line-height: 1.25; - color: #773b6f; + color: #1c3e5c; background-color: #fff; border: 1px solid #dee2e6; } .page-link:hover { z-index: 2; - color: #44223f; + color: #0a1621; text-decoration: none; background-color: #e9ecef; border-color: #dee2e6; } .page-link:focus { z-index: 2; outline: 0; - box-shadow: 0 0 0 0.2rem rgba(119, 59, 111, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(28, 62, 92, 0.25); } .page-link:not(:disabled):not(.disabled) { cursor: pointer; } @@ -3312,8 +3312,8 @@ input[type="button"].btn-block { .page-item.active .page-link { z-index: 1; color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .page-item.disabled .page-link { color: #6c757d; @@ -3372,19 +3372,19 @@ input[type="button"].btn-block { .badge-primary { color: #fff; - background-color: #773b6f; } + background-color: #1c3e5c; } .badge-primary[href]:hover, .badge-primary[href]:focus { color: #fff; text-decoration: none; - background-color: #552a4f; } + background-color: #102435; } .badge-secondary { color: #212529; - background-color: #bdbdbd; } + background-color: #b6b6ba; } .badge-secondary[href]:hover, .badge-secondary[href]:focus { color: #212529; text-decoration: none; - background-color: #a4a4a4; } + background-color: #9c9ca1; } .badge-success { color: #fff; @@ -3471,22 +3471,22 @@ input[type="button"].btn-block { color: inherit; } .alert-primary { - color: #3e1f3a; - background-color: #e4d8e2; - border-color: #d9c8d7; } + color: #0f2030; + background-color: #d2d8de; + border-color: #bfc9d1; } .alert-primary hr { - border-top-color: #cfb9cc; } + border-top-color: #b0bcc6; } .alert-primary .alert-link { - color: #1c0e1a; } + color: #030609; } .alert-secondary { - color: #626262; - background-color: #f2f2f2; - border-color: #ededed; } + color: #5f5f61; + background-color: #f0f0f1; + border-color: #ebebec; } .alert-secondary hr { - border-top-color: #e0e0e0; } + border-top-color: #dedee0; } .alert-secondary .alert-link { - color: #494949; } + color: #464647; } .alert-success { color: #155724; @@ -3563,7 +3563,7 @@ input[type="button"].btn-block { color: #fff; text-align: center; white-space: nowrap; - background-color: #773b6f; + background-color: #1c3e5c; transition: width 0.6s ease; } @media screen and (prefers-reduced-motion: reduce) { .progress-bar { @@ -3624,8 +3624,8 @@ input[type="button"].btn-block { .list-group-item.active { z-index: 2; color: #fff; - background-color: #773b6f; - border-color: #773b6f; } + background-color: #1c3e5c; + border-color: #1c3e5c; } .list-group-flush .list-group-item { border-right: 0; @@ -3639,26 +3639,26 @@ input[type="button"].btn-block { border-bottom: 0; } .list-group-item-primary { - color: #3e1f3a; - background-color: #d9c8d7; } + color: #0f2030; + background-color: #bfc9d1; } .list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { - color: #3e1f3a; - background-color: #cfb9cc; } + color: #0f2030; + background-color: #b0bcc6; } .list-group-item-primary.list-group-item-action.active { color: #fff; - background-color: #3e1f3a; - border-color: #3e1f3a; } + background-color: #0f2030; + border-color: #0f2030; } .list-group-item-secondary { - color: #626262; - background-color: #ededed; } + color: #5f5f61; + background-color: #ebebec; } .list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { - color: #626262; - background-color: #e0e0e0; } + color: #5f5f61; + background-color: #dedee0; } .list-group-item-secondary.list-group-item-action.active { color: #fff; - background-color: #626262; - border-color: #626262; } + background-color: #5f5f61; + border-color: #5f5f61; } .list-group-item-success { color: #155724; @@ -4283,20 +4283,20 @@ button.close { vertical-align: text-top !important; } .bg-primary { - background-color: #773b6f !important; } + background-color: #1c3e5c !important; } a.bg-primary:hover, a.bg-primary:focus, button.bg-primary:hover, button.bg-primary:focus { - background-color: #552a4f !important; } + background-color: #102435 !important; } .bg-secondary { - background-color: #bdbdbd !important; } + background-color: #b6b6ba !important; } a.bg-secondary:hover, a.bg-secondary:focus, button.bg-secondary:hover, button.bg-secondary:focus { - background-color: #a4a4a4 !important; } + background-color: #9c9ca1 !important; } .bg-success { background-color: #28a745 !important; } @@ -4383,10 +4383,10 @@ button.bg-dark:focus { border-left: 0 !important; } .border-primary { - border-color: #773b6f !important; } + border-color: #1c3e5c !important; } .border-secondary { - border-color: #bdbdbd !important; } + border-color: #b6b6ba !important; } .border-success { border-color: #28a745 !important; } @@ -6187,16 +6187,16 @@ button.bg-dark:focus { color: #fff !important; } .text-primary { - color: #773b6f !important; } + color: #1c3e5c !important; } a.text-primary:hover, a.text-primary:focus { - color: #552a4f !important; } + color: #102435 !important; } .text-secondary { - color: #bdbdbd !important; } + color: #b6b6ba !important; } a.text-secondary:hover, a.text-secondary:focus { - color: #a4a4a4 !important; } + color: #9c9ca1 !important; } .text-success { color: #28a745 !important; } @@ -6323,14 +6323,18 @@ a.text-dark:hover, a.text-dark:focus { /* font color */ /* font color */ /* Data colors */ +/* For alt text */ +img { + font-weight: 400 !important; } + .react-grid-item { background-color: white; - border: 1px solid #e1e1e1; + border: 1px solid #e8e8ec; border-radius: 0.25rem; } .react-grid-item-header { - color: #8d8d8d; - background-color: #e1e1e1; + color: #86868a; + background-color: #e8e8ec; font-weight: bold; padding: .3rem; /* Note: must be same as the Bootstrap table for nice alignment */ @@ -6355,8 +6359,8 @@ a.text-dark:hover, a.text-dark:focus { top: 2.25em; /* Check with height of .react-grid-item-header */ left: 0.25em; - border: 0.55rem solid #e1e1e1; - border-top: 0.55rem solid #a7689d; + border: 0.55rem solid #e8e8ec; + border-top: 0.55rem solid #4a6889; border-radius: 50%; width: 3rem; height: 3rem; @@ -6385,41 +6389,10 @@ a.text-dark:hover, a.text-dark:focus { text-align: center; font-weight: 700; } -.toolbar-top { - background-color: #a7689d; - color: #ffffff; - padding: 5px 10px; - /* Note: same padding as ResponsiveGridLayout */ - width: 100%; - overflow: visible; } - -.toolbar-top { - font-weight: 500; } - -.toolbar-top .btn-info:not(:disabled):not(.disabled).active, -.sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.toolbar-top .btn-info:not(:disabled):not(.disabled):active, -.toolbar-top .show > .btn-info.dropdown-toggle { - color: white; - background-color: #8d8d8d; - border-color: #8d8d8d; } - -.toolbar-top .btn-info, -.sts-toolbar .btn-info { - color: white; - background-color: #bdbdbd; - border-color: #bdbdbd; } - -.toolbar-top .btn-info:hover, -.sts-toolbar .btn-info:hover { - color: white; - background-color: #8d8d8d; - border-color: #8d8d8d; } - .tooltip > .tooltip-inner { background-color: white !important; color: black !important; - border: 1px solid #8d8d8d; } + border: 1px solid #86868a; } .react-select-container { min-width: 20rem; } @@ -6434,4 +6407,8 @@ a.text-dark:hover, a.text-dark:focus { border: none; } .form-input button:hover { - background-color: #e1e1e1 !important; } + background-color: #e8e8ec !important; } + +/* Padding on column in bootstrap grid */ +.row .col-padding { + padding-top: 15px; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js index f1c0459dbdcc0c20ce539bf024a17775c1a2f0b2..f40e48031d0706909593487d33fd8ee7103e5a8c 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js @@ -1,23 +1,77 @@ export const HBATilesPerStationType = { - C: 48, - R: 48, + C: 96, + R: 96, I: 96 -} +}; export const LBHAntennasPerStationType = { C: 48, R: 48, - I: 48 -} + I: 96 +}; export const LBLAntennasPerStationType = { C: 48, R: 48, I: 48 -} +}; + +export const AntennaIdsPerTypeStationType = { + C: { + HBA: { + start: 0, + end: 96, + number: 96 + }, + LBL: { + start: 48, + end: 96, + number: 48 + }, + LBH: { + start: 0, + end: 48, + number: 48 + } + }, + R: { + HBA: { + start: 0, + end: 96, + number: 96 + }, + LBL: { + start: 48, + end: 96, + number: 48 + }, + LBH: { + start: 0, + end: 48, + number: 48 + } + }, + I: { + HBA: { + start: 0, + end: 96, + number: 96 + }, + LBH: { + start: 0, + end: 96, + number: 96 + }, + LBL: { + start: 0, + end: 0, + number: 0 + } + }, +}; export function stationTypeFromName(stationName){ - if(stationName === undefined) return undefined + if(stationName === undefined) return undefined; if(stationName.includes('CS')) return 'C'; if(stationName.includes('RS')) return 'R'; return 'I' @@ -27,3 +81,8 @@ export function getInspectPageURLFromSASid(sasId){ const url =`https://proxy.lofar.eu/inspect/HTML/${sasId}/index.html` return url } + +export const LOFARTESTS = { + S: "StationTest", + R: "RTSM" +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js index bdaf6ab2677a4fc813ef527034d3f3b8482b0b79..b635c0c9e109a2f14ae6b6d3754ff29ea212e3aa 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js @@ -1,3 +1,5 @@ +import moment from "moment"; + let lastId = 0; function unique_id(prefix='id_') { @@ -32,5 +34,20 @@ function stringSort(a, b, caseInsensitive = true) { } return 0; } +function renderDateRange(data){ + const lastTest = data[0]; + const firstTest = data[data.length - 1]; + const duration = moment.duration(moment(lastTest.end_date)-moment(firstTest.start_date)); + const years = duration.years(); + const days = duration.days(); + const months = duration.months(); + const hours = duration.hours(); + let string = `${data.length} obs `; + if (years > 0) {string += `${years}Y`;} + if (months > 0) {string += `${months}M`;} + if (days > 0) {string += `${days}d`;} + if (hours > 0) {string += `${hours}h`;} + return string; +} -export { unique_id, capitalize, composeQueryString, stringSort }; +export { unique_id, capitalize, composeQueryString, stringSort, renderDateRange };