From 07ed8594397b06781b429429c6b7565330eeac0b Mon Sep 17 00:00:00 2001 From: Mattia Mancini <mancini@astron.nl> Date: Fri, 30 Nov 2018 09:46:57 +0000 Subject: [PATCH] OSB-34: code merging with master branch and implementing referee suggetions --- .gitattributes | 17 + LCU/Maintenance/DBInterface/CMakeLists.txt | 2 + .../DBInterface/django_postgresql/settings.py | 3 +- .../DBInterface/monitoringdb/urls.py | 2 +- .../monitoringdb/views/controllers.py | 11 +- .../monitoringdb/views/rtsm_views.py | 44 +- .../maintenancedb_view/package.json | 97 ++-- .../MDB_WebView/maintenancedb_view/src/App.js | 25 +- .../src/components/LatestObservations.js | 63 ++- .../src/components/MultiSelectDropdown.js | 123 +++++ .../src/components/ObservationInspectTag.js | 18 + .../src/components/StationAutoComplete.css | 59 +++ .../src/components/StationAutoComplete.js | 130 +++++ .../src/components/StationAutoComplete.scss | 68 +++ .../src/components/StationOverview.js | 25 +- .../src/components/StationStatistics.js | 39 +- .../src/components/StationTestChildView.css | 13 + .../src/components/StationTestChildView.js | 144 +++++ .../src/components/StationTestChildView.scss | 12 + .../src/components/StationTestDetails.js | 26 + .../src/components/StationTestSummary.js | 27 +- .../src/components/StationTestView.css | 95 ++++ .../src/components/StationTestView.js | 492 ++++++++++++++++++ .../src/components/StationTestView.scss | 119 +++++ .../maintenancedb_view/src/index.js | 4 +- .../src/pages/DetailsPage.js | 7 - .../src/pages/LandingPage.js | 116 +++-- .../src/pages/StationOverviewPage.js | 218 +++++++- .../src/redux/actions/appInitDataActions.js | 45 ++ .../src/redux/actions/landingPageActions.js | 4 +- .../src/redux/actions/mainFiltersActions.js | 24 + .../actions/stationOverviewPageActions.js | 12 + .../src/redux/reducers/appInitDataReducers.js | 28 +- .../src/redux/reducers/index.js | 5 +- .../src/redux/reducers/landingPageReducers.js | 4 +- .../src/redux/reducers/mainFilters.js | 67 ++- .../reducers/stationOverviewPageReducers.js | 65 +++ .../maintenancedb_view/src/redux/store.js | 16 +- .../testdata/ctrl_station_component_errors.js | 35 ++ .../src/themes/lofar-styles.css | 49 +- .../src/themes/lofar-styles.scss | 55 +- .../maintenancedb_view/src/themes/lofar.css | 49 +- .../src/utils/LOFARDefinitions.js | 29 ++ .../src/utils/autoLoader.js | 69 ++- .../maintenancedb_view/src/utils/constants.js | 6 +- .../maintenancedb_view/src/utils/grid.js | 21 + .../maintenancedb_view/src/utils/utils.js | 20 +- 47 files changed, 2325 insertions(+), 277 deletions(-) create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/ObservationInspectTag.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.scss create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestDetails.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/stationOverviewPageActions.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/stationOverviewPageReducers.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/ctrl_station_component_errors.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js create mode 100644 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/grid.js diff --git a/.gitattributes b/.gitattributes index f8dc862274b..d0a6fb9123b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1853,6 +1853,11 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/api_configuration.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 +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/ObservationInspectTag.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationDetails.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationList.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationList.js -text @@ -1860,9 +1865,16 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.cs LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.scss -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.scss -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestDetails.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.scss -text +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/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 @@ -1879,12 +1891,15 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/TilesPage.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/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 LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/mainFilters.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/stationOverviewPageReducers.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/store.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/registerServiceWorker.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/ctrl_station_component_errors.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/latest_observations.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/station_details.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/station_overview.js -text @@ -1895,8 +1910,10 @@ LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.css -t LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar-variables.scss -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.scss -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/autoLoader.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/constants.js -text +LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/grid.js -text LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js -text LCU/Maintenance/MDB_tools/CMakeLists.txt -text LCU/Maintenance/MDB_tools/bin/mdb_loader.py -text diff --git a/LCU/Maintenance/DBInterface/CMakeLists.txt b/LCU/Maintenance/DBInterface/CMakeLists.txt index 99c17d8d018..b5fec93c6e8 100644 --- a/LCU/Maintenance/DBInterface/CMakeLists.txt +++ b/LCU/Maintenance/DBInterface/CMakeLists.txt @@ -10,6 +10,8 @@ find_python_module(django REQUIRED) find_python_module(psycopg2 REQUIRED) find_python_module(rest_framework REQUIRED) #sudo pip install djangorestframework find_python_module(requests REQUIRED) +find_python_module(celery REQUIRED) +find_python_module(django_filters REQUIRED) # includes every python file excepts for the manage.py diff --git a/LCU/Maintenance/DBInterface/django_postgresql/settings.py b/LCU/Maintenance/DBInterface/django_postgresql/settings.py index f0cbc115e13..e257b43e2da 100644 --- a/LCU/Maintenance/DBInterface/django_postgresql/settings.py +++ b/LCU/Maintenance/DBInterface/django_postgresql/settings.py @@ -179,5 +179,4 @@ REST_FRAMEWORK = { CELERY_RESULT_BACKEND = 'amqp://guest@localhost//' # LOFAR SPECIFIC PARAMETERS -URL_TO_RTSM_PLOTS = 'https://proxy.lofar.eu/rtsm/obs_plots/' -URL_TO_STORE_RTSM_PLOTS = '/home/mmancini/svn-tree/MonitoringMaintenance-OSB-34/build/gnucxx11_debug/plots' \ No newline at end of file +URL_TO_STORE_RTSM_PLOTS = './' \ No newline at end of file diff --git a/LCU/Maintenance/DBInterface/monitoringdb/urls.py b/LCU/Maintenance/DBInterface/monitoringdb/urls.py index d75e6d9c389..67ff6ed7521 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/urls.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/urls.py @@ -26,6 +26,7 @@ rtsm_router = routers.DefaultRouter() # RTSM rtsm_router.register(r'errors', RTSMErrorsViewSet) rtsm_router.register(r'spectra', RTSMSpectrumViewSet) +rtsm_router.register(r'error_summary_plot', RTSMSummaryPlot, base_name='rtsm-summary-plot') rtsm_router.register(r'', RTSMObservationViewSet) @@ -38,7 +39,6 @@ urlpatterns = [ url(r'^api/api-auth', include('rest_framework.urls', namespace='rest_framework')), url(r'^api/stationtests/raw/insert', insert_raw_station_test), url(r'^api/rtsm/raw/insert', insert_raw_rtsm_test), - path('api/rtsm/error_summary_plot/<int:pk>', get_summary_plot, name='rtsm-summary-plot'), url(r'^api/view/ctrl_stationoverview', ControllerStationOverview.as_view()), url(r'^api/view/ctrl_stationtestsummary', ControllerStationTestsSummary.as_view()), url(r'^api/view/ctrl_latest_observation', ControllerLatestObservations.as_view()), diff --git a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py index 90afd381296..5457d175472 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py @@ -473,6 +473,9 @@ class ControllerStationTestStatistics(ValidableReadOnlyView): station_group = 'A' test_type = 'B' error_types = [] + from_date = None + to_date = None + averaging_interval = None fields = [ coreapi.Field( @@ -732,7 +735,9 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): .filter(start_datetime__gte=self.from_date, end_datetime__lte=self.to_date) - failing_component_types = station_tests.distinct('component_errors__component__type').exclude(component_errors__component__type__isnull=True).values_list('component_errors__component__type') + failing_component_types = station_tests.distinct('component_errors__component__type').\ + exclude(component_errors__component__type__isnull=True).\ + values_list('component_errors__component__type') for failing_component_type in failing_component_types: failing_component_type = failing_component_type[0] @@ -812,7 +817,7 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): count = component_error['count']) error_type = component_error['error_type'] # CHECKS IF THE ERROR IS PRESENT IN BOTH RCUS (hence, both polarizations of the antenna) - url_to_plot = reverse('rtsm-summary-plot', (component_error['pk'],), request=self.request) + url_to_plot = reverse('rtsm-summary-plot-detail', (component_error['pk'],), request=self.request) details['url'] = url_to_plot if component_id not in component_errors: component_errors_dict[str(component_id)] = list() @@ -984,7 +989,7 @@ class ControllerStationComponentElementErrors(ValidableReadOnlyView): mode = item['mode'] samples = item['observation__samples'] - url_to_plot = reverse('rtsm-summary-plot', (item['pk'],), request=self.request) + url_to_plot = reverse('rtsm-summary-plot-detail', (item['pk'],), request=self.request) errors[observation_pk]['component_errors'][polarization]['errors'][error_type] = dict(samples=samples, percentage=percentage, count=count, diff --git a/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py b/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py index 5649d10a18c..1bfb15789fa 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py @@ -3,6 +3,8 @@ from ..models.rtsm import RTSMObservation, RTSMError, RTSMSpectrum from ..serializers.rtsm import RTSMObservationSerializer, RTSMErrorSerializer, \ RTSMSpectrumSerializer, RTSMSummaryPlotSerializer, RTSMErrorSummary from ..rtsm_test_raw_parser import parse_rtsm_test +from django.shortcuts import get_object_or_404 +from django.http import Http404, HttpResponseServerError import os from ..tasks import check_error_summary_plot @@ -26,32 +28,36 @@ class RTSMObservationViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = RTSMObservationSerializer filter_fields = '__all__' -@api_view(['GET']) -def get_summary_plot(request, pk): - try: - entity = RTSMErrorSummary.objects.get(pk=pk) - summary_plot = entity.summary_plot.first() - if summary_plot is None: +class RTSMSummaryPlot(viewsets.ViewSet): + """ + Get the summary plot associated to the error summary given + """ + queryset = RTSMErrorSummary.objects.all() + + def retrieve(self, request, pk=None): + try: + entity = get_object_or_404(self.queryset, pk=pk) + summary_plot = entity.summary_plot.first() - raise ObjectDoesNotExist() + if summary_plot is None: + raise ObjectDoesNotExist() - uri = RTSMSummaryPlotSerializer(summary_plot).data['uri'] - except ObjectDoesNotExist as e: - check_error_summary_plot.delay(pk) - return HttpResponse('<h1>NOT FOUND</h1>', status=status.HTTP_404_NOT_FOUND) + uri = RTSMSummaryPlotSerializer(summary_plot).data['uri'] + + except ObjectDoesNotExist as e: + check_error_summary_plot.delay(pk) + raise Http404() - try: if os.path.exists(uri) and os.path.isfile(uri): - with open(uri, 'rb') as f_stream: - image = f_stream.read() - return HttpResponse(image, status=status.HTTP_200_OK, content_type='image/gif') + with open(uri, 'rb') as f_stream: + image = f_stream.read() + return HttpResponse(image, status=status.HTTP_200_OK, content_type='image/gif') else: check_error_summary_plot.delay(pk) - return HttpResponse('<h1>NOT FOUND</h1>', status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return HttpResponse('exception %s occurred: %s' % (e.__class__.__name__, e), - status=status.HTTP_500_INTERNAL_SERVER_ERROR) + print(uri) + raise Http404() + @api_view(['POST']) def insert_raw_rtsm_test(request): diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json b/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json index 5cbbf37a648..bce306cf061 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/package.json @@ -1,41 +1,60 @@ { - "name": "maintenancedb_view", - "version": "0.1.0", - "description": "WebPage meant to display the content of the maintenance db in the web browser,", - "proxy": "http://lofarmonitortest.control.lofar", - "scripts": { - "flow": "flow", - "build-css": "node-sass-chokidar src/ -o src/", - "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", - "start-js": "react-scripts start", - "build-js": "react-scripts build", - "start": "npm run -p watch-css & npm run start-js", - "build": "npm run build-css && npm run build-js", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject", - "deploy": "npm run build" - }, - "dependencies": { - "ajv": "^6.5.4", - "axios": "^0.18.0", - "bootstrap": "^4.1.3", - "moment": "^2.22.2", - "node-sass-chokidar": "^1.3.3", - "react": "^16.4.2", - "react-dom": "^16.4.2", - "react-grid-layout": "^0.16.6", - "react-redux": "^5.0.7", - "react-router": "^4.3.1", - "react-router-dom": "^4.3.1", - "react-scripts": "1.1.4", - "react-table": "^6.8.6", - "react-vega-lite": "^2.0.2", - "reactstrap": "^6.3.1", - "redux": "^4.0.1", - "redux-thunk": "^2.3.0", - "vega": "^4.3.0", - "vega-lite": "^2.6.0", - "vega-tooltip": "^0.13.0" - }, - "private": true + "name": "maintenancedb_view", + "version": "0.1.0", + "description": "WebPage meant to display the content of the maintenance db in the web browser,", + "proxy": "http://lofarmonitortest.control.lofar", + "scripts": { + "flow": "flow", + "build-css": "node-sass-chokidar src/ -o src/", + "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", + "start-js": "react-scripts start", + "build-js": "react-scripts build", + "start": "npm run -p watch-css & npm run start-js", + "build": "npm run build-css && npm run build-js", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject", + "deploy": "npm run build" + }, + "dependencies": { + "ajv": "^6.5.4", + "axios": "^0.18.0", + "bootstrap": "^4.1.3", + "bootstrap-select": "^1.13.3", + "connected-react-router": "^4.5.0", + "jquery": "^3.3.1", + "moment": "^2.22.2", + "node-sass-chokidar": "^1.3.3", + "query-string": "^6.2.0", + "react": "^16.4.2", + "react-autosuggest": "^9.4.2", + "react-datepicker": "^1.7.0", + "react-dom": "^16.4.2", + "react-grid-layout": "^0.16.6", + "react-icons": "^3.2.2", + "react-popout": "^1.0.1", + "react-redux": "^5.0.7", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", + "react-select": "^2.1.1", + "react-sticky": "^6.0.3", + "react-table": "^6.8.6", + "react-table-container": "^2.0.1", + "react-vega-lite": "^2.0.2", + "reactstrap": "^6.3.1", + "redux": "^4.0.1", + "redux-thunk": "^2.3.0", + "vega": "^4.3.0", + "vega-lite": "^2.6.0", + "vega-tooltip": "^0.13.0" + }, + "devDependencies": { + "react-scripts": "latest" + }, + "private": true, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js index a2b05ebe328..584a61eb515 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/App.js @@ -5,15 +5,17 @@ import TilesPage from './pages/TilesPage.js' import DetailsPage from './pages/DetailsPage.js' import { connect } from "react-redux"; -import { fetchErrorTypes } from "./redux/actions/appInitDataActions.js"; +import { fetchErrorTypes, fetchStations } from "./redux/actions/appInitDataActions.js"; - -import './App.css'; import { - BrowserRouter as Router, - Route + //BrowserRouter as Router, + Route, + Switch } from 'react-router-dom'; +import { ConnectedRouter as Router } from 'connected-react-router'; +import { history } from "./redux/store.js"; +import './App.css'; class AppC extends Component { @@ -21,16 +23,19 @@ class AppC extends Component { componentDidMount( ) { // Load initial application data this.props.dispatch(fetchErrorTypes()); + this.props.dispatch(fetchStations()); } render(){ return ( - <Router> + <Router history={history}> <div> - <Route exact path="/" component={LandingPage}/> - <Route exact path="/station_overview" component={StationOverviewPage}/> - <Route exact path="/tiles" component={TilesPage}/> - <Route exact path="/details" component={DetailsPage}/> + <Switch> + <Route exact path="/" component={LandingPage}/> + <Route path="/station_overview/:name?" component={StationOverviewPage}/> + <Route exact path="/tiles" component={TilesPage}/> + <Route exact path="/details" component={DetailsPage}/> + </Switch> </div> </Router> ); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.js index a7d482c1af7..530012f72fc 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/LatestObservations.js @@ -1,25 +1,27 @@ import React, { Component } from 'react'; -import { Table, Popover, PopoverHeader, PopoverBody } from 'reactstrap'; +import { Table, Tooltip } from 'reactstrap'; import { unique_id } from '../utils/utils.js' import AutoLoadWrapper from '../utils/autoLoader.js' import * as moment from 'moment'; +import { datetime_format } from '../utils/constants' +import ObservationInspectTag from './ObservationInspectTag.js' +// CSS import './LatestObservations.css' + + /** * SORow; Class to render the row for a station in the StationOverview. */ class SORow extends Component { + constructor(props){ super(props); - this.state = {popoverOpen:false, mouseOverPopup:false}; + this.state = {popoverOpen: false}; this.id = unique_id(); this.togglePopover = this.togglePopover.bind(this); - this.mouseDown = this.mouseDown.bind(this); } - mouseDown(e){ - if(e.button === 1) - this.setState({mouseOverPopup: !this.state.mouseOverPopup}); - } + renderObservationID() { return this.props.data.observation_id; } @@ -42,11 +44,8 @@ class SORow extends Component { } togglePopover(){ - if(this.state.mouseOverPopup === false){ - this.setState({popoverOpen: !this.state.popoverOpen}); - } + this.setState({popoverOpen: !this.state.popoverOpen}); } - render() { const data = this.props.data; const station_involved_list = this.getStationInvolvedList(); @@ -58,27 +57,32 @@ class SORow extends Component { <td>{station.n_errors}</td> </tr>); return ( - <tr id={this.id} onMouseOver={this.togglePopover} onMouseOut={this.togglePopover} - onMouseDown={this.mouseDown} - className="hoverable"> - <th scope="row">{ this.renderObservationID() }</th> - <td>{ moment(start_datetime).format('lll') }</td> + <tr id={this.id} className="hoverable"> + + <ObservationInspectTag observationId={this.props.data.observation_id} /> + <td>{ moment.utc(start_datetime).format(datetime_format) }</td> <td>{ station_involved_list.length }</td> <td>{ this.renderStationsWithProblems(station_involved_list) }</td> <td>{ total_component_errors }</td> - <Popover placement="auto-start" isOpen={this.state.popoverOpen} target={ this.id }> - <PopoverHeader>{data.observation_id}</PopoverHeader> - <PopoverBody> - <strong>Start:</strong> { start_datetime}<br/> - <strong>End:</strong> { end_datetime }<br/> + <Tooltip placement="auto" isOpen={this.state.popoverOpen} + target={this.id } + toggle={this.togglePopover} + style={{backgroundColor: "white", color:"black", opacity: "1"}} + autohide={false}> + <div className='popover-header'>{data.observation_id}</div> + <div> + + <strong>Start:</strong> { moment.utc(start_datetime).format(datetime_format) }<br/> + <strong>End:</strong> { moment.utc(end_datetime).format(datetime_format) }<br/> <strong>Mode:</strong> { mode.join(',') }<br/> <Table size="sm" className="so-table table-wrapper"> - <thead><tr><th>Station name</th><th>errors</th></tr></thead> - <tbody>{stations_and_errors}</tbody> + <thead><tr><th>Station name</th><th>errors</th></tr></thead> + <tbody>{stations_and_errors}</tbody> </Table> - </PopoverBody> - </Popover> + </div> + + </Tooltip> </tr> ); } @@ -94,8 +98,15 @@ class LatestObservationsC extends Component { return this.props.data.map( (stationData) => <SORow key={stationData.observation_id} data={ stationData } /> ); } - render() { + // Do not (re)render when data is loading (performance improvement) + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.isLoading) { + return false; + } + return true; + } + render() { return ( <div className="station-overview-ctrl"> <Table size="sm" className="so-table"> diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js new file mode 100644 index 00000000000..ddebb174766 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/MultiSelectDropdown.js @@ -0,0 +1,123 @@ +import React, {Component} from 'react'; +import { IoMdCheckmark as IsSelectIcon } from 'react-icons/io'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap' + +class SelectableOption extends Component { + + selectedItem = () => {this.props.onSelectedItem(this.props.value)} + + render(){ + const selectMark = this.props.isSelected ? <IsSelectIcon style={{width:'1rem'}} /> : <div style={{paddingRight:'1rem'}}/>; + const jsx = ( + <DropdownItem onClick={this.selectedItem} > + <tr style={{position:'relative', left: '-1rem'}}> + <td>{selectMark}</td> + <td>{this.props.children}</td></tr> + </DropdownItem> + ) + return jsx + } +} + +export class MultiSelectDropdown extends Component{ + constructor(props){ + super(props) + + this.state = { + isOpen: false, + selectedItems: {} + } + } + // Toggle the dropdown state + toggle = () => { + if(!this.state.isOpen) + { + this.setState({isOpen:true}); + }else if(!this.state.mouseOverMenu){ + this.closeMenu() + } + + } + + closeMenu(){ + this.setState({isOpen:false}) + this.props.onSelectionChange(this.getSelectedItemsList()) + } + + itemSelected = (e) => { + if (e === 'all'){ + this.setState({selectedItems: {}}) + this.props.onSelectionChange([]) + this.setState({isOpen:false}) + }else{ + const newSelectedItems = this.state.selectedItems + newSelectedItems[e] = !newSelectedItems[e] + this.setState({selectedItems: newSelectedItems}) + } + } + + getSelectedItemsList (selectedItems) { + if (selectedItems === undefined){ + selectedItems = this.state.selectedItems; + } + return Object.keys(selectedItems).filter(item => this.state.selectedItems[item]); + + + } + + renderLabel(){ + const selectedItemsList = this.getSelectedItemsList() + if(selectedItemsList.length === 0 ){ + return this.props.placeHolder; + }else if(selectedItemsList.length <= 4){ + return selectedItemsList.join(', ') + }else { + const firstFour = selectedItemsList.slice(0, 4) + return firstFour.join(', ') + ', ...' + } + } + + isItemSelected = (e) => { + if(this.state.selectedItems.hasOwnProperty(e)) + return this.state.selectedItems[e] + return false + } + + componentDidMount() { + const selectedItems = this.state.selectedItems + let update = false + if(this.props.selectedItems === undefined) return + for(let item of this.props.selectedItems){ + if(!selectedItems.hasOwnProperty(item) || !selectedItems[item] ){ + selectedItems[item] = true + update = true + } + } + + if(update) this.setState({selectedItems: selectedItems}) + } + + mouseOverMenu = () => {this.setState({mouseOverMenu:true})} + mouseExitsMenu = () => {this.setState({mouseOverMenu:false})} + + render(){ + let allOptions = [{value:'all', label:'<ALL>'}].concat(this.props.options) + let options = allOptions.map((item, key) => <SelectableOption key={key} value={item.value} isSelected={this.isItemSelected(item.value)} onSelectedItem={this.itemSelected}>{item.label}</SelectableOption>) + + const jsx = ( + <Dropdown isOpen={this.state.isOpen} toggle={this.toggle} className={this.props.className}> + <DropdownToggle caret> + {this.renderLabel()} + </DropdownToggle> + <DropdownMenu style={{width:'max-content'}} + onMouseOver={this.mouseOverMenu} + onMouseOut={this.mouseExitsMenu}> + {options} + </DropdownMenu> + </Dropdown> + ) + return jsx; + } +} + +export default MultiSelectDropdown diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/ObservationInspectTag.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/ObservationInspectTag.js new file mode 100644 index 00000000000..e01ea17b8e6 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/ObservationInspectTag.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react'; +import { getInspectPageURLFromSASid } from '../utils/LOFARDefinitions.js' + +class ObservationInspectTag extends Component { + + clicked = () => { + const url = getInspectPageURLFromSASid(this.props.observationId) + window.open(url) + } + + render () { + const observationId = this.props.observationId; + return ( + <div onClick={this.clicked} style={{fontWeight:'600'}}>{observationId}</div> + ) + } +} +export default ObservationInspectTag; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css new file mode 100644 index 00000000000..d76b043672b --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.css @@ -0,0 +1,59 @@ +/* COLORS */ +/* Color palette interface (created with https://material.io/tools/color/) */ +/* font color */ +/* font color */ +/* Data colors */ +.react-autosuggest__container { + position: relative; + display: inline-block; } + +/* +.react-autosuggest__input { +} +*/ +.react-autosuggest__container--open .react-autosuggest__input { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.react-autosuggest__suggestions-container { + display: none; } + +.react-autosuggest__container--open .react-autosuggest__suggestions-container { + display: block; + position: absolute; + width: 100%; + border: 1px solid #aaa; + background-color: #fff; + color: black; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2000; + overflow-y: auto; + max-height: 30em; } + +.react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; } + +.react-autosuggest__suggestion { + cursor: pointer; + padding: 10px 20px; } + +.react-autosuggest__suggestion:not(:first-child) { + border-top: 1px solid #ddd; } + +.react-autosuggest__suggestion--focused { + background-color: #0C7EAF; + color: #fff; } + +.suggestion-content { + display: flex; + align-items: center; + background-repeat: no-repeat; } + +.react-autosuggest__suggestion--highlighted { + background-color: #8d8d8d; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js new file mode 100644 index 00000000000..383544a188e --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.js @@ -0,0 +1,130 @@ +import React, {Component} from 'react'; +import {connect} from "react-redux"; +import Autosuggest from 'react-autosuggest'; + +import './StationAutoComplete.css' + + +/** + * StationAutoCompleteC; 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 + * suggestions is chosen. + * + * When using this component, in most cases you want to add a key with the value of the current + * selected station. This forces a new instance instead of only a rerendering when the + * 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} /> + */ +class StationAutoCompleteC extends Component { + + // Autosuggest is a controlled component. + // However the input value is decoupled from Redux state but gets its + // initial value from the props. + state = { + suggestions: this.props.stations, + value: this.props.selectedStation + }; + + // Get list of suggestions based on user input (while typing) + getSuggestions = value => { + 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 + ); + }; + + // 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 + }); + }; + + onKeyPress = (event) => { + if (event.key === "Enter" && this.state.value !== this.props.selectedStation) { + // Call the callback provided by parent component to announce the change + this.props.onChange(this.state.value); + } + }; + + // Autosuggest calls this function when the list of suggestions need to be updated + onSuggestionsFetchRequested = ({ value }) => { + this.setState({ + suggestions: this.getSuggestions(value) + }); + }; + + // Autosuggest will call this function when the suggestions need to be cleared. + onSuggestionsClearRequested = () => { + this.setState({ + suggestions: [] + }); + }; + + // onSuggestionSelected(event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) + onSuggestionSelected = (event, { suggestionValue }) => { + // Call the callback provided by parent component to announce the change + this.props.onChange(suggestionValue); + } + + // Always show suggestions, also when input gets the initial focus + shouldRenderSuggestions = (value) => { + return true; + }; + + + render() { + + // Autosuggest will pass through all these props to the input. + const inputProps = { + placeholder: 'Type station name..', + value: this.state.value, + onChange: this.onChange, + onKeyPress: this.onKeyPress, + className: 'form-control form-control-sm react-autosuggest__input' + }; + + return ( + <Autosuggest + suggestions={this.state.suggestions} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + shouldRenderSuggestions={this.shouldRenderSuggestions} + getSuggestionValue={this.getSuggestionValue} + renderSuggestion={this.renderSuggestion} + inputProps={inputProps} + /> + ); + } +} + +// Get full list of stations from redux +const mapStateToPropsToolBar = state => { + return { + //stations: [{name:'cs001c'},{name:'cs002c'},{name:'cs003c'}] + stations: state.appInitData.stations + }; +}; + +const StationAutoComplete = connect(mapStateToPropsToolBar)(StationAutoCompleteC); + + +export default StationAutoComplete; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss new file mode 100644 index 00000000000..a9d65f862b6 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationAutoComplete.scss @@ -0,0 +1,68 @@ + +@import '../themes/lofar-variables.scss'; + +.react-autosuggest__container { + position: relative; + display: inline-block; +} + +/* +.react-autosuggest__input { +} +*/ + +.react-autosuggest__container--open .react-autosuggest__input { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.react-autosuggest__suggestions-container { + display: none; +} + +.react-autosuggest__container--open .react-autosuggest__suggestions-container { + display: block; + position: absolute; + width: 100%; + border: 1px solid #aaa; + background-color: #fff; + color: black; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2000; + overflow-y: auto; + max-height: 30em; +} + +.react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.react-autosuggest__suggestion { + cursor: pointer; + padding: 10px 20px; +} + +.react-autosuggest__suggestion:not(:first-child) { + border-top: 1px solid #ddd; +} + +.react-autosuggest__suggestion--focused { + background-color: #0C7EAF; + color: #fff; +} + +.suggestion-content { + display: flex; + align-items: center; + background-repeat: no-repeat; +} + +.react-autosuggest__suggestion--highlighted { + background-color: $secondary-dark; +} 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 86aa0301257..551053e73fe 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationOverview.js @@ -3,6 +3,10 @@ import {withRouter} from "react-router"; import {Table, Popover, PopoverHeader, PopoverBody} from 'reactstrap'; import {unique_id} from '../utils/utils.js' import AutoLoadWrapper from '../utils/autoLoader.js' +import * as moment from 'moment'; +import { datetime_format } from '../utils/constants' + +// CSS import './StationOverview.css' /** @@ -60,7 +64,8 @@ class StationTestBadgeC extends Component { } onClick() { - this.props.history.push('/station_overview') + let station = this.props.station; + this.props.history.push(`/station_overview?station=${station}`); } togglePopover() { @@ -93,11 +98,11 @@ class StationTestBadgeC extends Component { <tbody> <tr> <th>Start:</th> - <td>{data.start_datetime}</td> + <td>{moment.utc(data.start_datetime).format(datetime_format)}</td> </tr> <tr> <th>End:</th> - <td>{data.end_datetime}</td> + <td>{moment.utc(data.end_datetime).format(datetime_format)}</td> </tr> <tr> <th>Checks:</th> @@ -178,11 +183,11 @@ class RTSMBadge extends Component { <tbody> <tr> <th>Start:</th> - <td>{data.start_datetime}</td> + <td>{moment.utc(data.start_datetime).format(datetime_format)}</td> </tr> <tr> <th>End:</th> - <td>{data.end_datetime}</td> + <td>{moment.utc(data.end_datetime).format(datetime_format)}</td> </tr> <tr> <th>Mode:</th> @@ -215,7 +220,7 @@ class RTSMBadge extends Component { /** * SORow; Class to render the row for a station in the StationOverview. */ -class SORow extends Component { +class SORowC extends Component { renderStationName() { return this.props.data.station_name; @@ -233,16 +238,20 @@ class SORow extends Component { return this.props.data.rtsm.map((testData) => <RTSMBadge key={testData.observation_id} data={testData}/>); } + onClick() { + let station = this.props.data.station_name; + this.props.history.push(`/station_overview?station=${station}`); + } render() { return (<tr> - <th scope="row">{this.renderStationName()}</th> + <th scope="row" onClick={()=>this.onClick()}>{this.renderStationName()}</th> <td>{this.renderStationTests()}</td> <td>{this.renderRTSM()}</td> </tr>); } } - +const SORow = withRouter(SORowC); /** * StationOverview class. */ 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 bfb26b37374..5ff27fd80e3 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationStatistics.js @@ -17,15 +17,15 @@ class ToolBarC extends Component { this.state = {isNavbarCollapsed: true}; } - setAveragingWindow(e) { + setAveragingWindow = (e) => { this.props.setStationStatisticsAveragingWindow(e.target.value); } - setTestType(e) { + setTestType = (e) => { this.props.setStationStatisticsTestType(e.target.value); } - toggle(){ + toggle = () => { this.setState({ isNavbarCollapsed: !this.state.isNavbarCollapsed }); @@ -35,9 +35,10 @@ class ToolBarC extends Component { return ( <Nav className="ml-auto"> <NavItem> - <select className="form-control custom-select custom-select-sm" id="selected-group" value={this.props.test_type} onChange={(e) => this.setTestType(e)} style={{ - width: 'auto' - }}> + <select className="form-control custom-select custom-select-sm" + id="selected-group" + value={this.props.test_type} onChange={this.setTestType} style={{ + width: 'auto'}}> <option value="B">Both test types</option> <option value="R">RTSM only</option> <option value="S">StationTest only</option> @@ -46,14 +47,17 @@ class ToolBarC extends Component { <NavItem> <Input type="select" className="form-control custom-select custom-select-sm" style={{ top: "0rem !important" - }} onChange={(e) => this.setAveragingWindow(e)} value={this.props.averaging_window}> + }} onChange={this.setAveragingWindow} value={this.props.averaging_window}> <option value={1}>day</option> <option value={7}>week</option> <option value={30}>month</option> </Input> </NavItem> <NavItem> - <Input type="select" className="form-control custom-select custom-select-sm" onChange={(e) => this.props.switchHistogramEvent(e)} value={this.props.histogramType}> + <Input type="select" + className="form-control custom-select custom-select-sm" + onChange={this.props.switchHistogramEvent} + value={this.props.histogramType}> <option value="per_error_type">per error type</option> <option value="per_station">per station</option> </Input> @@ -178,6 +182,16 @@ class StationStatisticsC extends Component { } } + onSwitchHistogramType = e => this.setState({histogramType: e.target.value}) + + // Do not (re)render when data is loading (performance improvement) + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.isLoading) { + return false; + } + return true; + } + render() { const {spec, data} = this.getSpecData(this.state.histogramType); if (this.ref.current !== null) { @@ -191,7 +205,8 @@ class StationStatisticsC extends Component { <Navbar className="react-grid-item-header justify-content-between" style={{ - padding: "0" + padding: "0", + zIndex: 1000 }}> <NavbarBrand style={{ padding: "0" @@ -200,8 +215,10 @@ class StationStatisticsC extends Component { Station statistics</h5> </NavbarBrand> <ToolBar style={{ - padding: "0" - }} histogramType={this.state.histogramType} switchHistogramEvent={e => this.setState({histogramType: e.target.value})}/> + padding: "0" + }} + histogramType={this.state.histogramType} + switchHistogramEvent={this.onSwitchHistogramType}/> </Navbar> <div className="react-grid-item-body" id="plot" ref={this.ref}> <ReactVegaLite spec={spec} data={data} enableHover={true}/> diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css new file mode 100644 index 00000000000..b4c07fae728 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.css @@ -0,0 +1,13 @@ +/* COLORS */ +/* Color palette interface (created with https://material.io/tools/color/) */ +/* font color */ +/* font color */ +/* Data colors */ +.stcv-header { + padding: .5rem 0; + color: #8d8d8d; } + +.stcv img { + cursor: pointer; + display: block; + width: 90%; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js new file mode 100644 index 00000000000..c5323ec642d --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.js @@ -0,0 +1,144 @@ +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 './StationOverview.css' +import './StationTestView.css' +import '../themes/lofar-styles.css' + +// CSS +import './StationTestChildView.css'; + + + +class StationTestChildViewC extends Component { + + state = { + modal: false, + modal_url: "" + } + + toggleModal = (e) => { + this.setState({ + modal: !this.state.modal, + modal_url: e.currentTarget.src + }); + } + + unpinPanel = () => { + this.props.unpinChildPanelData(); + } + + render() { + let rows, + title; + + // 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"; + + if (this.props.data !== null) { + 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>); + } + }); + + if (err_items.length === 0) { + err_items = <li><em>See element error.</em></li>; + } + + 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 = ""; + }); + } + } + + 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> + <Table size='sm'> + <tbody> + {rows} + </tbody> + </Table> + <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" /> + </ModalBody> + </Modal> + </div>; + } +} + + +/* Add some magic; use the AutoLoadWrapper to create a HOC that handles the + auto-loading of the data for StationOverviewC. + */ +const StationTestChildView = connect(state => { + return { + ...state.station_page.child_panel, + }; +}, { + unpinChildPanelData +})(StationTestChildViewC); + + +export default StationTestChildView; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.scss new file mode 100644 index 00000000000..72e949dee62 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestChildView.scss @@ -0,0 +1,12 @@ +@import '../themes/lofar-variables.scss'; + +.stcv-header { + padding: .5rem 0; + color: $secondary-dark; +} + +.stcv img { + cursor: pointer; + display: block; + width: 90%; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestDetails.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestDetails.js new file mode 100644 index 00000000000..788d33be71f --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestDetails.js @@ -0,0 +1,26 @@ +import React, {Component} from 'react'; +import {withRouter} from "react-router"; +import {Table, Popover, PopoverHeader, PopoverBody} from 'reactstrap'; +import {unique_id} from '../utils/utils.js' +import AutoLoadWrapper from '../utils/autoLoader.js' +import './StationOverview.css' + +/** + * StationTestDetails class. + */ +class StationTestDetailsC extends Component { + + render() { + return (<div > + funny + </div>); + } + +} + +/* Add some magic; use the AutoLoadWrapper to create a HOC that handles the + auto-loading of the data for StationOverviewC. + */ +const StationTestDetails = AutoLoadWrapper(StationTestDetailsC); + +export default StationTestDetails; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.js index 3a345d78a20..ccd0605aca9 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestSummary.js @@ -4,9 +4,12 @@ import { Table, Button } from 'reactstrap'; import { unique_id } from '../utils/utils.js' import { componentErrorTypes } from '../utils/constants.js' import AutoLoadWrapper from '../utils/autoLoader.js' +import * as moment from 'moment'; +import { date_format, time_format } from '../utils/constants' //import stdata from '../testdata/station_test_summary.js' +// CSS import './StationTestSummary.css' @@ -16,12 +19,11 @@ import './StationTestSummary.css' class STSRow extends Component { renderStartDate() { - return this.props.date; + return this.props.date ? moment.utc(this.props.date).format(date_format) : ""; } renderStartTime() { - let arr = this.props.data.start_datetime.match(/T(.*):..Z/); - return arr[1]; + return this.props.data.start_datetime ? moment.utc(this.props.data.start_datetime).format(time_format) : ""; } render() { @@ -49,7 +51,7 @@ class STSRow extends Component { class ToolBar extends Component { - onErrorsOnlyClick(selected) { + onErrorsOnlyClick = () => { this.props.onChange('allErrorTypes', !this.props.allErrorTypes ); } @@ -57,8 +59,8 @@ class ToolBar extends Component { return ( <div className="sts-toolbar"> { this.props.allErrorTypes - ? <Button color="info" size="xs" onClick={() => this.onErrorsOnlyClick()} active>All types</Button> - : <Button color="info" size="xs" onClick={() => this.onErrorsOnlyClick()}>All types</Button> + ? <Button color="info" size="xs" onClick={this.onErrorsOnlyClick} active>All types</Button> + : <Button color="info" size="xs" onClick={this.onErrorsOnlyClick}>All types</Button> } </div> ); @@ -78,7 +80,7 @@ class StationTestSummaryC extends Component { }; /* Handle changes of selected filters in the ToolBar */ - onToolbarChange(key, value) { + onToolbarChange = (key, value) => { let obj = {}; obj[key] = value; this.setState(obj); @@ -152,15 +154,22 @@ class StationTestSummaryC extends Component { return th; } - render() { + // Do not (re)render when data is loading (performance improvement) + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.isLoading) { + return false; + } + return true; + } + render() { this.setActiveErrorTypes(); return ( <React.Fragment> <h5 className="react-grid-item-header"> Station test summary - <ToolBar onChange={(key,value) => this.onToolbarChange(key,value) } allErrorTypes={this.state.allErrorTypes} /> + <ToolBar onChange={this.onToolbarChange} allErrorTypes={this.state.allErrorTypes} /> </h5> <div className="react-grid-item-body"> <Table bordered hover size="sm" className="sts-table"> diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css new file mode 100644 index 00000000000..395189869af --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.css @@ -0,0 +1,95 @@ +/* COLORS */ +/* Color palette interface (created with https://material.io/tools/color/) */ +/* font color */ +/* font color */ +/* Data colors */ +.stv-table { + font-size: .9rem; } + +.stv-table.table-sm td, .stv-table.table-sm th { + padding: .1rem; + min-width: 1.8em; } + +.stv-tableheader { + position: relative; + background-color: white; + text-align: center; } + +.stv-rtsm-summary-row { + background-color: #eeeeee; } + +.stv-component-status { + text-align: center; + font-size: 90%; } + +.stv-component-status.highlight, +.stv-testline.highlight { + background-color: #8d8d8d !important; + color: white; } + +.stv-testline.highlight td:first-child::before { + content: "> "; } + +.stv-testline-header { + width: 12rem !important; + min-width: 12rem !important; } + +.stv-component-status:hover { + color: #fff; + background-color: #8d8d8d; + border-color: #8d8d8d; } + +.stv-rtsm-summary-badge { + background-color: #ffcd74; + text-shadow: 1px 2px white; + font-size: 80%; + text-align: center; } + +.stv-rtsm-summary-row .row-header { + width: 12rem !important; + min-width: 12rem !important; + cursor: pointer; } + +.tab-navbar { + min-height: 2em !important; } + +.clickable-nav-link, .clickable-tab-active, .clickable-tab-unactive { + border-style: none; } + +.clickable-tab-active { + color: white !important; + background-color: #a7689d; } + +.clickable-tab { + cursor: pointer; + color: #490f44; } + +.clickable-tab:hover { + color: #a7689d; } + +@keyframes animation-open { + from { + transform: rotate(0deg); } + to { + transform: rotate(180deg); } } + +@keyframes animation-close { + from { + transform: rotate(180deg); } + to { + transform: rotate(0deg); } } + +.stv-rtsm-summary-row .row-header-dropdownbutton { + display: inline; + float: right; + animation: animation-close; + animation-duration: 100ms; + animation-iteration-count: 1; + animation-timing-function: linear; } + +.stv-rtsm-summary-row .row-header-dropdownbutton-up { + transform: rotate(180deg); + animation: animation-open; + animation-duration: 100ms; + animation-iteration-count: 1; + animation-timing-function: linear; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js new file mode 100644 index 00000000000..741261fec8c --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.js @@ -0,0 +1,492 @@ +import React, { + Component +} from 'react'; +import {connect} from "react-redux"; +import {setChildPanelData} from '../redux/actions/stationOverviewPageActions' + +import { + Badge, + NavItem, + NavLink, + TabPane, + TabContent, + Nav +} from 'reactstrap'; +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 ReactTableContainer from "react-table-container"; +import moment from 'moment'; + +// 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} + } +} + + +class GenericStatus extends Component { + + getClass(){ + let cls = 'stv-component-status'; + if (this.props.doHighlight) { + cls += " highlight"; + } + if (this.props.n_errors > 0) { + cls += ' so-serious' + } else { + cls += ' so-good' + } + return cls; + } + + onMouseOver = (e) => { + e.stopPropagation(); + if (this.props.n_errors > 0) { + this.props.onSelect(this.props.errors_per_polarization); + } + } + + onMouseOut = (e) => { + e.stopPropagation(); + if (this.props.n_errors > 0) { + this.props.onSelect(null); + } + + } + + // left-click with mouse + onClick = () => { + console.log('click (TODO, switch to tiles page)'); + } + + // right-click with mouse + onContextMenu = (e) => { + e.preventDefault(); + if (this.props.n_errors > 0) { + this.props.onSelect(this.props.errors_per_polarization, 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>; + + return jsx; + } +} + + +/** + * TestLine: renders a table row with the results of one station test or RTSM run. + */ +class TestLineC extends Component { + + doHighlight = false; + + formatDate(date){ + return moment(date).format(datetime_format); + } + + shouldHighlight() { + const props = this.props, + data = 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); + } + + shouldComponentUpdate(nextProps, nextState) { + // this.doHighlight will only be true for the previously and currently selected row + if (nextProps.highlightData !== this.props.highlightData && ! this.doHighlight) { + return false; + } + return true; + } + + onSelect = (data, doPin) => { + if (doPin) { + 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]) + } + } + + 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 rendered_component_errors; + } + + render() { + const date = this.formatDate(this.props.data.end_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> + } +} + +// TestLine is connected to Redux store +const TestLine = connect(state => { + return { + ...state.station_page.main_panel + }; +}, { + setChildPanelData +})(TestLineC); + + + +class RTSMSummaryLine extends Component { + + constructor(props){ + super(props); + this.state = {displaySingleTests: false} + } + + 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}/>) + } + + 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 = {} + 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) + + 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(summary).forEach(item => summary[item] /= n_tests/50. ) + 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 + }); + } + + 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 + + if (this.state.displaySingleTests){ + all_rtsm = this.props.data.map((item, key) => this.renderTestLine(key, item, component_id_list)) + } + + 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; + } +} + +/** + * ComponentClass; renders a table of station tests and rtsm data for a component (HBA, RSP, LBH, etc.) + * + * Props: + * station_type: C, R or I + * type: component type + * data: Data for this component + */ +class ComponentClass extends Component { + + 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 + 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_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}/>) + } + + 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){ + const component_id_list = this.computeComponentIDList(this.props.type) + const rows = [] + var tmp_rtsm_set = [] + + 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) + } + 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)); + } + } + + 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 + } + + render() { + const rows = this.renderTestLines(this.props.data) + + const jsx = ( + <ReactTableContainer width="100%" height="79vh"> + <table className="stv-table table-sm table-hover table-bordered"> + {this.renderHeader()} + <tbody> + {rows} + </tbody> + </table> + </ReactTableContainer> + ) + return jsx; + } +} +/** + * StationTestView class. + */ +class StationTestViewC extends Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.state = { + activeTab: undefined + }; + } + + toggle(tab) { + if (this.state.activeTab !== tab) { + this.setState({ + activeTab: tab + }); + } + } + + componentDidUpdate(){ + this.setDefaultTabIfUndefined() + } + + setDefaultTabIfUndefined(){ + const firstComponent = Object.keys(this.props.data)[0] + if(this.state !== undefined && this.state.activeTab === undefined && firstComponent !== undefined){ + this.setState({ + activeTab: firstComponent + }) + } + } + + 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) { + if (nextProps.isLoading) { + return false; + } + return true; + } + + render() { + //console.log("render main"); + const stationType = LOFARDefinitions.stationTypeFromName(this.props.selectedStation); + + 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> + ); + + return ( + <div> + <Nav tabs className="component-type-selector"> + {navBar} + </Nav> + <TabContent activeTab={this.state.activeTab}> + {componentListTabs} + </TabContent> + </div>); + } + + +} + +/* Add some magic; use the AutoLoadWrapper to create a HOC that handles the + auto-loading of the data for StationOverviewC. + */ +const StationTestViewController = connect(state => { + return { + selectedStation: state.mainFilters.selectedStation + }; +})(StationTestViewC); + +const StationTestView = AutoLoadWrapper(StationTestViewController); + +export default StationTestView; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss new file mode 100644 index 00000000000..e155759eb20 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/components/StationTestView.scss @@ -0,0 +1,119 @@ +@import '../themes/lofar-variables.scss'; + + + +.stv-table { + font-size: .9rem; +} + +.stv-table.table-sm td, .stv-table.table-sm th { + padding: .1rem; + min-width: 1.8em; +} + +.stv-tableheader { + position: relative; + background-color: white; + text-align: center; +} + +.stv-rtsm-summary-row { + background-color: #eeeeee; +} + +.stv-component-status{ + text-align: center; + font-size: 90%; +} +.stv-component-status.highlight, +.stv-testline.highlight { + background-color: $secondary-dark!important; + color: $secondary-color; +} +.stv-testline.highlight td:first-child::before { + content: "> " +} +.stv-testline-header { + width: 12rem !important; + min-width: 12rem !important; +} + +.stv-component-status:hover { + color: #fff; + background-color: $secondary-dark; + border-color: $secondary-dark; +} + +.stv-rtsm-summary-badge{ + background-color: $alarming; + text-shadow: 1px 2px white; + font-size: 80%; + text-align: center; +} + +.stv-rtsm-summary-row .row-header { + width: 12rem !important; + min-width: 12rem !important; + cursor: pointer; +} + +.tab-navbar { + min-height: 2em !important; +} + +.clickable-nav-link{ + border-style: none; +} + +.clickable-tab-active{ + @extend .clickable-nav-link; + color: white !important; + background-color: $primary-light; +} + +.clickable-tab-unactive{ + @extend .clickable-nav-link; + +} + +.clickable-tab { + cursor: pointer; + color: $primary-dark; +} + +.clickable-tab:hover { + color: $primary-light; +} + +@keyframes animation-open { + from { + transform: rotate(0deg); + } to { + transform: rotate(180deg); + } +} + +@keyframes animation-close { + from { + transform: rotate(180deg); + } to { + transform: rotate(0deg); + } +} + +.stv-rtsm-summary-row .row-header-dropdownbutton { + display: inline; + float: right; + animation: animation-close; + animation-duration: 100ms; + animation-iteration-count: 1; + animation-timing-function: linear; +} + +.stv-rtsm-summary-row .row-header-dropdownbutton-up { + transform: rotate(180deg); + animation: animation-open; + animation-duration: 100ms; + animation-iteration-count: 1; + animation-timing-function: linear; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/index.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/index.js index 560ea3fe281..b7894b80b43 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/index.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/index.js @@ -7,12 +7,12 @@ import registerServiceWorker from './registerServiceWorker'; // Redux import { Provider } from "react-redux"; -import store from "./redux/store.js"; +import { store } from "./redux/store.js"; ReactDOM.render( <Provider store={store}> - <App /> + <App /> </Provider>, document.getElementById('root') ); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/DetailsPage.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/DetailsPage.js index d7f531f9224..047689ab7e1 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/DetailsPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/DetailsPage.js @@ -1,21 +1,14 @@ import React, { Component } from 'react'; import Header from '../components/header.js' -var data = [ - 'cs001': 2, - 'cs002': 2, - 'cs003': 2, -] class DetailsPage extends Component { render() { - var list = data.map((item) => <div>{item}</div>); return ( <div> <Header active_page={this.props.location} /> <div>Details Overview!</div> - <div>{list}</div> </div> ); } 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 aa0cf629449..c0afbcd0c9d 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/LandingPage.js @@ -1,24 +1,29 @@ import React, {Component} from 'react'; import Header from '../components/header.js' -import StationOverview from '../components/StationOverview.js' -import StationTestSummary from '../components/StationTestSummary.js' -import LatestObservations from '../components/LatestObservations.js' -import StationStatistics from '../components/StationStatistics.js' +import StationOverview from '../components/StationOverview.js'; +import StationTestSummary from '../components/StationTestSummary.js'; +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 { setNewLayout } from "../redux/actions/landingPageActions"; -import { setPeriod, setStationGroup, setErrorsOnly } from "../redux/actions/mainFiltersActions" +import { setPeriod, setStationGroup, setErrorsOnly, setErrorTypes } from "../redux/actions/mainFiltersActions"; +import { composeQueryString } from '../utils/utils.js'; +import { createGridPanel } from '../utils/grid.js'; + +import MultiSelectDropdown from '../components/MultiSelectDropdown.js' import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import '../themes/lofar-styles.css'; -import { composeQueryString } from '../utils/utils.js' const ResponsiveGridLayout = WidthProvider(Responsive); + /** * Class to display a secondary header for selecting data filters. * The state is managed by the LandingPage class. @@ -38,8 +43,15 @@ class ToolBarC extends Component { this.props.setPeriod(i); } + onSelectionErrorTypes(errorTypes) { + this.props.setErrorTypes(errorTypes) + } + render() { - return (<div className="landing-page-toolbar"> + + 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={{ @@ -63,6 +75,14 @@ class ToolBarC extends Component { <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>); } @@ -70,14 +90,16 @@ class ToolBarC extends Component { const mapStateToPropsToolBar = state => { return { - ...state.mainFilters + ...state.mainFilters, + ...state.appInitData }; }; const mapDispatchToPropsToolBar = { setStationGroup, setErrorsOnly, - setPeriod + setPeriod, + setErrorTypes, }; const ToolBar = connect(mapStateToPropsToolBar, mapDispatchToPropsToolBar)(ToolBarC); @@ -85,47 +107,35 @@ const ToolBar = connect(mapStateToPropsToolBar, mapDispatchToPropsToolBar)(ToolB class LandingPageC extends Component { - /* Boiler plate for a Grid panel. - Looks like a React component but is ordinary function! */ - createGridPanel(props) { - let body = props.body; - if (props.renderHeader) { - body = <React.Fragment> - <h5 className="react-grid-item-header">{props.title}</h5> - <div className="react-grid-item-body"> - {props.body} - </div> - </React.Fragment>; - } - return (<div key={props.key}> - {body} - </div>); - } - getStationOverviewURL() { - const url = '/api/view/ctrl_stationoverview?format=json' - - const parametersString = - composeQueryString({ station_group: this.props.selectedStationGroup, - errors_only: this.props.errorsOnly, - n_station_tests: 4, - n_rtsm: 4 - }) - return `${url}&${parametersString}`; + const url = '/api/view/ctrl_stationoverview'; + + const parametersString = composeQueryString({ + format: 'json', + station_group: this.props.selectedStationGroup, + errors_only: this.props.errorsOnly, + n_station_tests: 4, + n_rtsm: 6, + error_types: this.props.selectedErrorTypes + }); + + return `${url}?${parametersString}`; } getStationTestSummaryURL() { - const url = '/api/view/ctrl_stationtestsummary?format=json' - - const parameters = {} - // ---- Mandatory parameters - parameters.lookback_time = this.props.period - // ---- Optional parameters - parameters.station_group = this.props.selectedStationGroup - parameters.errors_only = this.props.errorsOnly - - const parametersString = composeQueryString(parameters) - return `${url}&${parametersString}`; + const url = '/api/view/ctrl_stationtestsummary'; + + const parametersString = composeQueryString({ + // ---- Mandatory parameters + format: 'json', + lookback_time: this.props.period, + // ---- Optional parameters + station_group: this.props.selectedStationGroup, + errors_only: this.props.errorsOnly, + error_types: this.props.selectedErrorTypes + }); + + return `${url}?${parametersString}`; } getLatestObservationURL() { @@ -138,6 +148,7 @@ class LandingPageC extends Component { // ---- Optional parameters parameters.station_group = this.props.selectedStationGroup parameters.errors_only = this.props.errorsOnly + parameters.error_types = this.props.selectedErrorTypes const parametersString = composeQueryString(parameters) return `${url}&${parametersString}`; @@ -162,6 +173,7 @@ class LandingPageC extends Component { parameters.errors_only = this.props.errorsOnly parameters.station_group = this.props.selectedStationGroup parameters.test_type = test_type + parameters.error_types = this.props.selectedErrorTypes const parametersString = composeQueryString(parameters) return `${url}&${parametersString}`; @@ -170,11 +182,13 @@ class LandingPageC extends Component { return (<div> <Header active_page={this.props.location}/> <ToolBar/> - <ResponsiveGridLayout className="layout" layouts={this.props.layout.panels} measureBeforeMount={true} breakpoints={this.props.layout.breakpoints} cols={this.props.layout.cols} onResizeStop={e => this.props.setNewLayout(e)}> - {this.createGridPanel({key: "ul", renderHeader: true, title: "Station overview", body: <StationOverview url={this.getStationOverviewURL()}/>})} - {this.createGridPanel({key: "ur", renderHeader: true, title: "Latest observations", body: <LatestObservations url={this.getLatestObservationURL()}/>})} - {this.createGridPanel({key: "bl", renderHeader: false, body: <StationTestSummary url={this.getStationTestSummaryURL()}/>})} - {this.createGridPanel({key: "br", renderHeader: false, body: <StationStatistics url={this.getStationStatisticsURL()}/>})} + <ResponsiveGridLayout className="layout" layouts={this.props.layout.panels} measureBeforeMount={true} + breakpoints={this.props.layout.breakpoints} cols={this.props.layout.cols} + onResizeStop={this.props.setNewLayout}> + {createGridPanel({key: "ul", renderHeader: true, title: "Station overview", body: <StationOverview url={this.getStationOverviewURL()}/>})} + {createGridPanel({key: "ur", renderHeader: true, title: "Latest observations", body: <LatestObservations url={this.getLatestObservationURL()}/>})} + {createGridPanel({key: "bl", renderHeader: false, body: <StationTestSummary url={this.getStationTestSummaryURL()}/>})} + {createGridPanel({key: "br", renderHeader: false, body: <StationStatistics url={this.getStationStatisticsURL()}/>})} </ResponsiveGridLayout> </div>); } 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 52442eb76e2..d4f9413032a 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/pages/StationOverviewPage.js @@ -1,15 +1,219 @@ 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 moment from 'moment'; import Header from '../components/header.js' -class StationOverviewPage extends Component { - render() { - return ( - <div> - <Header active_page={this.props.location} /> - <div>STATION OVERVIEW</div> - </div> +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'; + +// 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. + */ +class ToolBarC extends Component { + + constructor(props) { + super(props); + this.handleChangeStart = this.handleChangeStart.bind(this); + this.handleChangeEnd = this.handleChangeEnd.bind(this); + this.onStationChange = this.onStationChange.bind(this); + } + + 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); + } + + // Callback for StationAutoComplete + onStationChange(station) { + //this.props.setStation(station); + store.dispatch(push(`?station=${station}`)) + } + + onSelectionErrorTypes(errorTypes) { + this.props.setErrorTypes(errorTypes) + } + + 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>); + } +} + +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 = {}; + + if (this.isParameterMissing()) { + 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 queryString = composeQueryString(parameters); + return `${baseURL}&${queryString}` + } + + isParameterMissing(){ + const stationName = this.props.selectedStation + return stationName === undefined || + stationName === "" + } + + 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 "" + } + } + setNewLayout() {} + + render() { + 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> + } + </React.Fragment> ); } } + +const StationOverviewPage = connect(state => { + return { + ...state.mainFilters + }; +})(StationOverviewPageC); + export default StationOverviewPage; 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 1ef29598eb6..674e6fc9a4e 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 @@ -1,9 +1,16 @@ import axios from 'axios'; +import { stringSort } from '../../utils/utils.js'; export const FETCH_ERRORTYPES_BEGIN = 'FETCH_ERRORTYPES_BEGIN'; export const FETCH_ERRORTYPES_SUCCESS = 'FETCH_ERRORTYPES_SUCCESS'; export const FETCH_ERRORTYPES_FAILURE = 'FETCH_ERRORTYPES_FAILURE'; +export const FETCH_STATIONS_BEGIN = 'FETCH_STATIONS_BEGIN'; +export const FETCH_STATIONS_SUCCESS = 'FETCH_STATIONS_SUCCESS'; +export const FETCH_STATIONS_FAILURE = 'FETCH_STATIONS_FAILURE'; + + +/* ERROR TYPES */ const errorTypesURL = '/api/view/ctrl_list_component_error_types'; export const fetchErrorTypesBegin = () => ({ @@ -37,3 +44,41 @@ export function fetchErrorTypes() { }); }; } + + + +/* STATIONS */ +const stationsURL = '/api/stationtests/station/?page_size=200'; + +export const fetchStationsBegin = () => ({ + type: FETCH_STATIONS_BEGIN +}); + +export const fetchStationsSuccess = stations => ({ + type: FETCH_STATIONS_SUCCESS, + payload: { stations } +}); + +export const fetchStationsFailure = error => ({ + type: FETCH_STATIONS_FAILURE, + payload: { error } +}); + +export function fetchStations() { + return dispatch => { + // Not used: dispatch(fetchstationsBegin()); + + return axios.get(stationsURL) + .then(res => { + let stations = res.data.results ? res.data.results : []; + stations.sort((a,b) => stringSort(a.name, b.name) ); + dispatch(fetchStationsSuccess(stations)); + }) + .catch(error => { + console.log("Error fetching error types: "+error); + dispatch(fetchStationsFailure(error)) + // Try again in 30s + setTimeout(() => dispatch(fetchStations()), 10000); + }); + }; +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/landingPageActions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/landingPageActions.js index 9a567db893d..9901bbe148e 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/landingPageActions.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/landingPageActions.js @@ -1,6 +1,6 @@ export const SET_COMPONENT_SIZES = "SET_COMPONENT_SIZES"; export const SET_AVERAGING_WINDOW = "SET_AVERAGING_WINDOW"; -export const SET_TEST_TYPE = "SET_TEST_TYPE"; +export const SET_STATISTICS_TEST_TYPE = "SET_STATISTICS_TEST_TYPE"; export const setNewLayout = function(newLayout) { var payload = {}; @@ -20,6 +20,6 @@ export const setStationStatisticsAveragingWindow = averagingWindow => ({ }); export const setStationStatisticsTestType = test_type => ({ - type: SET_TEST_TYPE, + type: SET_STATISTICS_TEST_TYPE, payload: { test_type } }); 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 5e208fd6d1f..c6bdd70742f 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,13 +1,22 @@ export const SET_STATION_GROUP = "SET_STATION_GROUP"; +export const SET_STATION = "SET_STATION"; export const SET_ERRORS_ONLY = "SET_ERRORS_ONLY"; 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 setStationGroup = stationGroup => ({ type: SET_STATION_GROUP, payload: { stationGroup: stationGroup } }); +export const setStation = station => ({ + type: SET_STATION, + payload: { station } +}); + export const setErrorsOnly = errorsOnly => ({ type: SET_ERRORS_ONLY, payload: { errorsOnly } @@ -17,3 +26,18 @@ export const setPeriod = period => ({ type: SET_PERIOD, payload: { period } }); + +export const setDateRange = rangeObj => ({ + type: SET_DATE_RANGE, + payload: { ...rangeObj } +}); + +export const setTestType = type => ({ + type: SET_TEST_TYPE, + payload: { type } +}); + +export const setErrorTypes = type_list => ({ + type: SET_ERROR_TYPES, + payload: { type_list } +}); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/stationOverviewPageActions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/stationOverviewPageActions.js new file mode 100644 index 00000000000..4e10214c1ab --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/actions/stationOverviewPageActions.js @@ -0,0 +1,12 @@ +export const SET_CHILD_PANEL_DATA = "SET_CHILD_PANEL_DATA"; +export const UNPIN_CHILD_PANEL_DATA = "UNPIN_CHILD_PANEL_DATA"; + +export const setChildPanelData = (data, doPin = false) => ({ + type: SET_CHILD_PANEL_DATA, + payload: { data, doPin } +}); + +export const unpinChildPanelData = () => ({ + type: UNPIN_CHILD_PANEL_DATA, + payload: null +}); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/appInitDataReducers.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/appInitDataReducers.js index 5e1b4036a0a..f4bfa0350a0 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/appInitDataReducers.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/appInitDataReducers.js @@ -1,13 +1,17 @@ import { FETCH_ERRORTYPES_BEGIN, FETCH_ERRORTYPES_SUCCESS, - FETCH_ERRORTYPES_FAILURE + FETCH_ERRORTYPES_FAILURE, + FETCH_STATIONS_SUCCESS, + FETCH_STATIONS_FAILURE } from '../actions/appInitDataActions'; const initialState = { errorTypesLoaded: false, - errorTypes: [] + errorTypes: [], + stationsLoaded: false, + stations: [] }; @@ -15,7 +19,6 @@ export default function (state = initialState, action) { switch(action.type) { case FETCH_ERRORTYPES_BEGIN: // Mark the state as "loading" so we can show a spinner or something - // Also, reset any errors. We're starting fresh. return { ...state, errorTypesLoaded: false, @@ -24,7 +27,6 @@ export default function (state = initialState, action) { case FETCH_ERRORTYPES_SUCCESS: // All done: set loading "false". - // Also, replace the items with the ones from the server return { ...state, errorTypesLoaded: true, @@ -33,7 +35,6 @@ export default function (state = initialState, action) { case FETCH_ERRORTYPES_FAILURE: // The request failed, but it did stop, so set loading to "false". - // Save the error, and we can display it somewhere return { ...state, errorTypesLoaded: false, @@ -41,6 +42,23 @@ export default function (state = initialState, action) { errorTypesError: action.payload.error }; + case FETCH_STATIONS_SUCCESS: + // All done: set loading "false". + return { + ...state, + stationsLoaded: true, + stations: action.payload.stations + }; + + case FETCH_STATIONS_FAILURE: + // The request failed, but it did stop, so set loading to "false". + return { + ...state, + stationsLoaded: false, + // don't: errorTypes: [] + stationsError: action.payload.error + }; + default: // ALWAYS have a default case in a reducer return state; 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 15d63fbdf30..c877894fe8e 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 @@ -2,10 +2,11 @@ import { combineReducers } from "redux"; import appInitData from "./appInitDataReducers"; import mainFilters from "./mainFilters"; import landingPageReducers from "./landingPageReducers"; - +import stationOverviewPageReducers from "./stationOverviewPageReducers"; export default combineReducers({ appInitData, mainFilters, - landing_page:landingPageReducers, + landing_page: landingPageReducers, + station_page: stationOverviewPageReducers }); diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/landingPageReducers.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/landingPageReducers.js index cfb5d731fbf..208a05a6fe6 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/landingPageReducers.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/landingPageReducers.js @@ -1,6 +1,6 @@ import { SET_AVERAGING_WINDOW, - SET_TEST_TYPE, + SET_STATISTICS_TEST_TYPE, SET_COMPONENT_SIZES } from '../actions/landingPageActions.js' @@ -94,7 +94,7 @@ export default function(state = initialState, action) { } }; } - case SET_TEST_TYPE: + case SET_STATISTICS_TEST_TYPE: { return { ...state, 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 80b7cc50a42..ef52320124f 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 @@ -1,40 +1,85 @@ -import { - SET_STATION_GROUP, - SET_ERRORS_ONLY, - SET_PERIOD -} from "../actions/mainFiltersActions"; - +import moment from 'moment'; +import * as a from "../actions/mainFiltersActions"; +import { LOCATION_CHANGE } from "connected-react-router"; +import queryString from 'query-string'; const initialState = { - selectedStationGroup: 'A', + selectedStationGroup: 'A', // A, C, R, I + selectedStation: '', // note: not necessarily in sync with selectedStationGroup, used at different pages + testType: 'B', // B, S, R errorsOnly: false, - period: 14 // days + period: 7, // days + startDate: moment().subtract(7, 'days'), + endDate: moment(), + selectedErrorTypes: [] }; export default function(state = initialState, action) { switch (action.type) { - case SET_STATION_GROUP: { + case LOCATION_CHANGE: { + // Location changed, check is a station parameter is present + let newState = { + ...state + }; + const values = queryString.parse( action.payload.location.search); + if ('station' in values) { + newState.selectedStation = values.station; + } + return newState; + } + + case a.SET_STATION_GROUP: { const { stationGroup } = action.payload; return { ...state, selectedStationGroup: stationGroup }; } - case SET_ERRORS_ONLY: { + case a.SET_STATION: { + const { station } = action.payload; + return { + ...state, + selectedStation: station + }; + } + case a.SET_ERRORS_ONLY: { const { errorsOnly } = action.payload; return { ...state, errorsOnly: errorsOnly }; } - case SET_PERIOD: { + case a.SET_PERIOD: { const { period } = action.payload; return { ...state, period: period }; } + case a.SET_DATE_RANGE: { + const { startDate, endDate } = action.payload; + return { + ...state, + startDate, + endDate + }; + } + case a.SET_TEST_TYPE: { + const { type } = action.payload; + return { + ...state, + testType: type + }; + } + case a.SET_ERROR_TYPES: { + const { type_list } = action.payload; + + return { + ...state, + selectedErrorTypes: type_list + } + } default: return state; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/stationOverviewPageReducers.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/stationOverviewPageReducers.js new file mode 100644 index 00000000000..75d9e46d994 --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/reducers/stationOverviewPageReducers.js @@ -0,0 +1,65 @@ +import { + SET_CHILD_PANEL_DATA, + UNPIN_CHILD_PANEL_DATA +} from '../actions/stationOverviewPageActions.js' + +//import { SET_STATION } from "../actions/mainFiltersActions"; +import { LOCATION_CHANGE } from "connected-react-router"; + +const initialState = { + + main_panel: { + highlightData: null + }, + + // Child panel showing data on mouse hover + child_panel: { + data: null, + isPinned: false + } +}; + + +export default function(state = initialState, action) { + switch (action.type) { + case SET_CHILD_PANEL_DATA: + + if (state.child_panel.isPinned && ! action.payload.doPin) { + // action.payload.doPin is only true when right-mouse click is used, so + // this is probably a mouse move event. Ignore it. + return state; + } + let newState = { + ...state, + child_panel: { + data: action.payload.data, + isPinned: action.payload.doPin + } + }; + + if (action.payload.doPin) { + newState.main_panel = { + highlightData: action.payload.data + }; + } + return newState; + + case UNPIN_CHILD_PANEL_DATA: + case LOCATION_CHANGE: + // Note: LOCATION_CHANGE is triggered by changes in the main filters (e.g. selectedStation) + // then also reset the child panel + return { + ...state, + main_panel: { + highlightData: null + }, + child_panel: { + data: null, + isPinned: false + } + }; + + default: + return state; + } +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/store.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/store.js index caceabe064b..18df1b359cb 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/store.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/redux/store.js @@ -1,14 +1,24 @@ import { createStore, applyMiddleware, compose } from "redux"; import rootReducer from "./reducers/index.js"; import thunk from 'redux-thunk'; +import { createBrowserHistory } from 'history'; +import { connectRouter, routerMiddleware } from 'connected-react-router' + // Needed for the chrome Redux DevTools extension const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +const history = createBrowserHistory(); + const store = createStore( - rootReducer, + connectRouter(history)(rootReducer), /* preloadedState, */ - composeEnhancers( applyMiddleware(thunk) ) + composeEnhancers( + applyMiddleware( + routerMiddleware(history), + thunk + ) + ) ); -export default store; +export { store, history }; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/ctrl_station_component_errors.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/ctrl_station_component_errors.js new file mode 100644 index 00000000000..cd27f0efb7f --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/testdata/ctrl_station_component_errors.js @@ -0,0 +1,35 @@ +// /view/ctrl_station_component_errors +// parameters: +// station_name: str #required +// from_date: date #required +// to_date: date #required +// test_type: [R,S,B] #optional default B +// response: +// { +// COMPONENT_TYPE: [ +// {test_type: R,S, +// start_date: end_date +// end_date: start_date +// component_errors: { +// rcu_id: [{ error_type: whatever, +// details:{...}}] +// } +// } +// ,... +// ] +// } + +export const data = { + "HBA": [ + { + "test_type": 'R', + "start_datetime": "2018-10-28T17:30:00Z", + "end_datetime": "2018-10-28T20:27:17Z", + "component_errors": { + // rcu_id: [{ error_type: whatever, + // details:{...}}] + // } + } + } + ] +}; 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 11f694951bd..f48fba4d3e0 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 @@ -10,7 +10,7 @@ border-radius: 0.25rem; } .react-grid-item-header { - color: #333333; + color: #8d8d8d; background-color: #e1e1e1; font-weight: bold; padding: .3rem; @@ -26,18 +26,23 @@ .react-grid-item > .react-resizable-handle::after { border-color: #bce8f1; } +.autoloader-container { + position: relative; + height: 100%; } + /* autoLoader; CSS spinner*/ -.alLoading { +.autoloader-loading { position: absolute; top: 2.25em; /* Check with height of .react-grid-item-header */ left: 0.25em; border: 0.55rem solid #e1e1e1; - border-top: 0.55rem solid #8d8d8d; + border-top: 0.55rem solid #a7689d; border-radius: 50%; width: 3rem; height: 3rem; - animation: spin 2s linear infinite; } + animation: spin 2s linear infinite; + z-index: 1000; } @keyframes spin { 0% { @@ -61,33 +66,53 @@ text-align: center; font-weight: 700; } -.landing-page-toolbar { +.toolbar-top { background-color: #a7689d; color: #ffffff; padding: 5px 10px; /* Note: same padding as ResponsiveGridLayout */ width: 100%; - overflow: auto; } + overflow: visible; } -.landing-page-toolbar { +.toolbar-top { font-weight: 500; } -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled).active, +.toolbar-top .btn-info:not(:disabled):not(.disabled).active, .sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled):active, -.landing-page-toolbar .show > .btn-info.dropdown-toggle { +.toolbar-top .btn-info:not(:disabled):not(.disabled):active, +.toolbar-top .show > .btn-info.dropdown-toggle { color: white; background-color: #8d8d8d; border-color: #8d8d8d; } -.landing-page-toolbar .btn-info, +.toolbar-top .btn-info, .sts-toolbar .btn-info { color: white; background-color: #bdbdbd; border-color: #bdbdbd; } -.landing-page-toolbar .btn-info:hover, +.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; } + +.react-select-container { + min-width: 20rem; } + +.form-input { + display: inline-block; } + +.form-input button { + font-size: .8rem; + background-color: white !important; + color: black !important; + border: none; } + +.form-input button:hover { + background-color: #e1e1e1 !important; } 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 879601175c1..ae80678c68f 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 @@ -7,7 +7,7 @@ body { } $griditem: $secondary-light; -$griditem-color: #333333; +$griditem-color: $secondary-dark; .react-grid-item { background-color: white; @@ -33,17 +33,23 @@ $griditem-color: #333333; border-color: #bce8f1; } + +.autoloader-container { + position: relative; + height: 100%; +} /* autoLoader; CSS spinner*/ -.alLoading { +.autoloader-loading { position: absolute; top: 2.25em; /* Check with height of .react-grid-item-header */ left: 0.25em; border: .55rem solid $secondary-light; - border-top: .55rem solid $secondary-dark; + border-top: .55rem solid $primary-light; border-radius: 50%; width: 3rem; height: 3rem; animation: spin 2s linear infinite; + z-index: 1000; } @keyframes spin { 0% { transform: rotate(0deg); } @@ -68,36 +74,63 @@ $griditem-color: #333333; font-weight: 700; } -.landing-page-toolbar { +.toolbar-top { background-color: $primary-light; color: $primary-color; padding: 5px 10px; /* Note: same padding as ResponsiveGridLayout */ width: 100%; - overflow: auto; + overflow: visible; } -.landing-page-toolbar { +.toolbar-top { font-weight: 500; } -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled).active, +.toolbar-top .btn-info:not(:disabled):not(.disabled).active, .sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled):active, -.landing-page-toolbar .show>.btn-info.dropdown-toggle { +.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; } -.landing-page-toolbar .btn-info, +.toolbar-top .btn-info, .sts-toolbar .btn-info { color: $secondary-color; background-color: $secondary; border-color: $secondary; } -.landing-page-toolbar .btn-info:hover, +.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; + + border: 1px solid $secondary-dark; + +} +.react-select-container{ + min-width: 20rem; +} + +.form-input { + display: inline-block; +} + +.form-input button{ + font-size: .8rem; + background-color: white !important; + color: black !important; + border: none; + +} + +.form-input button:hover { + background-color: $secondary-light !important; +} 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 7f9870222ee..a40b42bb57c 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/themes/lofar.css @@ -6329,7 +6329,7 @@ a.text-dark:hover, a.text-dark:focus { border-radius: 0.25rem; } .react-grid-item-header { - color: #333333; + color: #8d8d8d; background-color: #e1e1e1; font-weight: bold; padding: .3rem; @@ -6345,18 +6345,23 @@ a.text-dark:hover, a.text-dark:focus { .react-grid-item > .react-resizable-handle::after { border-color: #bce8f1; } +.autoloader-container { + position: relative; + height: 100%; } + /* autoLoader; CSS spinner*/ -.alLoading { +.autoloader-loading { position: absolute; top: 2.25em; /* Check with height of .react-grid-item-header */ left: 0.25em; border: 0.55rem solid #e1e1e1; - border-top: 0.55rem solid #8d8d8d; + border-top: 0.55rem solid #a7689d; border-radius: 50%; width: 3rem; height: 3rem; - animation: spin 2s linear infinite; } + animation: spin 2s linear infinite; + z-index: 1000; } @keyframes spin { 0% { @@ -6380,33 +6385,53 @@ a.text-dark:hover, a.text-dark:focus { text-align: center; font-weight: 700; } -.landing-page-toolbar { +.toolbar-top { background-color: #a7689d; color: #ffffff; padding: 5px 10px; /* Note: same padding as ResponsiveGridLayout */ width: 100%; - overflow: auto; } + overflow: visible; } -.landing-page-toolbar { +.toolbar-top { font-weight: 500; } -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled).active, +.toolbar-top .btn-info:not(:disabled):not(.disabled).active, .sts-toolbar .btn-info:not(:disabled):not(.disabled).active, -.landing-page-toolbar .btn-info:not(:disabled):not(.disabled):active, -.landing-page-toolbar .show > .btn-info.dropdown-toggle { +.toolbar-top .btn-info:not(:disabled):not(.disabled):active, +.toolbar-top .show > .btn-info.dropdown-toggle { color: white; background-color: #8d8d8d; border-color: #8d8d8d; } -.landing-page-toolbar .btn-info, +.toolbar-top .btn-info, .sts-toolbar .btn-info { color: white; background-color: #bdbdbd; border-color: #bdbdbd; } -.landing-page-toolbar .btn-info:hover, +.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; } + +.react-select-container { + min-width: 20rem; } + +.form-input { + display: inline-block; } + +.form-input button { + font-size: .8rem; + background-color: white !important; + color: black !important; + border: none; } + +.form-input button:hover { + background-color: #e1e1e1 !important; } diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js new file mode 100644 index 00000000000..f1c0459dbdc --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/LOFARDefinitions.js @@ -0,0 +1,29 @@ +export const HBATilesPerStationType = { + C: 48, + R: 48, + I: 96 +} + +export const LBHAntennasPerStationType = { + C: 48, + R: 48, + I: 48 +} + +export const LBLAntennasPerStationType = { + C: 48, + R: 48, + I: 48 +} + +export function stationTypeFromName(stationName){ + if(stationName === undefined) return undefined + if(stationName.includes('CS')) return 'C'; + if(stationName.includes('RS')) return 'R'; + return 'I' +} + +export function getInspectPageURLFromSASid(sasId){ + const url =`https://proxy.lofar.eu/inspect/HTML/${sasId}/index.html` + return url +} diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/autoLoader.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/autoLoader.js index 953aff96601..29dfba285ad 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/autoLoader.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/autoLoader.js @@ -1,6 +1,12 @@ import React from 'react'; import axios from 'axios'; +import {Alert} from 'reactstrap'; + + +function Spinner() { + return <div className="autoloader-loading"></div>; +} function AutoLoadWrapper(WrappedComponent) { @@ -13,23 +19,32 @@ function AutoLoadWrapper(WrappedComponent) { state = { data: [], - isLoading: false, - prevUrl: null + isLoading: false, // A request is currenly pending (true/false) + hasError: false, // A request got an unexpected error (true/false) + strError: "", // When hasError=true, this item contains the error text + prevUrl: null // The last URL that has been loaded, used to detect changes } static defaultProps = { reloadInterval: 60000, - url: '' + url: "" // When url = "" no request will be issued and data will be set to [] } - /* Called when props changed, before the render phase */ + /* Called when props changed, before the render phase */ static getDerivedStateFromProps(props, state) { // Store prevUrl in state so we can compare when props change. if (props.url !== state.prevUrl) { - return { - isLoading: true, - prevUrl: props.url + let newState = { + prevUrl: props.url, + isLoading: true }; + + if (props.url === "") { + newState.isLoading = false; + newState.data = []; + } + + return newState; } // No state update necessary @@ -38,6 +53,10 @@ function AutoLoadWrapper(WrappedComponent) { fetchData() { + if (this.props.url === "") { + return; + } + // Set loading state if (! this.state.isLoading) { this.setState({ @@ -45,10 +64,8 @@ function AutoLoadWrapper(WrappedComponent) { }); } - // Create cancellation token - if (typeof this._source !== typeof undefined) { - this._source.cancel('Operation canceled due to new request.') - } + // Create new cancellation token + this.cancelFetchData('Operation canceled due to new request.'); this._source = axios.CancelToken.source(); // Handle the request @@ -57,17 +74,20 @@ function AutoLoadWrapper(WrappedComponent) { }).then(res => { this.setState({ data: res.data, - isLoading: false + isLoading: false, + hasError: false }); }).catch(error => { if (axios.isCancel(error)) { console.log('Request canceled: ', error); } else { - // TODO render console.log(error); this.setState({ - isLoading: false + isLoading: false, + hasError: true, + strError: error.message, + data: [] }); } }).then(() => { @@ -76,6 +96,13 @@ function AutoLoadWrapper(WrappedComponent) { }); } + cancelFetchData(reason) { + if (typeof this._source !== typeof undefined) { + this._source.cancel(reason) + this._source = undefined; + } + } + componentDidMount() { this.fetchData(); this._intervalId = setInterval(() => this.fetchData(), this.props.reloadInterval); @@ -89,19 +116,25 @@ function AutoLoadWrapper(WrappedComponent) { componentWillUnmount() { clearInterval(this._intervalId); + this.cancelFetchData('Component is unmounting'); } render() { let loadingHtml = ""; + let errorHtml = ""; if (this.state.isLoading) { - loadingHtml = <div className="alLoading"></div>; + loadingHtml = <Spinner />; + } + if (this.state.hasError) { + errorHtml = <Alert className="py-1" color="danger"><strong>Error: </strong>{this.state.strError}</Alert>; } return( - <React.Fragment> + <div className="autoloader-container"> {loadingHtml} - <WrappedComponent data={this.state.data} {...this.props} /> - </React.Fragment> + {errorHtml} + <WrappedComponent data={this.state.data} isLoading={this.state.isLoading} {...this.props} /> + </div> ); } }; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/constants.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/constants.js index a452dac6284..75bf15078a1 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/constants.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/constants.js @@ -1,6 +1,6 @@ // List of known component error types and their abbreviations -const componentErrorTypes = { +export const componentErrorTypes = { "BOARD": "BD", "C_SUMMATOR": "CS", "CHECKSRV": "CK", @@ -23,4 +23,6 @@ const componentErrorTypes = { "VOLTAGE": "VO" }; -export { componentErrorTypes }; +export const date_format = 'YYYY-MM-DD'; +export const time_format = 'HH:mm'; +export const datetime_format = 'YYYY-MM-DD HH:mm'; diff --git a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/grid.js b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/grid.js new file mode 100644 index 00000000000..a1b168089df --- /dev/null +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/grid.js @@ -0,0 +1,21 @@ +import React from 'react'; + +/* Boiler plate for a Grid panel. + Looks like a React component but is ordinary function! */ +function createGridPanel(props) { + let body = props.body; + if (props.renderHeader) { + body = <React.Fragment> + <h5 className="react-grid-item-header">{props.title}</h5> + <div className="react-grid-item-body"> + {props.body} + </div> + </React.Fragment>; + } + return (<div key={props.key}> + {body} + </div>); +} + + +export { createGridPanel }; 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 638bec56e69..bdaf6ab2677 100644 --- a/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js +++ b/LCU/Maintenance/MDB_WebView/maintenancedb_view/src/utils/utils.js @@ -10,11 +10,27 @@ function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() } -export function composeQueryString(params) { +function composeQueryString(params) { const parametersString = Object.keys(params).map((key) => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) }).join('&'); return `${parametersString}`; } -export { unique_id, capitalize }; +// Function to pass to Array.sort for sorting strings. Can be used +// to sort objects that have a string key. E.g. my_array is +// an array of objects that have a name key: +// my_array.sort((a,b) => stringSort(a.name, b.name) ); +function stringSort(a, b, caseInsensitive = true) { + let A = caseInsensitive ? a.toUpperCase() : a; + let B = caseInsensitive ? b.toUpperCase() : b; + if (A < B) { + return -1; + } + if (A > B) { + return 1; + } + return 0; +} + +export { unique_id, capitalize, composeQueryString, stringSort }; -- GitLab