diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..59dcb89 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } + } + ], + "@babel/preset-react" + ] +} diff --git a/README.md b/README.md index 34d96f9..dd3ae2f 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,6 @@ WatsonC Vidi extension `test.py` - runs tests for the `intersectiontool.py` -The `FOHM_layers_v2.npy` was not added to repo, please download it from https://s3-eu-west-1.amazonaws.com/mapcentia-tmp/ProfileTool.zip and put in `/extensions/watsonc/scripts` folder. \ No newline at end of file +The `FOHM_layers_v2.npy` was not added to repo, please download it from https://s3-eu-west-1.amazonaws.com/mapcentia-tmp/ProfileTool.zip and put in `/extensions/watsonc/scripts` folder. + +test diff --git a/browser/PlotManager.js b/browser/PlotManager.js index 56358a9..9950fd2 100644 --- a/browser/PlotManager.js +++ b/browser/PlotManager.js @@ -10,112 +10,6 @@ class PlotManager { this.apiUrlLocal = `/api/key-value/` + window.vidiConfig.appDatabase; } - dehydratePlots(plots) { - plots.map((plot, index) => { - delete plots[index].measurements; - delete plots[index].measurementsCachedData; - }); - - return plots; - } - - hydratePlotsFromIds(plots) { - if (typeof plots === "undefined") return; - plots = plots.filter(e => !!e.id) - return new Promise((methodResolve, methodReject) => { - let hydrationPromises = []; - plots.map((plot, index) => { - let hydrateRequest = new Promise((resolve, reject) => { - $.ajax({ - url: `${this.apiUrlLocal}/${plot.id}`, - method: 'GET', - dataType: 'json', - contentType: 'application/json; charset=utf-8', - success: (body) => { - if (body.success) { - resolve(body.data); - } else { - throw new Error(`Failed to perform operation`, body); - } - }, - error: error => { - console.error(error); - reject(`Failed to query keyvalue API`); - } - }); - }); - - hydrationPromises.push(hydrateRequest); - }); - - Promise.all(hydrationPromises).then(results => { - plots.map((item, index) => { - results.map((dataItem) => { - if (dataItem.key === item.id && typeof dataItem.value !== "undefined") { - plots[index] = JSON.parse(dataItem.value); - delete plots[index].measurementsCachedData; - } - }); - }); - - plots.map((item, index) => { - if (typeof item === "object" && (`measurements` in item === false || !item.measurements - || `measurementsCachedData` in item === false || !item.measurementsCachedData)) { - console.warn(`The ${item.id} plot was not properly populated`, item); - } - }); - // Filter non objects. A non object can occur when time a project time series is deleted from from key value store - plots = plots.filter((item)=>{ - return (typeof item === 'object') - }); - methodResolve(plots); - }).catch(methodReject); - }); - } - - hydratePlotsFromUser() { - return new Promise((methodResolve, methodReject) => { - let hydrationPromises = []; - let userId = session.getUserName(); - $.ajax({ - url: `${this.apiUrlLocal}/?like=watsonc_plot_%&filter='{userId}'='${userId}'`, - method: 'GET', - dataType: 'json', - contentType: 'application/json; charset=utf-8', - success: (body) => { - if (body.success) { - let results = []; - body.data.map(item => { - results.push(item); - }); - let plots = []; - results.map((item, index) => { - plots[index] = JSON.parse(item.value); - delete plots[index].measurementsCachedData; - }); - - plots.map((item, index) => { - if (`measurements` in item === false || !item.measurements - || `measurementsCachedData` in item === false || !item.measurementsCachedData) { - console.warn(`The ${item.id} plot was not properly populated`, item); - } - }); - - methodResolve(plots); - - - } else { - throw new Error(`Failed to perform operation`, body); - } - }, - error: error => { - console.error(error); - reject(`Failed to query keyvalue API`); - } - }); - - }); - } create(title) { return new Promise((resolve, reject) => { @@ -126,7 +20,8 @@ class PlotManager { title, userId: session.getUserName(), measurements: [], - measurementsCachedData: {} + measurementsCachedData: {}, + relations: {} }; $.ajax({ diff --git a/browser/ProfileManager.js b/browser/ProfileManager.js index 9c0bfe3..792c329 100644 --- a/browser/ProfileManager.js +++ b/browser/ProfileManager.js @@ -1,68 +1,87 @@ -/** - * Abstraction class for storing profiles in the key-value storage - */ - -import axios from 'axios'; - -class ProfileManager { - constructor() { - let hostname = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : ''); - //this.apiUrl = hostname + `/api/key-value/` + window.vidiConfig.appDatabase; - this.apiUrl = hostname + `/api/extension/watsonc/${window.vidiConfig.appDatabase}/profiles`; - } - - getAll() { - return new Promise((resolve, reject) => { - $.ajax({ - url: this.apiUrl, - method: 'GET', - dataType: 'json' - }).then(response => { - let parsedData = []; - response.map(item => { - parsedData.push(JSON.parse(item.value)); - }); - - resolve(parsedData); - }, (jqXHR) => { - console.error(`Error occured while refreshing profiles list`); - reject(`Error occured while refreshing profiles list`); - }); - }); - } - - create(savedProfile) { - return new Promise((resolve, reject) => { - axios.post(`/api/extension/watsonc/profile`, savedProfile).then(response => { - if (response.data) { - savedProfile.data = response.data; - axios.post(`${this.apiUrl}`, savedProfile).then(response => { - let data = JSON.parse(response.data.data.value); - resolve(data); - }).catch(error => { - console.error(error); - reject(error); - }); - } else { - console.error(`Unable to generate plot data`); - reject(`Unable to generate plot data`); - } - }).catch(error => { - console.error(`Error occured during plot generation`, error); - reject(error); - }); - }); - } - - delete(profileKey) { - return new Promise((resolve, reject) => { - if (profileKey) { - axios.delete(`${this.apiUrl}/${profileKey}`).then(resolve).catch(reject); - } else { - reject(`Empty profile identifier was provided`); - } - }); - } -} - -export default ProfileManager; +/** + * Abstraction class for storing profiles in the key-value storage + */ + +import axios from "axios"; + +class ProfileManager { + constructor() { + let hostname = + location.protocol + + "//" + + location.hostname + + (location.port ? ":" + location.port : ""); + //this.apiUrl = hostname + `/api/key-value/` + window.vidiConfig.appDatabase; + // let hostname = 'https://map.calypso.watsonc.dk' + this.apiUrl = + hostname + + `/api/extension/watsonc/${window.vidiConfig.appDatabase}/profiles`; + } + + getAll() { + return new Promise((resolve, reject) => { + $.ajax({ + url: this.apiUrl, + method: "GET", + dataType: "json", + }).then( + (response) => { + let parsedData = []; + response.map((item) => { + parsedData.push(JSON.parse(item.value)); + }); + + resolve(parsedData); + }, + (jqXHR) => { + console.error(`Error occured while refreshing profiles list`); + reject(`Error occured while refreshing profiles list`); + } + ); + }); + } + + create(savedProfile) { + return new Promise((resolve, reject) => { + axios + .post(`/api/extension/watsonc/profile`, savedProfile) + .then((response) => { + if (response.data) { + savedProfile.data = response.data; + axios + .post(`${this.apiUrl}`, savedProfile) + .then((response) => { + let data = JSON.parse(response.data.data.value); + resolve(data); + }) + .catch((error) => { + console.error(error); + reject(error); + }); + } else { + console.error(`Unable to generate plot data`); + reject(`Unable to generate plot data`); + } + }) + .catch((error) => { + console.error(`Error occured during plot generation`, error); + reject(error); + }); + }); + } + + delete(profileKey) { + return new Promise((resolve, reject) => { + if (profileKey) { + axios + .delete(`${this.apiUrl}/${profileKey}`) + .then(resolve) + .catch(reject); + } else { + reject(`Empty profile identifier was provided`); + } + }); + } +} + +export default ProfileManager; diff --git a/browser/api/baseApi.js b/browser/api/baseApi.js new file mode 100644 index 0000000..928f667 --- /dev/null +++ b/browser/api/baseApi.js @@ -0,0 +1,52 @@ + +export default class BaseApi { + + get(url = "") { + try { + + // Default options are marked with * + return fetch(url, { + method: 'GET', // *GET, POST, PUT, DELETE, etc. + // mode: 'cors', // no-cors, *cors, same-origin + // cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + // credentials: 'same-origin', // include, *same-origin, omit + // headers: { + // 'Content-Type': 'application/json; charset=utf-8' + // }, + // redirect: 'follow', // manual, *follow, error + // referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + // body: JSON.stringify(query) // body data type must match "Content-Type" header + }); + + + } catch (error) { + // console.log("BaseApi error", error); + return ""; + } + } + + post(url = "", body = "") { + + try { + + // Default options are marked with * + return fetch(url, { + method: 'POST', // *GET, POST, PUT, DELETE, etc. + // mode: 'cors', // no-cors, *cors, same-origin + // cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + // credentials: 'same-origin', // include, *same-origin, omit + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + // redirect: 'follow', // manual, *follow, error + // referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + // body: JSON.stringify(query) // body data type must match "Content-Type" header + body: JSON.stringify(body) + }); + + } catch (error) { + // console.log("BaseApi error", error); + return ""; + } + } +} diff --git a/browser/api/meta/MetaApi.js b/browser/api/meta/MetaApi.js new file mode 100644 index 0000000..89b8494 --- /dev/null +++ b/browser/api/meta/MetaApi.js @@ -0,0 +1,31 @@ +import BaseApi from "../baseApi"; + +const metaUrl = "/api/meta/jupiter/"; + +export default class MetaApi { + getMetaData(parameter) { + const baseApi = new BaseApi(); + const url = metaUrl + parameter; + return baseApi + .get(url) + .then((response) => { + return response.json(); + }) + .then((results) => { + return results.data + .filter((item) => item.f_table_schema == parameter) + .map((item) => { + let value = `${item.f_table_schema}.${item.f_table_name}`; + if (item.f_table_title == "Jupiter boringer") { + value = "v:system.all"; + } + return { + label: item.f_table_title, + group: item.layergroup, + value: value, + privileges: JSON.parse(item.privileges), + }; + }); + }); + } +} diff --git a/browser/api/plots/PlotApi.js b/browser/api/plots/PlotApi.js new file mode 100644 index 0000000..40db26c --- /dev/null +++ b/browser/api/plots/PlotApi.js @@ -0,0 +1,13 @@ +import BaseApi from '../baseApi'; + +const downloadPlotUrl = '/api/extension/watsonc/download-plot'; + +export default class PlotApi { + downloadPlot(payload) { + const baseApi = new BaseApi(); + const response = baseApi.post(downloadPlotUrl, payload); + return response.then((response) => { + return response.blob(); + }); + } +} diff --git a/browser/api/projects/ProjectsApi.js b/browser/api/projects/ProjectsApi.js new file mode 100644 index 0000000..dbeba76 --- /dev/null +++ b/browser/api/projects/ProjectsApi.js @@ -0,0 +1,11 @@ +import BaseApi from '../baseApi'; + +const projectsUrl = '/api/state-snapshots/jupiter?ownerOnly=true' + +export default class ProjectsApi { + getProjects() { + const baseApi = new BaseApi(); + const response = baseApi.get(projectsUrl); + return response; + } +} diff --git a/browser/components/AnalyticsComponent.js b/browser/components/AnalyticsComponent.js index c8598aa..a84ce44 100644 --- a/browser/components/AnalyticsComponent.js +++ b/browser/components/AnalyticsComponent.js @@ -1,62 +1,84 @@ import React from "react"; import axios from "axios"; import fileSaver from "file-saver"; -import LoadingOverlay from './../../../../browser/modules/shared/LoadingOverlay'; - +import LoadingOverlay from "./../../../../browser/modules/shared/LoadingOverlay"; /** * Analytics Component */ + +const session = require("./../../../session/browser/index"); + class AnalyticsComponent extends React.Component { - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - this.kommune = React.createRef(); - this.type = React.createRef(); - this.state = { - loading: false - }; - } + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + this.kommune = React.createRef(); + this.type = React.createRef(); + this.state = { + loading: false, + }; + } - handleSubmit(event) { - this.setState({loading: true}); - axios.get(`/api/extension/watsonc/report?komcode=${this.kommune.current.value}&userid=1234`).then(response => { - fileSaver.saveAs(response.data.url, "rapport.xlsx"); - }).catch(error => { - console.log(`Error occured`, error); - }).finally(() => { - this.setState({loading: false}); - } - ) - event.preventDefault(); - } + handleSubmit(event) { + this.setState({ loading: true }); + axios + .get( + `/api/extension/watsonc/report?komcode=${ + this.kommune.current.value + }&userid=${session.getUserName()}` + ) + .then((response) => { + fileSaver.saveAs(response.data.url, "rapport.xlsx"); + }) + .catch((error) => { + console.log(`Error occured`, error); + }) + .finally(() => { + this.setState({ loading: false }); + }); + event.preventDefault(); + } - render() { - let data = this.props.kommuner; - let makeItem = function (i) { - return ; - }; + render() { + let data = this.props.kommuner; + let makeItem = function (i) { + return ( + + ); + }; - return ( -
- {this.state.loading ? : false} -
- - -
-
- - -
-
- -
- - ); - } + return ( +
+ {this.state.loading ? : false} +
+ + +
+
+ + +
+
+ +
+ + ); + } } export default AnalyticsComponent; diff --git a/browser/components/ChemicalSelector.js b/browser/components/ChemicalSelector.js deleted file mode 100644 index 2de1814..0000000 --- a/browser/components/ChemicalSelector.js +++ /dev/null @@ -1,170 +0,0 @@ -import React from 'react'; -import {connect} from 'react-redux'; - -import SearchFieldComponent from './../../../../browser/modules/shared/SearchFieldComponent'; -import {selectChemical} from '../redux/actions'; - -import {WATER_LEVEL_KEY} from './../constants'; - -const uuidv4 = require('uuid/v4'); - -/** - * Chemical selector - */ -class ChemicalSelector extends React.Component { - constructor(props) { - super(props); - - this.state = {searchTerm: ``}; - this.handleSearch = this.handleSearch.bind(this); - } - - generateWaterGroup(runId) { - let checked = false; - if (this.props.useLocalSelectedChemical) { - checked = this.props.localSelectedChemical === WATER_LEVEL_KEY; - } else { - checked = this.props.selectedChemical === WATER_LEVEL_KEY; - } - - return (
-
-
{__(`Water level`)}
-
-
-
-
- -
-
-
-
); - } - - generateChemicalGroups(runId) { - let chemicalGroupsForLayer = []; - for (let layerName in this.props.categories) { - if (layerName.indexOf(LAYER_NAMES[0]) > -1) { - for (let key in this.props.categories[layerName]) { - let chemicalsMarkup = []; - for (let key2 in this.props.categories[layerName][key]) { - if (this.state.searchTerm === `` || this.props.categories[layerName][key][key2].toLowerCase().indexOf(this.state.searchTerm.toLowerCase()) > -1) { - let checked = false; - if (this.props.useLocalSelectedChemical) { - checked = this.props.localSelectedChemical === key2; - } else { - checked = this.props.selectedChemical === key2; - } - - chemicalsMarkup.push(
-
- -
-
); - } - } - - if (chemicalsMarkup.length > 0) { - if (key !== `Vandstand`) { - chemicalGroupsForLayer.push(
-
-
{key}
-
-
{chemicalsMarkup}
-
); - } - } - } - } - } - return chemicalGroupsForLayer; - } - - handleSearch(searchTerm) { - this.setState({searchTerm}); - } - - render() { - let runId = uuidv4(); - - let layerGroupsList = []; - - if (this.props.emptyOptionTitle) { - layerGroupsList.push(
-
-
-
- -
-
-
-
); - } - - if (this.props.selectedLayers.indexOf(LAYER_NAMES[0]) > -1) { - let waterGroup = this.generateWaterGroup(runId); - layerGroupsList.push(waterGroup); - let chemicalGroups = this.generateChemicalGroups(runId); - layerGroupsList = layerGroupsList.concat(chemicalGroups); - } - - return (
- {this.props.selectedLayers.length > 0 ? (
- - {layerGroupsList.length > 0 ? ( -
{layerGroupsList}
) : ( -

{__(`Nothing found`)}

)} -
) : false} -
); - } -} - -ChemicalSelector.defaultProps = { - useLocalSelectedChemical: false, - localSelectedChemical: false -}; - -const mapStateToProps = state => ({ - categories: state.global.categories, - selectedLayers: state.global.selectedLayers, - selectedChemical: state.global.selectedChemical -}); - -const mapDispatchToProps = dispatch => ({ - selectChemical: (key) => dispatch(selectChemical(key)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ChemicalSelector); diff --git a/browser/components/ChemicalSelectorModal.js b/browser/components/ChemicalSelectorModal.js deleted file mode 100644 index c098115..0000000 --- a/browser/components/ChemicalSelectorModal.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; - -import ChemicalSelector from './ChemicalSelector'; -import { selectChemical } from '../redux/actions'; - -/** - * Chemical selector - */ -class ChemicalSelectorModal extends React.Component { - constructor(props) { - super(props); - } - - render() { - return (
-
-

- {__(`Select datatype`)} -

-
-
-
- -
-
- - {this.props.onCancelControl ? () : false} -
-
-
); - } -} - -ChemicalSelectorModal.defaultProps = { - useLocalSelectedChemical: false, - localSelectedChemical: false -}; - -const mapStateToProps = state => ({ - selectedChemical: state.global.selectedChemical, - selectedLayers: state.global.selectedLayers, -}); - -const mapDispatchToProps = dispatch => ({ - selectChemical: (key) => dispatch(selectChemical(key)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ChemicalSelectorModal); diff --git a/browser/components/DashboardComponent.js b/browser/components/DashboardComponent.js index de14e2c..bd151b7 100644 --- a/browser/components/DashboardComponent.js +++ b/browser/components/DashboardComponent.js @@ -1,24 +1,24 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {Provider} from 'react-redux'; -import reduxStore from './../redux/store'; - -import ReactTooltip from 'react-tooltip'; -import {SELECT_CHEMICAL_DIALOG_PREFIX, TEXT_FIELD_DIALOG_PREFIX, VIEW_MATRIX, VIEW_ROW} from './../constants'; -import PlotManager from './../PlotManager'; -import ProfileManager from './../ProfileManager'; -import TextFieldModal from './TextFieldModal'; -import SortablePlotComponent from './SortablePlotComponent'; -import SortableProfileComponent from './SortableProfileComponent'; -import SortablePlotsGridComponent from './SortablePlotsGridComponent'; -import {isNumber} from 'util'; -import arrayMove from 'array-move'; -import trustedIpAddresses from '../trustedIpAddresses'; -import {getPlotData} from '../services/plot'; - -let syncInProg = false; - -const uuidv1 = require('uuid/v1'); +import React from "react"; +import PropTypes from "prop-types"; +import { Provider } from "react-redux"; +import reduxStore from "./../redux/store"; +import { setDashboardMode } from "./../redux/actions"; +import { + SELECT_CHEMICAL_DIALOG_PREFIX, + TEXT_FIELD_DIALOG_PREFIX, + VIEW_MATRIX, + VIEW_ROW, +} from "./../constants"; +import PlotManager from "./../PlotManager"; +import ProfileManager from "./../ProfileManager"; +import TextFieldModal from "./TextFieldModal"; +import arrayMove from "array-move"; +import trustedIpAddresses from "../trustedIpAddresses"; +import { getPlotData } from "../services/plot"; +import ProjectContext from "../contexts/project/ProjectContext"; + +const uuidv1 = require("uuid/v1"); +const session = require("./../../../session/browser/index"); const DASHBOARD_ITEM_PLOT = 0; const DASHBOARD_ITEM_PROJECT_PLOT = 3; @@ -28,1180 +28,1046 @@ const DASHBOARD_ITEM_PROJECT_PROFILE = 2; const DISPLAY_MIN = 0; const DISPLAY_HALF = 1; const DISPLAY_MAX = 2; -let currentDisplay = DISPLAY_HALF, previousDisplay = DISPLAY_MAX; +let currentDisplay = DISPLAY_HALF, + previousDisplay = DISPLAY_MAX; let modalHeaderHeight = 70; -let _self = false, resizeTimeout = false; +let _self = false, + resizeTimeout = false; /** * Component creates plots management form and is the source of truth for plots overall */ class DashboardComponent extends React.Component { - constructor(props) { - super(props); - let queryParams = new URLSearchParams(window.location.search); - let licenseToken = queryParams.get('license'); - let license = null; - if (licenseToken) { - license = JSON.parse(base64.decode(licenseToken.split('.')[1])); - if (typeof license === 'object') { - license = license.license; - } - } - if (trustedIpAddresses.includes(window._vidiIp)) { - license = "premium"; - } - - let dashboardItems = []; - if (this.props.initialPlots) { - this.props.initialPlots.map(item => { - dashboardItems.push({ - type: DASHBOARD_ITEM_PLOT, - item - }); - }); - } - - this.state = { - view: VIEW_MATRIX, - newPlotName: ``, - dashboardItems, - plots: this.props.initialPlots, - projectPlots: [], - profiles: [], - projectProfiles: [], - activePlots: [], - activeProfiles: [], - dataSource: [], - highlightedPlot: false, - createdProfileChemical: false, - createdProfileName: false, - lastUpdate: false, - license: license, - modalScroll: {} - }; - - this.plotManager = new PlotManager(); - this.profileManager = new ProfileManager(); - - this.handleShowPlot = this.handleShowPlot.bind(this); - this.handleHidePlot = this.handleHidePlot.bind(this); - this.handleCreatePlot = this.handleCreatePlot.bind(this); - this.handleRemovePlot = this.handleRemovePlot.bind(this); - this.handleDeletePlot = this.handleDeletePlot.bind(this); - this.handleHighlightPlot = this.handleHighlightPlot.bind(this); - this.handleArchivePlot = this.handleArchivePlot.bind(this); - - this.handleShowProfile = this.handleShowProfile.bind(this); - this.handleHideProfile = this.handleHideProfile.bind(this); - this.handleCreateProfile = this.handleCreateProfile.bind(this); - this.handleRemoveProfile = this.handleRemoveProfile.bind(this); - this.handleDeleteProfile = this.handleDeleteProfile.bind(this); - this.handleProfileClick = this.handleProfileClick.bind(this); - this.handleChangeDatatypeProfile = this.handleChangeDatatypeProfile.bind(this); - this.setProjectProfiles = this.setProjectProfiles.bind(this); - this.getProfilesLength = this.getProfilesLength.bind(this); - this.getPlotsLength = this.getPlotsLength.bind(this); - - this.getFeatureByGidFromDataSource = this.getFeatureByGidFromDataSource.bind(this); - this.handleNewPlotNameChange = this.handleNewPlotNameChange.bind(this); - this.handlePlotSort = this.handlePlotSort.bind(this); - this.getLicense = this.getLicense.bind(this); - - this.setModalScroll = this.setModalScroll.bind(this); - this.getModalScroll = this.getModalScroll.bind(this); - - _self = this; - } - - UNSAFE_componentWillMount() { - $(window).resize(function () { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => { - _self.setState({lastUpdate: new Date()}); - }, 500); - }); - - this.props.backboneEvents.get().on(`session:authChange`, (authenticated) => { - if (authenticated) { - _self.refreshProfilesList(); - _self.hydratePlotsFromIds(); - } else { - let newDashboardItems = []; - _self.state.dashboardItems.map(item => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - newDashboardItems.push(JSON.parse(JSON.stringify(item))); - } - }); - - _self.setState({ - profiles: [], - activeProfiles: [], - dashboardItems: newDashboardItems - }); - } + static contextType = ProjectContext; + + constructor(props) { + super(props); + let license = session.getProperties()?.["license"]; + + let dashboardItems = []; + if (this.props.initialPlots) { + this.props.initialPlots.map((item) => { + dashboardItems.push({ + type: DASHBOARD_ITEM_PLOT, + item, }); - - this.refreshProfilesList(); - } - - componentDidMount() { - this.nextDisplayType(); - } - - getLicense() { - return this.state.license; - } - - getModalScroll() { - return this.state.modalScroll; + }); } - setModalScroll(modalScroll) { - this.setState({modalScroll}); - } - - refreshProfilesList() { - this.profileManager.getAll().then(profiles => { - let newDashboardItems = []; - this.state.dashboardItems.map(item => { - if (item.type !== DASHBOARD_ITEM_PROFILE) { - newDashboardItems.push(JSON.parse(JSON.stringify(item))); - } - }); - - profiles.map(item => { - newDashboardItems.push({ - type: DASHBOARD_ITEM_PROFILE, - item - }); - }); - - this.setState({ - profiles, - dashboardItems: newDashboardItems - }); - this.props.onProfilesChange(this.getProfiles()); - - }); - } - - dehydratePlots(plots) { - return this.plotManager.dehydratePlots(plots); - } - - hydratePlotsFromIds(plots) { - return this.plotManager.hydratePlotsFromIds(plots); - } - - getProfiles() { - let allProfiles = []; - this.state.projectProfiles.map(item => { - item.fromProject = true; - allProfiles.push(item); - }); - this.state.profiles.map(item => { - allProfiles.push(item); - }) - allProfiles = allProfiles.sort((a, b) => b['created_at'] - a['created_at']); - return allProfiles; - } - - getActiveProfiles() { - return JSON.parse(JSON.stringify(this.state.activeProfiles)); - } - - getActiveProfileObjects() { - let activeProfiles = this.getProfiles().filter((item) => { - if (this.state.activeProfiles.indexOf(item.key) !== -1) { - return item; - } - }); - return JSON.parse(JSON.stringify(activeProfiles)); - } - - handleCreateProfile(data, activateOnCreate = true, callback = false) { - this.profileManager.create(data).then(newProfile => { - let profilesCopy = JSON.parse(JSON.stringify(this.state.profiles)); - profilesCopy.unshift(newProfile); - - if (activateOnCreate) { - let activeProfilesCopy = JSON.parse(JSON.stringify(this.state.activeProfiles)); - if (activeProfilesCopy.indexOf(newProfile.key) === -1) activeProfilesCopy.push(newProfile.key); - - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.push({ - type: DASHBOARD_ITEM_PROFILE, - item: newProfile - }); - - this.setState({ - profiles: profilesCopy, - dashboardItems: dashboardItemsCopy, - activeProfiles: activeProfilesCopy - }); - - this.props.onActiveProfilesChange(activeProfilesCopy); - } else { - this.setState({profiles: profilesCopy}); - } - - if (callback) callback(); - - this.props.onProfilesChange(this.getProfiles()); - }).catch(error => { - console.error(`Error occured while creating profile (${error})`); - alert(`Error occured while creating profile (${error})`); - if (callback) callback(); - }); - } - - handleChangeDatatypeProfile(profileKey) { - let selectedProfile = false; - this.getProfiles().map(item => { - if (item.key === profileKey) { - selectedProfile = item; - } - }); - - if (selectedProfile === false) throw new Error(`Unable to find the profile with key ${profileKey}`); - - this.setState({createdProfileChemical: false}, () => { - const abortDataTypeChange = () => { - this.setState({createdProfileChemical: false}); - $('#' + SELECT_CHEMICAL_DIALOG_PREFIX).modal('hide'); - }; - - const uniqueKey = uuidv1(); - - try { - ReactDOM.render(
- - { - this.setState({createdProfileChemical: selectorValue}, () => { - try { - ReactDOM.render(
- { - $.snackbar({ - id: "snackbar-watsonc", - content: "" + __("The profile with the new datatype is being created") + "", - htmlAllowed: true, - timeout: 1000000 - }); - - this.handleCreateProfile({ - title, - profile: selectedProfile.value.profile, - buffer: selectedProfile.value.buffer, - depth: selectedProfile.value.depth, - compound: this.state.createdProfileChemical, - boreholeNames: selectedProfile.value.boreholeNames, - layers: selectedProfile.value.layers, - }, true, () => { - this.setState({createdProfileChemical: false}, () => { - jquery("#snackbar-watsonc").snackbar("hide"); - }); - }); - }} - onCancelControl={abortDataTypeChange}/> -
, document.getElementById(`${TEXT_FIELD_DIALOG_PREFIX}-placeholder`)); - } catch (e) { - console.error(e); - } - - $('#' + TEXT_FIELD_DIALOG_PREFIX).modal({backdrop: `static`}); - }); + this.state = { + view: VIEW_MATRIX, + newPlotName: ``, + dashboardItems, + plots: this.props.initialPlots, + projectPlots: [], + profiles: [], + projectProfiles: [], + activePlots: [], + activeProfiles: [], + dataSource: [], + highlightedPlot: false, + createdProfileChemical: false, + createdProfileName: false, + lastUpdate: false, + license: license, + modalScroll: {}, + }; - $('#' + SELECT_CHEMICAL_DIALOG_PREFIX).modal('hide'); - }} - onCancelControl={abortDataTypeChange}/> -
-
, document.getElementById(`${SELECT_CHEMICAL_DIALOG_PREFIX}-placeholder`)); - } catch (e) { - console.error(e); + this.plotManager = new PlotManager(); + this.profileManager = new ProfileManager(); + + // this.handleHighlightPlot = this.handleHighlightPlot.bind(this); + + this.handleShowProfile = this.handleShowProfile.bind(this); + this.handleHideProfile = this.handleHideProfile.bind(this); + this.handleCreateProfile = this.handleCreateProfile.bind(this); + this.handleAddProfile = this.handleAddProfile.bind(this); + this.handleDeleteProfile = this.handleDeleteProfile.bind(this); + this.handleProfileClick = this.handleProfileClick.bind(this); + this.handleChangeDatatypeProfile = + this.handleChangeDatatypeProfile.bind(this); + this.setProjectProfiles = this.setProjectProfiles.bind(this); + this.getProfilesLength = this.getProfilesLength.bind(this); + this.getPlotsLength = this.getPlotsLength.bind(this); + this.getDashboardItems = this.getDashboardItems.bind(this); + + // this.getFeatureByGidFromDataSource = this.getFeatureByGidFromDataSource.bind(this); + // this.handleNewPlotNameChange = this.handleNewPlotNameChange.bind(this); + this.handlePlotSort = this.handlePlotSort.bind(this); + this.getLicense = this.getLicense.bind(this); + + this.setModalScroll = this.setModalScroll.bind(this); + this.getModalScroll = this.getModalScroll.bind(this); + + _self = this; + } + + UNSAFE_componentWillMount() { + $(window).resize(function () { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + _self.setState({ lastUpdate: new Date() }); + }, 500); + }); + + this.props.backboneEvents + .get() + .on(`session:authChange`, (authenticated) => { + if (authenticated) { + } else { + let newDashboardItems = []; + _self.state.dashboardItems.map((item) => { + if ( + item.type === DASHBOARD_ITEM_PLOT || + item.type === DASHBOARD_ITEM_PROJECT_PLOT + ) { + newDashboardItems.push(JSON.parse(JSON.stringify(item))); } + }); - $('#' + SELECT_CHEMICAL_DIALOG_PREFIX).modal({backdrop: `static`}); - }); - } - - handleProfileClick(e) { - if (e && e.points && e.points.length === 1 && e.points[0].data && e.points[0].data.text) { - if (e.points[0].data.text.indexOf(`DGU`) > -1) { - let boreholeNumber = false; - let lines = e.points[0].data.text.split(`
`); - lines.map(item => { - if (item.indexOf(`DGU`) > -1) { - boreholeNumber = item.replace(`DGU`, ``) - .replace(/>/g, ``) - .replace(/ { + this.setState({ profiles: response }); + this.props.setProfiles({ profiles: response }); + }); + this.props.backboneEvents.get().on("refresh:meta", () => { + this.profileManager.getAll().then((response) => { + this.setState({ profiles: response }); + this.props.setProfiles({ profiles: response }); + }); + }); + } + + getLicense() { + return session.getProperties()?.["license"]; + } + + getModalScroll() { + return this.state.modalScroll; + } + + setModalScroll(modalScroll) { + this.setState({ modalScroll }); + } + + getProfiles() { + let allProfiles = []; + this.state.projectProfiles.map((item) => { + item.fromProject = true; + allProfiles.push(item); + }); + this.state.profiles.map((item) => { + allProfiles.push(item); + }); + allProfiles = allProfiles.sort((a, b) => b["created_at"] - a["created_at"]); + return allProfiles; + } + + getActiveProfiles() { + return JSON.parse(JSON.stringify(this.state.activeProfiles)); + } + + getActiveProfileObjects() { + let activeProfiles = this.getProfiles().filter((item) => { + if (this.state.activeProfiles.indexOf(item.key) !== -1) { + return item; + } + }); + return JSON.parse(JSON.stringify(activeProfiles)); + } + + handleCreateProfile(data, activateOnCreate = true, callback = false) { + _self.profileManager + .create(data) + .then((newProfile) => { + let profilesCopy = JSON.parse(JSON.stringify(_self.state.profiles)); + profilesCopy.unshift(newProfile); + + if (activateOnCreate) { + let activeProfilesCopy = JSON.parse( + JSON.stringify(_self.state.activeProfiles) + ); + if (activeProfilesCopy.indexOf(newProfile.key) === -1) + activeProfilesCopy.push(newProfile.key); + + let dashboardItemsCopy = JSON.parse( + JSON.stringify(_self.state.dashboardItems) + ); + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROFILE, + item: newProfile, + }); + + _self.setState({ + profiles: profilesCopy, + dashboardItems: dashboardItemsCopy, + activeProfiles: activeProfilesCopy, + }); + + _self.props.onActiveProfilesChange( + activeProfilesCopy, + profilesCopy, + _self.context + ); + } else { + _self.setState({ profiles: profilesCopy }); } - } - - handleDeleteProfile(profileKey, callback = false) { - this.profileManager.delete(profileKey).then(() => { - let profilesCopy = JSON.parse(JSON.stringify(this.state.profiles)); - - let profileWasDeleted = false; - profilesCopy.map((profile, index) => { - if (profile.key === profileKey) { - profilesCopy.splice(index, 1); - profileWasDeleted = true; - return false; - } - }); - if (profileWasDeleted === false) { - console.warn(`Profile ${profileKey} was deleted only from backend storage`); - } + if (callback) callback(); + + _self.props.onProfilesChange(_self.getProfiles()); + }) + .catch((error) => { + console.error(`Error occured while creating profile (${error})`); + alert(`Error occured while creating profile (${error})`); + if (callback) callback(); + }); + } + + handleChangeDatatypeProfile(profileKey) { + let selectedProfile = false; + this.getProfiles().map((item) => { + if (item.key === profileKey) { + selectedProfile = item; + } + }); + + if (selectedProfile === false) + throw new Error(`Unable to find the profile with key ${profileKey}`); + + this.setState({ createdProfileChemical: false }, () => { + const abortDataTypeChange = () => { + this.setState({ createdProfileChemical: false }); + $("#" + SELECT_CHEMICAL_DIALOG_PREFIX).modal("hide"); + }; + + const uniqueKey = uuidv1(); + + try { + ReactDOM.render( +
+ + { + this.setState( + { createdProfileChemical: selectorValue }, + () => { + try { + ReactDOM.render( +
+ { + $.snackbar({ + id: "snackbar-watsonc", + content: + "" + + __( + "The profile with the new datatype is being created" + ) + + "", + htmlAllowed: true, + timeout: 1000000, + }); - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PROFILE) { - if (item.key === profileKey) { - dashboardItemsCopy.splice(index, 1); - return false; + this.handleCreateProfile( + { + title, + profile: selectedProfile.value.profile, + buffer: selectedProfile.value.buffer, + depth: selectedProfile.value.depth, + compound: this.state.createdProfileChemical, + boreholeNames: + selectedProfile.value.boreholeNames, + layers: selectedProfile.value.layers, + }, + true, + () => { + this.setState( + { createdProfileChemical: false }, + () => { + jquery("#snackbar-watsonc").snackbar( + "hide" + ); + } + ); + } + ); + }} + onCancelControl={abortDataTypeChange} + /> +
, + document.getElementById( + `${TEXT_FIELD_DIALOG_PREFIX}-placeholder` + ) + ); + } catch (e) { + console.error(e); + } + + $("#" + TEXT_FIELD_DIALOG_PREFIX).modal({ + backdrop: `static`, + }); } - } - }); - - let activeProfilesCopy = JSON.parse(JSON.stringify(this.state.activeProfiles)); - activeProfilesCopy.map((profile, index) => { - if (profile === profileKey) { - activeProfilesCopy.splice(index, 1); - return false; - } - }); - - if (callback) callback(); - - this.setState({ - profiles: profilesCopy, - activeProfiles: activeProfilesCopy, - dashboardItems: dashboardItemsCopy - }); - this.props.onProfilesChange(this.getProfiles()); - - }).catch(error => { - console.error(`Error occured while deleting profile (${error})`) - }); - } - - handleShowProfile(profileId) { - if (!profileId) throw new Error(`Empty profile identifier`); - - let activeProfiles = JSON.parse(JSON.stringify(this.state.activeProfiles)); - if (activeProfiles.indexOf(profileId) === -1) activeProfiles.push(profileId); - this.setState({activeProfiles}, () => { - this.props.onActiveProfilesChange(this.state.activeProfiles); - }); - } - - handleHideProfile(profileId) { - if (!profileId) throw new Error(`Empty profile identifier`); - - let activeProfiles = JSON.parse(JSON.stringify(this.state.activeProfiles)); - if (activeProfiles.indexOf(profileId) > -1) activeProfiles.splice(activeProfiles.indexOf(profileId), 1); - this.setState({activeProfiles}, () => { - this.props.onActiveProfilesChange(this.state.activeProfiles); - }); - } - - getPlots(getArchived = true) { - let allPlots = []; - this.state.projectPlots.map((item) => { - item.fromProject = true; - allPlots.push(item); + ); + + $("#" + SELECT_CHEMICAL_DIALOG_PREFIX).modal("hide"); + }} + onCancelControl={abortDataTypeChange} + /> +
+
, + document.getElementById( + `${SELECT_CHEMICAL_DIALOG_PREFIX}-placeholder` + ) + ); + } catch (e) { + console.error(e); + } + + $("#" + SELECT_CHEMICAL_DIALOG_PREFIX).modal({ backdrop: `static` }); + }); + } + + handleProfileClick(e) { + if ( + e && + e.points && + e.points.length === 1 && + e.points[0].data && + e.points[0].data.text + ) { + if (e.points[0].data.text.indexOf(`DGU`) > -1) { + let boreholeNumber = false; + let lines = e.points[0].data.text.split(`
`); + lines.map((item) => { + if (item.indexOf(`DGU`) > -1) { + boreholeNumber = item + .replace(`DGU`, ``) + .replace(/>/g, ``) + .replace(/ { - item.fromProject = false; - allPlots.push(item); - }) - allPlots = allPlots.filter((item) => { - if (getArchived) { - return item; - } else if (!item.isArchived) { - return item; - } - }) - allPlots = allPlots.sort((a, b) => b['created_at'] - a['created_at']); - const unique = (myArr) => { - return myArr.filter((obj, pos, arr) => { - return arr.map(mapObj => mapObj["id"]).indexOf(obj["id"]) === pos; - }); + if (boreholeNumber !== false) { + this.props.onOpenBorehole(boreholeNumber); } - allPlots = unique(allPlots); - return allPlots; - } - - getActivePlots() { - let activePlots = this.state.plots.filter((item) => { - if (this.state.activePlots.indexOf(item.id) !== -1) { - return item.id; - } - }); - this.state.projectPlots.map((item) => { - activePlots.push(item.id); - }); - return JSON.parse(JSON.stringify(activePlots)); - } - - - addPlot(newPlotName, activateOnCreate = false) { - this.handleCreatePlot(newPlotName, activateOnCreate); - } - - setProjectPlots(projectPlots) { - let dashboardItemsCopy = []; - let plotsNotOnDashboard = []; - let profilesNotOnDashboard = []; - const unique = (data, key) => { - return [...new Map(data.map(item => [key(item), item])).values()] - }; - - this.state.dashboardItems.map(item => { - if (item.type !== DASHBOARD_ITEM_PROJECT_PLOT) { - dashboardItemsCopy.push(item); - } - }); - - projectPlots.map(item => { - dashboardItemsCopy.push({ - type: DASHBOARD_ITEM_PROJECT_PLOT, - item - }); - }); - - dashboardItemsCopy.map(item => { - if (typeof item.type !== "undefined" && item.type === 0) { - plotsNotOnDashboard.push(item.item.id); - } else if (typeof item.type !== "undefined" && item.type === 3) { - // Remove an id again if it appears more than once in this.state.dashboardItems - plotsNotOnDashboard = plotsNotOnDashboard.filter(e => e !== item.item.id); - } - if (typeof item.type !== "undefined" && item.type === 1) { - profilesNotOnDashboard.push(item.item.key); - } else if (typeof item.type !== "undefined" && item.type === 2) { - // Remove a key again if it appears more than once in this.state.dashboardItems - profilesNotOnDashboard = profilesNotOnDashboard.filter(e => e !== item.item.key); - } - }); - - // Remove duplets - dashboardItemsCopy = unique(dashboardItemsCopy, item => item.item.id); - this.setState({projectPlots, dashboardItems: dashboardItemsCopy}, () => { - setTimeout(() => { - plotsNotOnDashboard.forEach(id => this.handleHidePlot(id)); - profilesNotOnDashboard.forEach(id => this.handleHideProfile(id)); - }, 1000) - }); + } } - - setPlots(plots) { - let dashboardItemsCopy = []; - this.state.dashboardItems.map(item => { - if (item.type !== DASHBOARD_ITEM_PLOT /* type 0 */ && item.type !== DASHBOARD_ITEM_PROJECT_PLOT /* type 3*/) { - dashboardItemsCopy.push(item); - } + } + + handleDeleteProfile(profileKey, callback = false) { + this.profileManager + .delete(profileKey) + .then(() => { + var profilesCopy = JSON.parse(JSON.stringify(this.state.profiles)); + let profileWasDeleted = false; + profilesCopy = profilesCopy.filter((profile) => { + if (profile.key === profileKey) { + return false; + } + return true; }); - plots.map(item => { - dashboardItemsCopy.push({ - type: DASHBOARD_ITEM_PLOT, - item - }); - }); - - this.setState({plots, dashboardItems: dashboardItemsCopy}); - } - - setProjectProfiles(projectProfiles) { - const unique = (myArr) => { - return myArr.filter((obj, pos, arr) => { - return arr.map(mapObj => mapObj["key"]).indexOf(obj["key"]) === pos; - }); + if (profileWasDeleted === false) { + console.warn( + `Profile ${profileKey} was deleted only from backend storage` + ); } - // Remove duplets - projectProfiles = unique(projectProfiles); - let dashboardItemsCopy = []; - this.state.dashboardItems.map(item => { - if (item.type !== DASHBOARD_ITEM_PROJECT_PROFILE) { - dashboardItemsCopy.push(item); - } - }); - projectProfiles.map(item => { - dashboardItemsCopy.push({ - type: DASHBOARD_ITEM_PROJECT_PROFILE, - item - }); - this.handleShowProfile(item.key); - }) - this.setState({projectProfiles, dashboardItems: dashboardItemsCopy}); - } - getProfilesLength() { - let activeProfiles = []; - this.state.profiles.map((item) => { - if (activeProfiles.indexOf(item.key) === -1) { - activeProfiles.push(item.key); - } - }); - this.state.activeProfiles.map((item) => { - if (activeProfiles.indexOf(item.key) === -1) { - activeProfiles.push(item.key); + var dashboardItemsCopy = JSON.parse( + JSON.stringify(this.state.dashboardItems) + ); + dashboardItemsCopy = dashboardItemsCopy.filter((item) => { + if (item.type === DASHBOARD_ITEM_PROFILE) { + if (item.key === profileKey) { + return false; } + } + return true; }); - return activeProfiles.length; - } - - getPlotsLength() { - return this.state.plots.length + this.state.projectPlots.length; - } - syncPlotData() { - let activePlots = this.state.activePlots; - let plots = this.state.dashboardItems.map(e => e.item); - let newPlots = plots; - let preCount = 0; - let count = 0; - plots.forEach((e, i) => { - if ('id' in e) { - let obj = e.measurements; - if (obj && Object.keys(obj).length === 0 && obj.constructor === Object) { - preCount++; - } else if (!obj) { - preCount++; - } else { - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - preCount++; - } - } - } - } - }); - plots.forEach((e, i) => { - if ('id' in e) { - let obj = e.measurementsCachedData; - let shadowI = i; - newPlots[shadowI] = e; - if (!e.measurementsCachedData) { - e.measurementsCachedData = {}; - } - for (let index in e.measurements) { - let key = e.measurements[index]; - // Lazy load data and sync - // Only load active plots - if (activePlots.includes(e.id)) { - getPlotData(key).then((response) => { - if (!newPlots[shadowI].measurementsCachedData[key]) { - newPlots[shadowI].measurementsCachedData[key] = {}; - } - newPlots[shadowI].measurementsCachedData[key].data = response.data.features[0]; - count++; - if (count === preCount) { - console.log("All plots synced"); - _self.setPlots(newPlots); - } - }).catch((error) => { - console.log(error); - }) - } else { - count++; - } - } - } + var activeProfilesCopy = JSON.parse( + JSON.stringify(this.state.activeProfiles) + ); + activeProfilesCopy = activeProfilesCopy.filter((profile) => { + if (profile === profileKey) { + return false; + } + return true; }); - } - - handleCreatePlot(title, activateOnCreate = false) { - this.plotManager.create(title).then(newPlot => { - let plotsCopy = JSON.parse(JSON.stringify(this.state.plots)); - plotsCopy.unshift(newPlot); - - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.push({ - type: DASHBOARD_ITEM_PLOT, - item: newPlot - }); - if (activateOnCreate) { - let activePlotsCopy = JSON.parse(JSON.stringify(this.state.activePlots)); - if (activePlotsCopy.indexOf(newPlot.id) === -1) activePlotsCopy.push(newPlot.id); - - this.setState({ - plots: plotsCopy, - dashboardItems: dashboardItemsCopy, - activePlots: activePlotsCopy - }); - - this.props.onActivePlotsChange(activePlotsCopy, this.getPlots()); - } else { - this.setState({ - plots: plotsCopy, - dashboardItems: dashboardItemsCopy - }); - } + if (callback) callback(); - this.props.onPlotsChange(this.getPlots()); - }).catch(error => { - console.error(`Error occured while creating plot (${error})`) + this.setState({ + profiles: profilesCopy, + activeProfiles: activeProfilesCopy, + dashboardItems: dashboardItemsCopy, }); + this.props.onProfilesChange(this.getProfiles()); + this.props.onActiveProfilesChange( + activeProfilesCopy, + profilesCopy, + this.context + ); + }) + .catch((error) => { + console.error(`Error occured while deleting profile (${error})`); + }); + } + + handleShowProfile(profileId) { + if (!profileId) throw new Error(`Empty profile identifier`); + + let activeProfiles = JSON.parse(JSON.stringify(this.state.activeProfiles)); + if (activeProfiles.indexOf(profileId) === -1) + activeProfiles.push(profileId); + this.setState({ activeProfiles }, () => { + this.props.onActiveProfilesChange( + this.state.activeProfiles, + this.state.profiles, + this.context + ); + }); + } + + handleAddProfile(profile) { + let dashboardItemsCopy = []; + let activeProfiles = []; + + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROFILE, + item: profile, + }); + + this.state.dashboardItems.forEach((item) => { + dashboardItemsCopy.push(item); + if (item.type === DASHBOARD_ITEM_PROFILE) { + activeProfiles.push(item.item.key); + } + }); + + activeProfiles.push(profile.key); + + if (reduxStore.getState().global.dashboardMode === "minimized") { + reduxStore.dispatch(setDashboardMode("half")); } - handleRemoveProfile(profileKey) { - if (!profileKey) throw new Error(`Empty profile key`); - - let activeProfilesCopy = JSON.parse(JSON.stringify(this.state.activeProfiles)); - if (activeProfilesCopy.indexOf(profileKey) > -1) activeProfilesCopy.splice(activeProfilesCopy.indexOf(profileKey), 1); - - this.setState({activeProfiles: activeProfilesCopy}); - this.props.onActiveProfilesChange(activeProfilesCopy); - } - - handleRemovePlot(id) { - if (!id) throw new Error(`Empty plot identifier`); - - let activePlotsCopy = JSON.parse(JSON.stringify(this.state.activePlots)); - if (activePlotsCopy.indexOf(id) > -1) activePlotsCopy.splice(activePlotsCopy.indexOf(id), 1); - - this.setState({activePlots: activePlotsCopy}); - this.props.onActivePlotsChange(activePlotsCopy, this.getPlots()); - } - - handleDeletePlot(id, name) { - if (!id) throw new Error(`Empty plot identifier`); - - if (confirm(__(`Delete plot`) + ` ${name ? name : id}?`)) { - this.plotManager.delete(id).then(() => { - let plotsCopy = JSON.parse(JSON.stringify(this.state.plots)); - let plotWasDeleted = false; - plotsCopy.map((plot, index) => { - if (plot.id === id) { - plotsCopy.splice(index, 1); - plotWasDeleted = true; - return false; - } - }); - - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - if (item.item.id === id) { - dashboardItemsCopy.splice(index, 1); - return false; - } - } - }); - - if (plotWasDeleted === false) { - console.warn(`Plot ${id} was deleted only from backend storage`); - } - - this.setState({ - plots: plotsCopy, - dashboardItems: dashboardItemsCopy - }); + document.getElementById("chartsContainer").scrollTop = 0; + + this.setState( + { + dashboardItems: dashboardItemsCopy, + activeProfiles: activeProfiles, + }, + () => { + this.props.onActiveProfilesChange( + this.state.activeProfiles, + this.state.profiles, + this.context + ); + } + ); + } + + handleHideProfile(profileId) { + if (!profileId) throw new Error(`Empty profile identifier`); + + let activeProfiles = JSON.parse(JSON.stringify(this.state.activeProfiles)); + if (activeProfiles.indexOf(profileId) > -1) + activeProfiles.splice(activeProfiles.indexOf(profileId), 1); + this.setState({ activeProfiles }, () => { + this.props.onActiveProfilesChange( + this.state.activeProfiles, + this.state.profiles, + this.context + ); + }); + } + + getDashboardItems() { + return this.state.dashboardItems; + } + + getPlots() { + return this.state.plots; + } + + getActivePlots() { + let addedPlots = []; + let activePlots = this.state.plots.filter((item) => { + if ( + this.state?.activePlots && + addedPlots && + this.state.activePlots.indexOf(item.id) !== -1 && + addedPlots.indexOf(item.id) === -1 + ) { + addedPlots.push(item.id); + return item.id; + } + }); + this.state.projectPlots.map((item) => { + if ( + this.state?.activePlots && + addedPlots && + this.state.activePlots.indexOf(item.id) !== -1 && + addedPlots.indexOf(item.id) === -1 + ) { + addedPlots.push(item.id); + activePlots.push(item); + } + }); + return JSON.parse(JSON.stringify(activePlots)); + } + + setProjectPlots(projectPlots) { + let dashboardItemsCopy = []; + let plotsNotOnDashboard = []; + let profilesNotOnDashboard = []; + const unique = (data, key) => { + return [...new Map(data.map((item) => [key(item), item])).values()]; + }; - this.props.onPlotsChange(this.getPlots()); - }).catch(error => { - console.error(`Error occured while creating plot (${error})`) - }); + this.state.dashboardItems.map((item) => { + if (item.type !== DASHBOARD_ITEM_PROJECT_PLOT) { + dashboardItemsCopy.push(item); + } + }); + + projectPlots.map((item) => { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROJECT_PLOT, + item, + }); + }); + + dashboardItemsCopy.map((item) => { + if (typeof item.type !== "undefined" && item.type === 0) { + plotsNotOnDashboard.push(item.item.id); + } else if (typeof item.type !== "undefined" && item.type === 3) { + // Remove an id again if it appears more than once in this.state.dashboardItems + plotsNotOnDashboard = plotsNotOnDashboard.filter( + (e) => e !== item.item.id + ); + } + if (typeof item.type !== "undefined" && item.type === 1) { + profilesNotOnDashboard.push(item.item.key); + } else if (typeof item.type !== "undefined" && item.type === 2) { + // Remove a key again if it appears more than once in this.state.dashboardItems + profilesNotOnDashboard = profilesNotOnDashboard.filter( + (e) => e !== item.item.key + ); + } + }); + + // Remove duplets + dashboardItemsCopy = unique(dashboardItemsCopy, (item) => item.item.id); + this.setState({ projectPlots, dashboardItems: dashboardItemsCopy }, () => { + setTimeout(() => { + plotsNotOnDashboard.forEach((id) => this.handleHidePlot(id)); + profilesNotOnDashboard.forEach((id) => this.handleHideProfile(id)); + }, 1000); + }); + } + + setActiveProfiles(activeProfiles) { + if (typeof activeProfiles !== "undefined") { + let dashboardItemsCopy = []; + this.state.dashboardItems.map((item) => { + if ( + item.type !== DASHBOARD_ITEM_PROFILE /* type 0 */ && + item.type !== DASHBOARD_ITEM_PROJECT_PLOT /* type 3*/ + ) { + dashboardItemsCopy.push(item); } - } - - handleHighlightPlot(plotId) { - if (!plotId) throw new Error(`Empty plot identifier`); - - this.setState({highlightedPlot: (plotId === this.state.highlightedPlot ? false : plotId)}, () => { - this.props.onHighlightedPlotChange(this.state.highlightedPlot, this.state.plots); - }); - } - - handleShowPlot(plotId) { - if (!plotId) throw new Error(`Empty plot identifier`); - - let activePlots = JSON.parse(JSON.stringify(this.state.activePlots)); - - if (activePlots.indexOf(plotId) === -1) activePlots.push(plotId); - let plots = this.getPlots() - this.setState({activePlots}, () => { - this.props.onActivePlotsChange(this.state.activePlots, plots); - setTimeout(() => { - // document.getElementById("syncWithDatabaseBtn").click(); - this.syncPlotData(); - }, 200); - }); - } - - handleHidePlot(plotId) { - if (!plotId) throw new Error(`Empty plot identifier`); + }); - let activePlots = JSON.parse(JSON.stringify(this.state.activePlots)); - if (activePlots.indexOf(plotId) > -1) activePlots.splice(activePlots.indexOf(plotId), 1); - this.setState({activePlots}, () => { - this.props.onActivePlotsChange(this.state.activePlots, this.getPlots()); - }); - } - - handleNewPlotNameChange(event) { - this.setState({newPlotName: event.target.value}); - } - - handleArchivePlot(plotId, isArchived) { - let plots = JSON.parse(JSON.stringify(this.state.plots)); - let correspondingPlot = false; - let correspondingPlotIndex = false; - plots.map((plot, index) => { - if (plot.id == plotId) { - correspondingPlot = plot; - correspondingPlotIndex = index; - } + let profiles = this.getProfiles().filter((elem) => + activeProfiles.includes(elem.key) + ); + profiles.map((item) => { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROFILE, + item, }); - if (correspondingPlot === false) throw new Error(`Plot with id ${plotId} does not exist`); - correspondingPlot.isArchived = isArchived; - plots[correspondingPlotIndex] = correspondingPlot; - - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - if (item.item.id === correspondingPlot.id) { - dashboardItemsCopy[index].item = correspondingPlot; - return false; - } - } - }); - this.plotManager.update(correspondingPlot).then(() => { - this.setState({ - plots, - dashboardItems: dashboardItemsCopy - }); - - this.props.onPlotsChange(this.getPlots()); - }).catch(error => { - console.error(`Error occured while updating plot (${error})`) - }); + }); + + this.setState( + { activeProfiles, dashboardItems: dashboardItemsCopy }, + () => { + this.props.onActiveProfilesChange( + this.state.activeProfiles, + this.state.profiles, + this.context + ); + } + ); } - - _modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, action) { - if (!plotId) throw new Error(`Invalid plot identifier`); - if ((!gid && gid !== 0) || !measurementKey || (!measurementIntakeIndex && measurementIntakeIndex !== 0)) throw new Error(`Invalid measurement location parameters`); - - let plots = JSON.parse(JSON.stringify(this.state.plots)); - let correspondingPlot = false; - let correspondingPlotIndex = false; - plots.map((plot, index) => { - if (plot.id === plotId) { - correspondingPlot = plot; - correspondingPlotIndex = index; - } - }); - - if (correspondingPlot === false) throw new Error(`Plot with id ${plotId} does not exist`); - let measurementIndex = gid + ':' + measurementKey + ':' + measurementIntakeIndex; - if (action === `add`) { - if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { - let measurementData = this.getFeatureByGidFromDataSource(gid); - if (measurementData) { - var currentTime = new Date(); - correspondingPlot.measurements.push(measurementIndex); - correspondingPlot.measurementsCachedData[measurementIndex] = { - data: measurementData, - created_at: currentTime.toISOString() - } - } else { - throw new Error(`Unable to find data for measurement index ${measurementIndex}`); - } - } - } else if (action === `delete`) { - if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { - throw new Error(`Unable to find measurement ${measurementIndex} for ${plotId} plot`); - } else { - if (measurementIndex in correspondingPlot.measurementsCachedData) { - correspondingPlot.measurements.splice(correspondingPlot.measurements.indexOf(measurementIndex), 1); - delete correspondingPlot.measurementsCachedData[measurementIndex]; - } else { - throw new Error(`Data integrity violation: plot ${plotId} does not contain cached data for measurement ${measurementIndex}`); - } - } + } + + setProfiles(profiles) { + this.setState(profiles, () => { + this.props.onProfilesChange(this.getProfiles()); + }); + } + + setPlots(plots) { + let dashboardItemsCopy = []; + this.state.dashboardItems.map((item) => { + if ( + item.type !== DASHBOARD_ITEM_PLOT /* type 0 */ && + item.type !== DASHBOARD_ITEM_PROJECT_PLOT /* type 3*/ + ) { + dashboardItemsCopy.push(item); + } + }); + + plots.map((item) => { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PLOT, + item, + }); + }); + + this.setState({ plots, dashboardItems: dashboardItemsCopy }, () => { + this.props.onPlotsChange(this.getPlots(), this.context); + }); + } + + setItems(items) { + let dashboardItemsCopy = []; + + items.map((item) => { + dashboardItemsCopy.push({ + type: item?.key ? DASHBOARD_ITEM_PROFILE : DASHBOARD_ITEM_PLOT, + item, + }); + }); + + this.setState( + { + // profiles: items.filter((e) => !!e?.key), + activeProfiles: items.filter((e) => !!e?.key).map((e) => e.key), + plots: items.filter((e) => !!e?.id), + dashboardItems: dashboardItemsCopy, + }, + () => { + // this.props.onProfilesChange(this.getProfiles()); + this.props.onPlotsChange(this.getPlots(), this.context); + this.props.onActiveProfilesChange( + this.state.activeProfiles, + this.state.profiles, + this.context + ); + } + ); + } + + setActivePlots(activePlots) { + this.setState({ activePlots }); + } + + setProjectProfiles(projectProfiles) { + const unique = (myArr) => { + return myArr.filter((obj, pos, arr) => { + return arr.map((mapObj) => mapObj["key"]).indexOf(obj["key"]) === pos; + }); + }; + // Remove duplets + projectProfiles = unique(projectProfiles); + let dashboardItemsCopy = []; + this.state.dashboardItems.map((item) => { + if (item.type !== DASHBOARD_ITEM_PROJECT_PROFILE) { + dashboardItemsCopy.push(item); + } + }); + projectProfiles.map((item) => { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROJECT_PROFILE, + item, + }); + this.handleShowProfile(item.key); + }); + this.setState({ projectProfiles, dashboardItems: dashboardItemsCopy }); + } + + getProfilesLength() { + let activeProfiles = []; + this.state.profiles.map((item) => { + if (activeProfiles.indexOf(item.key) === -1) { + activeProfiles.push(item.key); + } + }); + this.state.activeProfiles.map((item) => { + if (activeProfiles.indexOf(item.key) === -1) { + activeProfiles.push(item.key); + } + }); + return activeProfiles.length; + } + + getPlotsLength() { + return this.state.plots.length + this.state.projectPlots.length; + } + + handleHighlightPlot(plotId) { + if (!plotId) throw new Error(`Empty plot identifier`); + + this.setState( + { + highlightedPlot: plotId === this.state.highlightedPlot ? false : plotId, + }, + () => { + this.props.onHighlightedPlotChange( + this.state.highlightedPlot, + this.state.plots + ); + } + ); + } + + handleNewPlotNameChange(event) { + this.setState({ newPlotName: event.target.value }); + } + + _modifyAxes( + plotId, + gid, + measurementKey, + measurementIntakeIndex, + action, + measurementsData, + relation + ) { + if (!plotId) throw new Error(`Invalid plot identifier`); + if ( + (!gid && gid !== 0) || + !measurementKey || + (!measurementIntakeIndex && measurementIntakeIndex !== 0) + ) + throw new Error(`Invalid measurement location parameters`); + + let plots = JSON.parse(JSON.stringify(this.state.plots)); + let correspondingPlot = false; + let correspondingPlotIndex = false; + plots.map((plot, index) => { + if (plot.id === plotId) { + correspondingPlot = plot; + correspondingPlotIndex = index; + } + }); + + if (correspondingPlot === false) + throw new Error(`Plot with id ${plotId} does not exist`); + let measurementIndex = gid + ":_0:" + measurementIntakeIndex; + if (action === `add`) { + if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { + let measurementData = this.getFeatureByGidFromDataSource(gid); + if (measurementsData) { + measurementData = measurementsData; + } + if (measurementData) { + correspondingPlot.measurements.push(measurementIndex); + correspondingPlot.measurementsCachedData[measurementIndex] = + measurementData; + correspondingPlot.relations[measurementIndex] = relation; } else { - throw new Error(`Unrecognized action ${action}`); + throw new Error( + `Unable to find data for measurement index ${measurementIndex}` + ); } - - plots[correspondingPlotIndex] = correspondingPlot; - - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - if (item.item.id === correspondingPlot.id) { - dashboardItemsCopy[index].item = correspondingPlot; - return false; - } - } - }); - //console.log("dashboardItemsCopy", dashboardItemsCopy) - //console.log("plots", plots) - this.plotManager.update(correspondingPlot).then(() => { - this.setState({ - plots, - projectPlots: plots, - dashboardItems: dashboardItemsCopy - }); - this.props.onPlotsChange(this.getPlots()); - }).catch(error => { - console.error(`Error occured while updating plot (${error})`) - }); - } - - setDataSource(dataSource) { - let plots = JSON.parse(JSON.stringify(this.state.plots)); - let updatePlotsPromises = []; - for (let i = 0; i < plots.length; i++) { - let plot = plots[i]; - let plotWasUpdatedAtLeastOnce = false; - if (typeof plot.measurements === "undefined") { - break; - } - plot.measurements.map(measurementIndex => { - let splitMeasurementIndex = measurementIndex.split(`:`); - if (splitMeasurementIndex.length !== 3 && splitMeasurementIndex.length !== 4) throw new Error(`Invalid measurement index`); - let measurementData = false; - dataSource.map(item => { - if (item.properties.boreholeno === parseInt(splitMeasurementIndex[0])) { - measurementData = item; - return false; - } - }); - - if (measurementData) { - let currentTime = new Date(); - plot.measurementsCachedData[measurementIndex] = { - data: measurementData, - created_at: currentTime.toISOString() - }; - - plotWasUpdatedAtLeastOnce = true; - } - }); - - if (plotWasUpdatedAtLeastOnce) { - updatePlotsPromises.push(this.plotManager.update(plot)); - } + } + } else if (action === `delete`) { + if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { + throw new Error( + `Unable to find measurement ${measurementIndex} for ${plotId} plot` + ); + } else { + if (measurementIndex in correspondingPlot.measurementsCachedData) { + correspondingPlot.measurements.splice( + correspondingPlot.measurements.indexOf(measurementIndex), + 1 + ); + delete correspondingPlot.measurementsCachedData[measurementIndex]; + } else { + throw new Error( + `Data integrity violation: plot ${plotId} does not contain cached data for measurement ${measurementIndex}` + ); } - - Promise.all(updatePlotsPromises).then(() => { - let dashboardItemsCopy = JSON.parse(JSON.stringify(this.state.dashboardItems)); - dashboardItemsCopy.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - plots.map(updatedPlot => { - if (item.item.id === updatedPlot.id) { - dashboardItemsCopy[index].item = updatedPlot; - return false; - } - }); - } - }); - - this.setState({dataSource, plots, dashboardItems: dashboardItemsCopy}); - }).catch(errors => { - console.error(`Unable to update measurement data upon updating the data source`, errors); - }); + } + } else { + throw new Error(`Unrecognized action ${action}`); } - - getFeatureByGidFromDataSource(boreholeno, check = true) { - if (check === false) { - throw new Error(`Invalid boreholeno ${boreholeno} was provided`); + plots[correspondingPlotIndex] = correspondingPlot; + + let dashboardItemsCopy = JSON.parse( + JSON.stringify(this.state.dashboardItems) + ); + dashboardItemsCopy.map((item, index) => { + if ( + item.type === DASHBOARD_ITEM_PLOT || + item.type === DASHBOARD_ITEM_PROJECT_PLOT + ) { + if (item.item.id === correspondingPlot.id) { + dashboardItemsCopy[index].item = correspondingPlot; + return false; } - - let featureWasFound = false; - this.state.dataSource.map(item => { - if (item.properties.boreholeno === boreholeno) { - featureWasFound = item; - return false; - } + } + }); + this.setState({ + plots, + projectPlots: plots, + dashboardItems: dashboardItemsCopy, + }); + this.plotManager + .update(correspondingPlot) + .then(() => { + this.props.onPlotsChange(this.getPlots(), this.context); + }) + .catch((error) => { + console.error(`Error occured while updating plot (${error})`); + }); + } + + setDataSource(dataSource) { + let plots = JSON.parse(JSON.stringify(this.state.plots)); + let updatePlotsPromises = []; + for (let i = 0; i < plots.length; i++) { + let plot = plots[i]; + let plotWasUpdatedAtLeastOnce = false; + if (typeof plot.measurements === "undefined") { + break; + } + plot.measurements.map((measurementIndex) => { + let splitMeasurementIndex = measurementIndex.split(`:`); + if ( + splitMeasurementIndex.length !== 3 && + splitMeasurementIndex.length !== 4 + ) + throw new Error(`Invalid measurement index`); + let measurementData = false; + dataSource.map((item) => { + if ( + item.properties.boreholeno === parseInt(splitMeasurementIndex[0]) + ) { + measurementData = item; + return false; + } }); - return featureWasFound; - } - - addMeasurement(plotId, gid, measurementKey, measurementIntakeIndex) { - this._modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, `add`); - } - - deleteMeasurement(plotId, gid, measurementKey, measurementIntakeIndex) { - this._modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, `delete`); - } + if (measurementData) { + let currentTime = new Date(); + plot.measurementsCachedData[measurementIndex] = { + data: measurementData, + created_at: currentTime.toISOString(), + }; - handlePlotSort({oldIndex, newIndex}) { - this.setState(({dashboardItems}) => ({ - dashboardItems: arrayMove(dashboardItems, oldIndex, newIndex) - })); - }; - - onSetMin() { - $(PLOTS_ID).animate({ - top: ($(document).height() - modalHeaderHeight) + 'px' - }, 500, function () { - $(PLOTS_ID).find('.modal-body').css(`max-height`, modalHeaderHeight + 'px'); - }); + plotWasUpdatedAtLeastOnce = true; + } + }); - $('.js-expand-less').hide(); - $('.js-expand-half').show(); - $('.js-expand-more').show(); - $(PLOTS_ID + ' .modal-body').css(`visibility`, `hidden`); + if (plotWasUpdatedAtLeastOnce) { + updatePlotsPromises.push(this.plotManager.update(plot)); + } } - onSetHalf() { - $(PLOTS_ID).animate({ - top: "50%" - }, 500, function () { - $(PLOTS_ID).find('.modal-body').css(`max-height`, ($(document).height() * 0.5 - modalHeaderHeight - 20) + 'px'); + Promise.all(updatePlotsPromises) + .then(() => { + let dashboardItemsCopy = JSON.parse( + JSON.stringify(this.state.dashboardItems) + ); + dashboardItemsCopy.map((item, index) => { + if ( + item.type === DASHBOARD_ITEM_PLOT || + item.type === DASHBOARD_ITEM_PROJECT_PLOT + ) { + plots.map((updatedPlot) => { + if (item.item.id === updatedPlot.id) { + dashboardItemsCopy[index].item = updatedPlot; + return false; + } + }); + } }); - $('.js-expand-less').show(); - $('.js-expand-half').hide(); - $('.js-expand-more').show(); - $(PLOTS_ID + ' .modal-body').css(`visibility`, `visible`); - } - - onSetMax() { - $(PLOTS_ID).animate({ - top: "10%" - }, 500, function () { - $(PLOTS_ID).find('.modal-body').css(`max-height`, ($(document).height() * 0.9 - modalHeaderHeight - 10) + 'px'); + this.setState({ + dataSource, + plots, + dashboardItems: dashboardItemsCopy, }); - - $('.js-expand-less').show(); - $('.js-expand-half').show(); - $('.js-expand-more').hide(); - $(PLOTS_ID + ' .modal-body').css(`visibility`, `visible`); + }) + .catch((errors) => { + console.error( + `Unable to update measurement data upon updating the data source`, + errors + ); + }); + } + + getFeatureByGidFromDataSource(boreholeno, check = true) { + if (check === false) { + throw new Error(`Invalid boreholeno ${boreholeno} was provided`); } - nextDisplayType() { - if (currentDisplay === DISPLAY_MIN) { - this.onSetHalf(); - currentDisplay = DISPLAY_HALF; - previousDisplay = DISPLAY_MIN; - } else if (currentDisplay === DISPLAY_HALF) { - if (previousDisplay === DISPLAY_MIN) { - this.onSetMax(); - currentDisplay = DISPLAY_MAX; - } else { - this.onSetMin(); - currentDisplay = DISPLAY_MIN; - } - - previousDisplay = DISPLAY_HALF; - } else if (currentDisplay === DISPLAY_MAX) { - this.onSetHalf(); - currentDisplay = DISPLAY_HALF; - previousDisplay = DISPLAY_MAX; - } + let featureWasFound = false; + this.state.dataSource.map((item) => { + if (item.properties.boreholeno === boreholeno) { + featureWasFound = item; + return false; + } + }); + + return featureWasFound; + } + + addMeasurement( + plotId, + gid, + measurementKey, + measurementIntakeIndex, + measurementsData, + relation + ) { + this._modifyAxes( + plotId, + gid, + measurementKey, + measurementIntakeIndex, + `add`, + measurementsData, + relation + ); + } + + deleteMeasurement(plotId, gid, measurementKey, measurementIntakeIndex) { + this._modifyAxes( + plotId, + gid, + measurementKey, + measurementIntakeIndex, + `delete` + ); + } + + handlePlotSort({ oldIndex, newIndex }) { + this.setState(({ dashboardItems }) => ({ + dashboardItems: arrayMove(dashboardItems, oldIndex, newIndex), + })); + } + + onSetMin() { + $(PLOTS_ID).animate( + { + top: $(document).height() - modalHeaderHeight + "px", + }, + 500, + function () { + $(PLOTS_ID) + .find(".modal-body") + .css(`max-height`, modalHeaderHeight + "px"); + } + ); + + $(".js-expand-less").hide(); + $(".js-expand-half").show(); + $(".js-expand-more").show(); + $(PLOTS_ID + " .modal-body").css(`visibility`, `hidden`); + } + + onSetHalf() { + $(PLOTS_ID).animate( + { + top: "50%", + }, + 500, + function () { + $(PLOTS_ID) + .find(".modal-body") + .css( + `max-height`, + $(document).height() * 0.5 - modalHeaderHeight - 20 + "px" + ); + } + ); + + $(".js-expand-less").show(); + $(".js-expand-half").hide(); + $(".js-expand-more").show(); + $(PLOTS_ID + " .modal-body").css(`visibility`, `visible`); + } + + onSetMax() { + $(PLOTS_ID).animate( + { + top: "10%", + }, + 500, + function () { + $(PLOTS_ID) + .find(".modal-body") + .css( + `max-height`, + $(document).height() * 0.9 - modalHeaderHeight - 10 + "px" + ); + } + ); + + $(".js-expand-less").show(); + $(".js-expand-half").show(); + $(".js-expand-more").hide(); + $(PLOTS_ID + " .modal-body").css(`visibility`, `visible`); + } + + nextDisplayType() { + if (currentDisplay === DISPLAY_MIN) { + this.onSetHalf(); + currentDisplay = DISPLAY_HALF; + previousDisplay = DISPLAY_MIN; + } else if (currentDisplay === DISPLAY_HALF) { + if (previousDisplay === DISPLAY_MIN) { + this.onSetMax(); + currentDisplay = DISPLAY_MAX; + } else { + this.onSetMin(); + currentDisplay = DISPLAY_MIN; + } + + previousDisplay = DISPLAY_HALF; + } else if (currentDisplay === DISPLAY_MAX) { + this.onSetHalf(); + currentDisplay = DISPLAY_HALF; + previousDisplay = DISPLAY_MAX; } + } - render() { - setTimeout(() => { - if (!syncInProg) { - //console.log("Syncing plots") - //this.syncPlotData(); - } - // Debounce sync - syncInProg = true; - setTimeout(() => syncInProg = false, 2000); - }, 500); - - let plotsControls = (

{__(`No timeseries were created or set as active yet`)}

); - - // Actualize elements location - if (currentDisplay === DISPLAY_MIN) { - this.onSetMin(); - } else if (currentDisplay === DISPLAY_HALF) { - this.onSetHalf(); - } else if (currentDisplay === DISPLAY_MAX) { - this.onSetMax(); - } - - let listItemHeightPx = Math.round(($(document).height() * 0.9 - modalHeaderHeight - 10) / 2); - - let localPlotsControls = []; - let plottedProfiles = []; - this.state.dashboardItems.map((item, index) => { - if (item.type === DASHBOARD_ITEM_PLOT || item.type === DASHBOARD_ITEM_PROJECT_PLOT) { - let plot = item.item; - let plotId = plot.id || plot.key; - if (this.state.activePlots.indexOf(plotId) > -1) { - localPlotsControls.push(); - } - } else if (item.type === DASHBOARD_ITEM_PROFILE || item.type === DASHBOARD_ITEM_PROJECT_PROFILE) { - if (plottedProfiles.indexOf(item.item.key) > -1) { - return; - } - let profile = item.item; - if (this.state.activeProfiles.indexOf(profile.key) > -1) { - localPlotsControls.push(); - plottedProfiles.push(profile.key); - } - } else { - throw new Error(`Unrecognized dashboard item type ${item.type}`); - } - }); - - if (localPlotsControls.length > 0) { - plotsControls = ( - {localPlotsControls}); - } - - const setNoExpanded = () => { - currentDisplay = DISPLAY_HALF; - previousDisplay = DISPLAY_MAX; - this.nextDisplayType(); - }; - - const setHalfExpanded = () => { - currentDisplay = DISPLAY_MIN; - previousDisplay = DISPLAY_HALF; - this.nextDisplayType(); - }; - - const setFullExpanded = () => { - currentDisplay = DISPLAY_HALF; - previousDisplay = DISPLAY_MIN; - this.nextDisplayType(); - }; - - return (
-
- -
-
-
-
- - - -
-
-
- {__(`Calypso dashboard`)} -
-
-

- ({__(`Timeseries total`).toLowerCase()}: {this.getPlotsLength()}, {__(`timeseries active`)}: {this.state.activePlots.length}; {__(`Profiles total`).toLowerCase()}: {this.getProfilesLength()}, {__(`profiles active`)}: {this.state.activeProfiles.length}) -

-
-
-
- -
-
- - -
-
-
-
-
-
-
{plotsControls}
-
-
); - } + render() { + return
; + } } DashboardComponent.propTypes = { - initialPlots: PropTypes.array.isRequired, - onOpenBorehole: PropTypes.func.isRequired, - onPlotsChange: PropTypes.func.isRequired, - onActivePlotsChange: PropTypes.func.isRequired, - onHighlightedPlotChange: PropTypes.func.isRequired, + initialPlots: PropTypes.array.isRequired, + //onOpenBorehole: PropTypes.func.isRequired, + onPlotsChange: PropTypes.func.isRequired, + onActivePlotsChange: PropTypes.func.isRequired, + onHighlightedPlotChange: PropTypes.func.isRequired, }; export default DashboardComponent; diff --git a/browser/components/DashboardWrapper.js b/browser/components/DashboardWrapper.js new file mode 100644 index 0000000..849f019 --- /dev/null +++ b/browser/components/DashboardWrapper.js @@ -0,0 +1,41 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import reduxStore from "../redux/store"; +import { Provider } from "react-redux"; +import { ToastContainer } from "react-toastify"; +import ThemeProvider from "../themes/ThemeProvider"; +import ProjectProvider from "../contexts/project/ProjectProvider"; +import DashboardShell from "./dashboardshell/DashboardShell"; +import DashboardComponent from "./DashboardComponent"; + +const DASHBOARD_CONTAINER_ID = "watsonc-plots-dialog-form"; +const HIDDEN_DIV = "watsonc-plots-dialog-form-hidden"; + +const DashboardWrapper = React.forwardRef((props, ref) => { + const DashboardShellWrapper = (props) => + ReactDOM.createPortal( + + + + + + , + document.getElementById(DASHBOARD_CONTAINER_ID) + ); + + const DashboardComponentWrapper = React.forwardRef((props, ref) => + ReactDOM.createPortal( + , + document.getElementById(HIDDEN_DIV) + ) + ); + + return ( + + + + + ); +}); + +export default DashboardWrapper; diff --git a/browser/components/DataSelectorFilterComponent.js b/browser/components/DataSelectorFilterComponent.js deleted file mode 100644 index 8e1a679..0000000 --- a/browser/components/DataSelectorFilterComponent.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import {connect} from 'react-redux'; - -const DEFAULT_FILTER_COUNT = 3; - -class DataSelectorFilterComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - filters: { - filter1: { - label: 'Filter 1', - operators: ['<', '>', '<='], - type: 'number' - }, - filter2: { - label: 'Filter 2', - operators: ['===', '!='], - type: 'text' - } - }, - selectedFilters: [{filter: null, selectedOperator: null, type: 'text', value: null, operators: []}, {filter: null, selectedOperator: null, type: 'text', value: null, operators: []}, {filter: null, selectedOperator: null, type: 'text', value: null, operators: []}], - - }; - } - - componentDidMount() { - } - - handleFilterChange(event, index) { - var selectedFilters = this.state.selectedFilters; - var filterInfo = this.state.filters[event.target.value]; - var operators = []; - var type = 'text' - if (filterInfo) { - operators = filterInfo.operators; - type = filterInfo.type; - } - selectedFilters[index].operators = operators; - selectedFilters[index].type = type; - this.setState(selectedFilters); - } - - getFilters(filterCount) { - return this.state.selectedFilters.map((filter, index) => { - return - - - - - - - - - - - }); - } - - render() { - return ( -
- {__('Match')} - - {__(' the conditions')} -
- - - {this.getFilters(DEFAULT_FILTER_COUNT)} - -
-
-
) - } -} - -DataSelectorFilterComponent.propTypes = { -}; - - -const mapStateToProps = state => ({ -}); - -const mapDispatchToProps = dispatch => ({ -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DataSelectorFilterComponent); diff --git a/browser/components/DataSelectorGlobalFilterComponent.js b/browser/components/DataSelectorGlobalFilterComponent.js deleted file mode 100644 index 036ab2b..0000000 --- a/browser/components/DataSelectorGlobalFilterComponent.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import {connect} from 'react-redux'; -import {selectStartDate, selectEndDate, selectMeasurementCount} from "../redux/actions"; - -class DataSelectorGlobalFilterComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - startDate: null, - endDate: null, - count: null - }; - } - - componentDidMount() { - } - - render() { - return ( -
-
-

Periode

-
-
Fra { - e.preventDefault(); - this.props.selectStartDate(e.target.value) - }} className='form-control' - value={this.props.selectedStartDate}/>
-
Til { - e.preventDefault(); - this.props.selectEndDate(e.target.value) - }} className='form-control' - value={this.props.selectedEndDate}/>
-
-
-
-

Antal målinger

- { - e.preventDefault(); - this.props.selectMeasurementCount(e.target.value) - }} className='form-control' - value={this.props.selectedMeasurementCount}/> -
-
-
-
) - } -} - -DataSelectorGlobalFilterComponent.propTypes = {}; - -const mapStateToProps = state => ({ - selectedStartDate: state.global.selectedStartDate, - selectedEndDate: state.global.selectedEndDate, - selectedMeasurementCount: state.global.selectedMeasurementCount -}); - -const mapDispatchToProps = dispatch => ({ - selectStartDate: (date) => dispatch(selectStartDate(date)), - selectEndDate: (date) => dispatch(selectEndDate(date)), - selectMeasurementCount: (count) => dispatch(selectMeasurementCount(count)) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DataSelectorGlobalFilterComponent); diff --git a/browser/components/DataSourceSelector.js b/browser/components/DataSourceSelector.js deleted file mode 100644 index b33e357..0000000 --- a/browser/components/DataSourceSelector.js +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; -import DataSelectorFilterComponent from './DataSelectorFilterComponent'; - -import {selectLayer, unselectLayer} from '../redux/actions' - -/** - * Data source selector - */ -class DataSourceSelector extends React.Component { - constructor(props) { - super(props); - this.state = { - openFilter: null - } - } - - componentDidMount() { - // If coming from state link, then check off boxes - if (typeof this.props.enabledLoctypeIds !== "undefined" && typeof this.props.urlparser !== "undefined" && this.props.urlparser.urlVars && this.props.urlparser.urlVars.state) { - if (this.props.boreholes) { - $(`#key0`).trigger("click"); - } - this.props.enabledLoctypeIds.forEach((v) => { - let key; - switch (v) { - case "1": - key = `key1`; - break; - case "3": - key = `key2`; - break; - case "4": - key = `key3`; - break; - case "5": - key = `key4`; - break; - } - $(`#${key}`).trigger("click"); - }); - } - } - - render() { - const generateLayerRecord = (key, item) => { - let selected = (this.props.selectedLayers.indexOf(item.originalLayerKey + (item.additionalKey ? `#${item.additionalKey}` : ``)) !== -1); - let titles = [] - let itemTitle = item.title; - if (itemTitle) { - titles.push(itemTitle); - } - let translatedTitle = __(item.title); - if (translatedTitle && titles.indexOf(translatedTitle) === -1) { - titles.push(translatedTitle) - } - - return (
-
- - {/**/} - {/*{this.state.openFilter === key ? : null}*/} -
-
); - }; - - return (
-

{__(`Please select at least one layer`)}

-
-

Grundvand

- {generateLayerRecord(`key0`, this.props.layers[0])} - {generateLayerRecord(`key4`, this.props.layers[4])} - {generateLayerRecord(`key1`, this.props.layers[1])} -

Vandløb, kyst, bassiner

- {generateLayerRecord(`key2`, this.props.layers[2])} -

Nedbør

- {generateLayerRecord(`key3`, this.props.layers[3])} -
-
); - } -} - -DataSourceSelector.propTypes = { - layers: PropTypes.array.isRequired, -}; - -const mapStateToProps = state => ({ - selectedLayers: state.global.selectedLayers -}); - -const mapDispatchToProps = dispatch => ({ - selectLayer: (originalLayerKey, additionalKey) => dispatch(selectLayer(originalLayerKey, additionalKey)), - unselectLayer: (originalLayerKey, additionalKey) => dispatch(unselectLayer(originalLayerKey, additionalKey)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DataSourceSelector); diff --git a/browser/components/IntroModal.js b/browser/components/IntroModal.js deleted file mode 100644 index c16ad8b..0000000 --- a/browser/components/IntroModal.js +++ /dev/null @@ -1,217 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux' - -import DataSourceSelector from './DataSourceSelector'; -import ChemicalSelector from './ChemicalSelector'; -import GlobalFilter from './DataSelectorGlobalFilterComponent'; -import {selectStartDate, setCategories} from '../redux/actions'; - -import StateSnapshotsDashboard from './../../../../browser/modules/stateSnapshots/components/StateSnapshotsDashboard'; - -const MODE_INDEX = 0; -const MODE_NEW = 1; -const MODE_SELECT = 2; - -/** - * Intro modal window content - */ -class IntroModal extends React.Component { - constructor(props) { - super(props); - - this.state = { - mode: MODE_INDEX, - layers: this.props.layers, - initialCategories: props.categories, - }; - } - - componentDidMount() { - if (this.state.initialCategories) { - this.props.setCategories(this.state.initialCategories); - } - } - - setCategories(categories) { - this.props.setCategories(categories); - } - - applyParameters() { - this.props.onApply({ - layers: this.props.selectedLayers, - chemical: (this.props.selectedChemical ? this.props.selectedChemical : false), - selectedStartDate: this.props.selectedStartDate, - selectedEndDate: this.props.selectedEndDate, - selectedMeasurementCount: this.props.selectedMeasurementCount, - }); - } - - render() { - let modalBodyStyle = { - paddingLeft: `0px`, - paddingRight: `0px`, - paddingTop: `35px`, - paddingBottom: `0px` - }; - - let buttonColumnStyle = { - paddingTop: `30px`, - backgroundColor: `rgb(0, 150, 136)`, - height: `90px`, - }; - - let buttonStyle = { - color: `white`, - fontSize: `20px`, - fontWeight: `600`, - cursor: `pointer` - }; - - let leftColumnBorder = {borderRadius: `0px 0px 0px 40px`}; - let rightColumnBorder = {borderRadius: `0px 0px 40px 0px`}; - if (this.state.mode !== MODE_INDEX) { - modalBodyStyle.minHeight = `550px`; - leftColumnBorder = {}; - rightColumnBorder = {}; - } - - let shadowStyle = { - boxShadow: `0 6px 8px 0 rgba(0, 0, 0, 0.5), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`, - zIndex: 1000 - }; - - return (
-
-

- {__(`Welcome to Calypso`)} -

-
-
-
-
-
{ - this.setState({mode: MODE_NEW}) - }}> -
- {__(`New project`)} {this.state.mode === MODE_NEW ? ( - ) : ()} -
-
-
{ - this.setState({mode: MODE_SELECT}) - }}> -
- {__(`Open existing project`)} {this.state.mode === MODE_SELECT ? ( - ) : ()} -
-
-
-
- - {this.state.mode === MODE_NEW ? (
-
-
- -
-
- -
-
-
) : false} - - {this.state.mode === MODE_SELECT ? (
-
-
- {this.props.authenticated === false ? ( - - lock_open - {__(`Sign in in order to access user projects`)} - ) : false} -
-
-
-
- -
-
-
) - - - /* (
-
-
- Her bliver det muligt at åbne et gemt projekt. Et projekt kan indeholde tidsserie-grafer, profiler mv. -
-
-
- )*/ : false} -
- -
- {this.state.mode === MODE_NEW ? (
-
-
- -
-
-
) : false} -
-
- {this.state.mode === MODE_NEW ? (
-
-
- -
-
-
) : false} -
-
); - } -} - -IntroModal.propTypes = { - layers: PropTypes.array.isRequired, - categories: PropTypes.object.isRequired, - onApply: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -const mapStateToProps = state => ({ - authenticated: state.global.authenticated, - selectedLayers: state.global.selectedLayers, - selectedChemical: state.global.selectedChemical, - selectedMeasurementCount: state.global.selectedMeasurementCount, - selectedStartDate: state.global.selectedStartDate, - selectedEndDate: state.global.selectedEndDate, -}); - -const mapDispatchToProps = dispatch => ({ - setCategories: (categories) => dispatch(setCategories(categories)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(IntroModal); diff --git a/browser/components/LoginModal.js b/browser/components/LoginModal.js new file mode 100644 index 0000000..59f73ed --- /dev/null +++ b/browser/components/LoginModal.js @@ -0,0 +1,259 @@ +import React from "react"; +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import CloseButton from "./shared/button/CloseButton"; +import ButtonGroup from "./shared/button/ButtonGroup"; +import Button from "./shared/button/Button"; +import { Variants } from "./shared/constants/variants"; +import Card from "./shared/card/Card"; +import Title from "./shared/title/Title"; +import TextInput from "./shared/inputs/TextInput"; +import { LOGIN_MODAL_DIALOG_PREFIX } from "./../constants"; +import { useState, useEffect, useRef } from "react"; +import useInterval from "./shared/hooks/useInterval"; + +const LoginModal = (props) => { + const [userName, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [statusText, setStatusText] = useState(""); + const [loggedIn, setLoggedIn] = useState(props.session.isAuthenticated()); + const [stopPoll, setStopPoll] = useState(false); + const timer = useRef(null); + + useInterval( + () => { + if (props.session.isStatusChecked()) { + setStopPoll(true); + setLoggedIn(props.session.isAuthenticated()); + if (!props.session.isAuthenticated()) { + console.log(props.urlparser); + if (!props.urlparser.urlVars || !props.urlparser.urlVars.state) { + $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("show"); + $("#watsonc-menu-dialog").modal("hide"); + } + } + } + }, + stopPoll ? null : 1000 + ); + + useEffect(() => { + props.backboneEvents.get().on(`session:authChange`, (authenticated) => { + clearTimeout(timer.current); + if (authenticated) { + setLoggedIn(true); + setUserName(""); + setPassword(""); + setTimeout(() => { + $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("hide"); + $("#watsonc-menu-dialog").modal("show"); + }, 1500); + } else { + setUserName(""); + setPassword(""); + setLoggedIn(false); + setStatusText("Du er nu logget ud"); + setTimeout(() => { + setStatusText(""); + }, 2000); + } + }); + }, []); + + const log_in = () => { + var input = document.getElementById("sessionScreenName"); + let lastValue = input.value; + input.value = userName; + let event = new Event("change", { bubbles: true }); + let tracker = input._valueTracker; + if (tracker) { + tracker.setValue(lastValue); + } + input.dispatchEvent(event); + + var input = document.getElementById("sessionPassword"); + lastValue = input.value; + input.value = password; + event = new Event("change", { bubbles: true }); + tracker = input._valueTracker; + if (tracker) { + tracker.setValue(lastValue); + } + input.dispatchEvent(event); + + $("#login-modal .btn-raised").removeAttr("disabled"); + setTimeout(() => { + $("#login-modal .btn-raised").click(); + }, 100); + + timer.current = setTimeout(() => { + if ($("#login-modal .alert-dismissible").text().includes("Wrong")) { + setStatusText("Forkert brugernavn og eller password"); + } else { + setStatusText("Noget gik galt. Prøv igen senere."); + } + }, 1000); + }; + + const log_out = () => { + $("#login-modal .btn-raised").click(); + }; + + const close_window = () => { + $(`#${LOGIN_MODAL_DIALOG_PREFIX}`).modal("hide"); + if (!loggedIn) { + $("#watsonc-menu-dialog").modal("show"); + } + }; + + return ( + + + + + + + + + </Grid> + </Grid> + </ModalHeader> + <ModalBody> + {(statusText !== "" || loggedIn) && ( + <Grid + container + spacing={8} + direction="column" + alignItems="center" + justify="center" + > + <Title + text={loggedIn ? `Du er logget ind` : statusText} + color={DarkTheme.colors.headings} + level={3} + /> + </Grid> + )} + {!loggedIn && ( + <> + <Grid + container + spacing={8} + direction="row" + alignItems="center" + justify="center" + > + <Title + text={"Log ind eller"} + color={DarkTheme.colors.headings} + level={4} + /> + <Title + text={ + <a + href="https://calypso.watsonc.dk/#create-user" + target="_blank" + style={{ color: DarkTheme.colors.interaction[4] }} + > +  opret bruger + </a> + } + color={DarkTheme.colors.interaction[4]} + level={4} + onClick={() => console.log("hej")} + /> + </Grid> + + <Grid + container + spacing={8} + direction="column" + alignItems="center" + justify="center" + > + <Grid item xs={10}> + <TextInput + value={userName} + onChange={setUserName} + placeholder={"Brugernavn"} + /> + </Grid> + <Grid item xs={10}> + <TextInput + value={password} + onChange={setPassword} + placeholder={"Password"} + password + onKeyDown={(e) => { + if (e.key === "Enter") { + log_in(); + } + }} + /> + </Grid> + </Grid> + </> + )} + <ButtonGroup align={Align.Center}> + {!loggedIn && ( + <Button + text={__("Log ind")} + variant={ + userName == "" && password == "" + ? Variants.PrimaryDisabled + : Variants.Primary + } + onClick={() => { + log_in(); + }} + size={Size.Large} + disabled={userName == "" && password == ""} + /> + )} + {loggedIn && ( + <Button + text={__("Log ud")} + variant={Variants.Primary} + onClick={() => { + log_out(); + }} + size={Size.Large} + /> + )} + <Button + text={__("Luk vindue")} + variant={Variants.None} + onClick={close_window} + size={Size.Large} + /> + </ButtonGroup> + </ModalBody> + </Root> + ); +}; + +const Root = styled.div` + background: ${({ theme }) => hexToRgbA(theme.colors.primary[1], 0.96)}; + border-radius: ${({ theme }) => `${theme.layout.borderRadius.large}px`}; + color: ${({ theme }) => `${theme.colors.headings}`}; +`; + +const ModalHeader = styled.div` + padding: ${({ theme }) => + `${theme.layout.gutter}px ${theme.layout.gutter}px 0 ${theme.layout.gutter}px`}; +`; + +const ModalBody = styled.div` + padding: ${({ theme }) => `${theme.layout.gutter}px`}; +`; +export default LoginModal; diff --git a/browser/components/MenuComponent.js b/browser/components/MenuComponent.js deleted file mode 100644 index 5c1f391..0000000 --- a/browser/components/MenuComponent.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import TitleFieldComponent from './../../../../browser/modules/shared/TitleFieldComponent'; -import MenuPlotComponent from './MenuPlotComponent'; - -const uuidv4 = require('uuid/v4'); - -/** - * Component creates plots management form and is the source of truth for plots overall - */ -class MenuPanelComponent extends React.Component { - constructor(props) { - super(props); - - this.state = { - newPlotName: ``, - plots: this.props.initialPlots, - dataSource: [] - }; - - this.handleCreatePlot = this.handleCreatePlot.bind(this); - this.handleDeletePlot = this.handleDeletePlot.bind(this); - this.getFeatureByGidFromDataSource = this.getFeatureByGidFromDataSource.bind(this); - this.handleNewPlotNameChange = this.handleNewPlotNameChange.bind(this); - } - - componentDidMount() {} - - getPlots() { - return JSON.parse(JSON.stringify(this.state.plots)); - } - - addPlot(newPlotName) { - this.handleCreatePlot(newPlotName); - } - - setPlots(plots) { - this.setState({ plots }); - } - - handleCreatePlot(title) { - let plotsCopy = JSON.parse(JSON.stringify(this.state.plots)); - plotsCopy.push({ - id: uuidv4(), - title, - measurements: [], - }); - - this.setState({ plots: plotsCopy }); - this.props.onPlotsChange(plotsCopy); - } - - handleDeletePlot(id) { - let plotsCopy = JSON.parse(JSON.stringify(this.state.plots)); - let plotWasDeleted = false; - plotsCopy.map((plot, index) => { - if (plot.id === id) { - plotsCopy.splice(index, 1); - plotWasDeleted = true; - return false; - } - }); - - if (plotWasDeleted === false) { - throw new Error(`Unable to delete plot with id ${id}`); - } - - this.setState({ plots: plotsCopy }); - this.props.onPlotsChange(plotsCopy); - } - - handleNewPlotNameChange(event) { - this.setState({ newPlotName: event.target.value}); - } - - _modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, action) { - if (!plotId) throw new Error(`Invalid plot identifier`); - if ((!gid && gid !== 0) || !measurementKey || (!measurementIntakeIndex && measurementIntakeIndex !== 0)) throw new Error(`Invalid measurement location parameters`); - - let plots = JSON.parse(JSON.stringify(this.state.plots)); - let correspondingPlot = false; - let correspondingPlotIndex = false; - plots.map((plot, index) => { - if (plot.id === plotId) { - correspondingPlot = plot; - correspondingPlotIndex = index; - } - }); - - if (correspondingPlot === false) throw new Error(`Plot with id ${plotId} does not exist`); - let measurementIndex = gid + ':' + measurementKey + ':' + measurementIntakeIndex; - if (action === `add`) { - if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { - correspondingPlot.measurements.push(measurementIndex); - } - } else if (action === `delete`) { - if (correspondingPlot.measurements.indexOf(measurementIndex) === -1) { - throw new Error(`Unable to find measurement ${measurementIndex} for ${plotId} plot`); - } else { - correspondingPlot.measurements.splice(correspondingPlot.measurements.indexOf(measurementIndex), 1); - } - } else { - throw new Error(`Unrecognized action ${action}`); - } - - plots[correspondingPlotIndex] = correspondingPlot; - this.setState({ plots }); - this.props.onPlotsChange(plots); - } - - setDataSource(dataSource) { - this.setState({ dataSource }); - } - - getFeatureByGidFromDataSource(boreholeno) { - let featureWasFound = false; - this.state.dataSource.map(item => { - if (item.properties.boreholeno === boreholeno) { - featureWasFound = item; - return false; - } - }); - - return featureWasFound; - } - - addMeasurement(plotId, gid, measurementKey, measurementIntakeIndex) { - this._modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, `add`); - } - - deleteMeasurement(plotId, gid, measurementKey, measurementIntakeIndex) { - this._modifyAxes(plotId, gid, measurementKey, measurementIntakeIndex, `delete`); - } - - render() { - let plotsControls = (<p>{__(`No plots were created yet`)}</p>); - - if (this.state.dataSource.length > 0) { - let localPlotsControls = []; - this.state.plots.map((plot, index) => { - localPlotsControls.push(<li key={`borehole_plot_${index}`} className="list-group-item"> - <div> - <MenuPlotComponent - getFeatureByGid={(boreholeno) => { return this.getFeatureByGidFromDataSource(boreholeno)}} - onDelete={(id) => { this.handleDeletePlot(id)}} - plotMeta={plot}/> - </div> - </li>); - }); - - if (localPlotsControls.length > 0) { - plotsControls = (<ul className="list-group">{localPlotsControls}</ul>); - } - } - - return (<div> - <div> - <h4> - {__(`Plots`)} - <TitleFieldComponent onAdd={(title) => { this.handleCreatePlot(title) }} type="userOwned"/> - </h4> - </div> - <div>{plotsControls}</div> - </div>); - } -} - -MenuPanelComponent.propTypes = { - initialPlots: PropTypes.array.isRequired, - onPlotsChange: PropTypes.func.isRequired, -}; - -export default MenuPanelComponent; diff --git a/browser/components/MenuDataSourceAndTypeSelectorComponent.js b/browser/components/MenuDataSourceAndTypeSelectorComponent.js deleted file mode 100644 index 6abe02c..0000000 --- a/browser/components/MenuDataSourceAndTypeSelectorComponent.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {Provider} from 'react-redux'; -import {connect} from 'react-redux'; -import reduxStore from '../redux/store'; - -import DataSourceSelector from './DataSourceSelector'; -import ChemicalSelectorModal from './ChemicalSelectorModal'; -import GlobalFilter from './DataSelectorGlobalFilterComponent'; - -const utils = require('./../utils'); - -/** - * Creates data source and type selector - */ -class MenuDataSourceAndTypeSelectorComponent extends React.Component { - constructor(props) { - super(props); - } - - applyParameters() { - this.props.onApply({ - layers: this.props.selectedLayers, - chemical: (this.props.selectedChemical ? this.props.selectedChemical : false), - selectedStartDate: this.props.selectedStartDate, - selectedEndDate: this.props.selectedEndDate, - selectedMeasurementCount: this.props.selectedMeasurementCount, - }); - } - - render() { - let chemicalName = __(`Not selected`); - if (this.props.selectedChemical) { - chemicalName = utils.getChemicalName(this.props.selectedChemical, this.props.categories); - } - - return (<div> - <div style={{display: `flex`}}> - <div style={{flexGrow: `1`}}> - <DataSourceSelector layers={this.props.layers} enabledLoctypeIds={this.props.enabledLoctypeIds} - urlparser={this.props.urlparser} boreholes={this.props.boreholes}/> - </div> - <div style={{flexGrow: `1`}}> - <p>{__(`Select datatype`)}</p> - <p>{chemicalName} - <button - type="button" - disabled={this.props.selectedLayers.length === 0} - className="btn btn-primary btn-sm" - onClick={() => { - const dialogPrefix = `watsonc-select-chemical-dialog`; - const selectChemicalModalPlaceholderId = `${dialogPrefix}-placeholder`; - - if ($(`#${selectChemicalModalPlaceholderId}`).children().length > 0) { - ReactDOM.unmountComponentAtNode(document.getElementById(selectChemicalModalPlaceholderId)) - } - - try { - ReactDOM.render(<div> - <Provider store={reduxStore}> - <ChemicalSelectorModal onClickControl={() => { - $('#' + dialogPrefix).modal('hide'); - }}/> - </Provider> - </div>, document.getElementById(selectChemicalModalPlaceholderId)); - } catch (e) { - console.error(e); - } - - $('#' + dialogPrefix).modal({backdrop: `static`}); - }}><i className="fas fa-edit" title={__(`Edit`)}></i></button> - </p> - </div> - </div> - <div> - <GlobalFilter/> - </div> - <div> - <button - type="button" - disabled={this.props.selectedLayers.length === 0} - className="btn btn-raised btn-block btn-primary btn-sm" - onClick={this.applyParameters.bind(this)}>{__(`Apply`)}</button> - </div> - </div>); - } -} - -MenuDataSourceAndTypeSelectorComponent.propTypes = {}; - -const mapStateToProps = state => ({ - selectedLayers: state.global.selectedLayers, - selectedChemical: state.global.selectedChemical, - categories: state.global.categories, - selectedMeasurementCount: state.global.selectedMeasurementCount, - selectedStartDate: state.global.selectedStartDate, - selectedEndDate: state.global.selectedEndDate, -}); - -export default connect(mapStateToProps)(MenuDataSourceAndTypeSelectorComponent); diff --git a/browser/components/MenuPlotComponent.js b/browser/components/MenuPlotComponent.js deleted file mode 100644 index 21aa6ad..0000000 --- a/browser/components/MenuPlotComponent.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DropTarget } from 'react-dnd'; - -const utils = require('./../utils'); - -/** - * Plot component - */ -class ModalPlotComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let removeButtons = []; - this.props.plot.measurements.map((measurement, index) => { - let measurementDisplayTitle = measurement; - let splitMeasurementId = measurement.split(':'); - - let customGraph = -1, key, intakeIndex; - if (splitMeasurementId.length === 3) { - customGraph = false; - key = splitMeasurementId[1]; - intakeIndex = splitMeasurementId[2]; - } else if (splitMeasurementId.length === 4) { - customGraph = true; - key = splitMeasurementId[1] + ':' + splitMeasurementId[2]; - intakeIndex = splitMeasurementId[3]; - } else { - throw new Error(`Invalid measurement key (${measurement})`); - } - - let boreholeno = parseInt(splitMeasurementId[0]); - if (this.props.dataSource && this.props.dataSource.length > 0) { - this.props.dataSource.map(item => { - if (item.properties.boreholeno === boreholeno) { - if (customGraph) { - let json = JSON.parse(item.properties[splitMeasurementId[1]]).data[splitMeasurementId[2]]; - let intakeName = `#` + (parseInt(splitMeasurementId[3]) + 1); - if (`intakes` in json && Array.isArray(json.intakes) && json.intakes[parseInt(splitMeasurementId[3])] !== null) { - intakeName = json.intakes[parseInt(splitMeasurementId[3])]; - } - - measurementDisplayTitle = (`${item.properties.boreholeno}, ${json.data[0].name} (${intakeName})`); - return false; - } else { - let json = JSON.parse(item.properties[splitMeasurementId[1]]); - let intakeName = `#` + (parseInt(splitMeasurementId[2]) + 1); - if (`intakes` in json && Array.isArray(json.intakes) && json.intakes[parseInt(splitMeasurementId[2])] !== null) { - intakeName = json.intakes[parseInt(splitMeasurementId[2])]; - } - - let title = utils.getMeasurementTitle(item); - measurementDisplayTitle = (`${title}, ${json.title} (${intakeName})`); - return false; - } - } - }); - } - - const onDelete = () => { this.props.onDeleteMeasurement(this.props.plot.id, boreholeno, key, intakeIndex); }; - - removeButtons.push(<div key={`remove_measurement_` + index + `_` + splitMeasurementId[1] + `_` + splitMeasurementId[2]}> - <button - title={__(`Remove from time series`)} - type="button" - className="btn btn-sm btn-primary" - data-plot-id="{this.props.plot.id}" - data-gid="{boreholeno}" - data-key="{splitMeasurementId[1]}" - data-intake-index="{splitMeasurementId[2]}" - onClick={onDelete} - style={{ padding: `4px`, margin: `1px` }}> - <i className="fa fa-remove"></i> {measurementDisplayTitle} - </button> - </div>); - }); - - const isOver = this.props.isOver; - return this.props.connectDropTarget(<div - className="well well-sm js-plot" - data-id="{this.props.plot.id}" - style={{ - marginBottom: `18px`, - boxShadow: `0 4px 12px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19)`, - backgroundColor: (isOver ? `darkgreen` : ``), - color: (isOver ? `white` : ``), - }}> - <div>{this.props.plot.title}</div> - <div>{removeButtons}</div> - </div>); - } -} - -const plotTarget = { - drop(props, monitor) { - let item = monitor.getItem(); - item.onAddMeasurement(props.plot.id, item.boreholeno, item.itemKey, item.intakeIndex); - } -}; - -function collect(connect, monitor) { - return { - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver() - }; -} - -ModalPlotComponent.propTypes = { - onDeleteMeasurement: PropTypes.func.isRequired -}; - -export default DropTarget(`MEASUREMENT`, plotTarget, collect)(ModalPlotComponent); diff --git a/browser/components/MenuProfilesComponent.js b/browser/components/MenuProfilesComponent.js index 9afbef1..9cc6874 100644 --- a/browser/components/MenuProfilesComponent.js +++ b/browser/components/MenuProfilesComponent.js @@ -1,738 +1,983 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import axios from 'axios'; -import Slider from 'rc-slider'; -import {Provider} from 'react-redux'; -import {connect} from 'react-redux'; - -import {SELECT_CHEMICAL_DIALOG_PREFIX, FREE_PLAN_MAX_PROFILES_COUNT} from './../constants'; -import TitleFieldComponent from './../../../../browser/modules/shared/TitleFieldComponent'; -import LoadingOverlay from './../../../../browser/modules/shared/LoadingOverlay'; -import SearchFieldComponent from './../../../../browser/modules/shared/SearchFieldComponent'; - -import reduxStore from './../redux/store'; - -import {selectChemical} from './../redux/actions'; - -const utils = require('./../utils'); - -const wkt = require('terraformer-wkt-parser'); -const utmZone = require('./../../../../browser/modules/utmZone'); - - -const STEP_ENTER_NAME = -1; -const STEP_NOT_READY = 0; -const STEP_BEING_DRAWN = 1; -const STEP_READY_TO_LOAD = 2; -const STEP_CHOOSE_LAYERS = 3; - -const DEFAULT_API_URL = `/api/key-value`; - -let drawnItems = new L.FeatureGroup(), displayedItems = new L.FeatureGroup(), embedDrawControl = false; - -/** - * Component for creating profiles - */ -class MenuProfilesComponent extends React.Component { - constructor(props) { - super(props); - - - this.state = { - apiUrl: (props.apiUrl ? props.apiUrl : DEFAULT_API_URL), - loading: false, - localSelectedChemical: false, - showDrawingForm: true, - showExistingProfiles: true, - showProjectProfiles: true, - boreholeNames: [], - layers: [], - selectedLayers: [], - authenticated: props.authenticated ? props.authenticated : false, - profiles: (props.initialProfiles ? props.initialProfiles : []), - activeProfiles: (props.initialActiveProfiles ? props.initialActiveProfiles : []), - profile: false, - step: STEP_ENTER_NAME, - bufferedProfile: false, - profileBottom: -100, - buffer: 100, - newTitle: '', - profilesSearchTerm: '', - }; - - this.search = this.search.bind(this); - this.startDrawing = this.startDrawing.bind(this); - this.stopDrawing = this.stopDrawing.bind(this); - this.saveProfile = this.saveProfile.bind(this); - this.handleLayerSelect = this.handleLayerSelect.bind(this); - - props.cloud.get().map.addLayer(drawnItems); - props.cloud.get().map.addLayer(displayedItems); - - this.bufferSliderRef = React.createRef(); - this.bufferValueRef = React.createRef(); - this.onNewProfileAdd = this.onNewProfileAdd.bind(this); - this.canCreateProfile = this.canCreateProfile.bind(this); - - window.menuProfilesComponentInstance = this; - } - - componentDidMount() { - let _self = this; - this.props.backboneEvents.get().on(`session:authChange`, (authenticated) => { - if (_self.state.authenticated !== authenticated) { - _self.setState({authenticated}); - } - }); - - this.displayActiveProfiles(); - } - - canCreateProfile() { - if (this.props.license === 'premium') { - return true; - } else { - return this.getProfilesLength() < FREE_PLAN_MAX_PROFILES_COUNT; - - } - } - - displayActiveProfiles() { - displayedItems.eachLayer(layer => { - displayedItems.removeLayer(layer); - }); - - if (this.state.activeProfiles) { - this.state.activeProfiles.map(activeProfileKey => { - this.state.profiles.map(profile => { - if (profile.key === activeProfileKey) { - this.displayProfile(profile); - } - }); - }); - } - } - - setProfiles(profiles) { - this.setState({profiles}); - } - - setActiveProfiles(activeProfiles) { - this.setState({activeProfiles}, () => { - this.displayActiveProfiles(); - }); - } - - onNewProfileAdd(newTitle) { - if (!this.canCreateProfile()) { - $('#watsonc-limits-reached-text').show(); - $('#upgrade-modal').modal('show'); - return; - } - this.setState({newTitle, step: STEP_NOT_READY}); - } - - getProjectProfilesLength() { - let count = 0; - this.state.profiles.map(item => { - if (item.fromProject) { - count += 1; - } - }); - return count; - } - - getProfilesLength() { - let count = 0; - this.state.profiles.map(item => { - if (!item.fromProject) { - count += 1; - } - }); - return count; - } - - saveProfile() { - let layers = []; - this.state.layers.map(item => { - if (this.state.selectedLayers.indexOf(item.id) > -1) { - layers.push(item); - } - }); - - this.setState({loading: true}); - this.props.onProfileCreate({ - title: this.state.newTitle, - profile: this.state.profile, - buffer: this.state.buffer, - depth: this.state.profileBottom, - compound: this.state.localSelectedChemical, - boreholeNames: this.state.boreholeNames, - layers - }, true, () => { - this.setState({ - step: STEP_ENTER_NAME, - bufferedProfile: false, - profileBottom: -100, - buffer: 100, - newTitle: '', - loading: false - }); - }); - } - - handleProfileDelete(item) { - if (confirm(__(`Delete`) + ' ' + item.profile.title + '?')) { - this.setState({loading: true}); - this.props.onProfileDelete(item.key, () => { - this.setState({loading: false}); - }); - } - } - - handleLayerSelect(checked, layer) { - let layesrCopy = JSON.parse(JSON.stringify(this.state.selectedLayers)); - if (checked) { - if (layesrCopy.indexOf(layer.id) === -1) { - layesrCopy.push(layer.id); - } - } else { - if (layesrCopy.indexOf(layer.id) > -1) { - layesrCopy.splice(layesrCopy.indexOf(layer.id), 1); - } - } - - this.setState({selectedLayers: layesrCopy}) - } - - search() { - this.setState({ - step: STEP_NOT_READY, - layers: [], - selectedLayers: [] - }, () => { - this.stopDrawing(); - this.setState({loading: true}); - axios.post(`/api/extension/watsonc/intersection`, { - data: wkt.convert(this.state.bufferedProfile), - bufferRadius: this.state.buffer, - profileDepth: this.state.profileBottom, - profile: this.state.profile - }).then(response => { - let responseCopy = JSON.parse(JSON.stringify(response.data.result)); - response.data.result.map((item, index) => { - responseCopy[index].id = btoa(item.title); - }); - - this.setState({ - step: STEP_CHOOSE_LAYERS, - loading: false, - layers: responseCopy, - boreholeNames: response.data.boreholeNames - }); - }).catch(error => { - this.setState({loading: false}); - console.log(`Error occured`, error); - }); - }); - } - - clearDrawnLayers() { - drawnItems.eachLayer(layer => { - drawnItems.removeLayer(layer); - }); - } - - startDrawing() { - this.clearDrawnLayers(); - - if (embedDrawControl) embedDrawControl.disable(); - embedDrawControl = new L.Draw.Polyline(this.props.cloud.get().map); - embedDrawControl.enable(); - - embedDrawControl._map.off('draw:created'); - embedDrawControl._map.on('draw:created', e => { - if (embedDrawControl) embedDrawControl.disable(); - - let coord, layer = e.layer; - - let primitive = layer.toGeoJSON(); - if (primitive) { - if (typeof layer.getBounds !== "undefined") { - coord = layer.getBounds().getSouthWest(); - } else { - coord = layer.getLatLng(); - } - - // Get utm zone - var zone = utmZone.getZone(coord.lat, coord.lng); - var crss = { - "proj": "+proj=utm +zone=" + zone + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs", - "unproj": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs" - }; - - var reader = new jsts.io.GeoJSONReader(); - var writer = new jsts.io.GeoJSONWriter(); - var geom = reader.read(reproject.reproject(primitive, "unproj", "proj", crss)); - var buffer4326 = reproject.reproject(writer.write(geom.geometry.buffer(this.state.buffer)), "proj", "unproj", crss); - - L.geoJson(buffer4326, { - "color": "#ff7800", - "weight": 1, - "opacity": 1, - "fillOpacity": 0.1, - "dashArray": '5,3' - }).addTo(drawnItems); - - this.setState({ - step: STEP_READY_TO_LOAD, - bufferedProfile: buffer4326, - profile: primitive - }); - } - }); - } - - stopDrawing() { - if (drawnItems) drawnItems.clearLayers(); - if (embedDrawControl) embedDrawControl.disable(); - } - - displayProfile(data) { - this.clearDrawnLayers(); - let profile = data.profile.profile; - - // Get utm zone - var zone = utmZone.getZone(profile.geometry.coordinates[0][1], profile.geometry.coordinates[0][0]); - var crss = { - "proj": "+proj=utm +zone=" + zone + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs", - "unproj": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs" - }; - - let reader = new jsts.io.GeoJSONReader(); - let writer = new jsts.io.GeoJSONWriter(); - let geom = reader.read(reproject.reproject(profile, "unproj", "proj", crss)); - let buffer4326 = reproject.reproject(writer.write(geom.geometry.buffer(data.profile.buffer)), "proj", "unproj", crss); - - L.geoJson(buffer4326, { - "color": "#ff7800", - "weight": 1, - "opacity": 1, - "fillOpacity": 0.1, - "dashArray": '5,3' - }).addTo(displayedItems); - - var profileLayer = new L.geoJSON(profile); - - profileLayer.bindTooltip(data.profile.title, { - className: 'watsonc-profile-tooltip', - permanent: true, - offset: [0, 0] - }); - - profileLayer.addTo(displayedItems); - } - - handleProfileToggle(checked, profileKey) { - if (checked) { - this.props.onProfileShow(profileKey); - } else { - this.props.onProfileHide(profileKey); - } - } - - render() { - let overlay = false; - if (this.state.loading) { - overlay = (<LoadingOverlay/>); - } - - let availableLayers = (<div style={{textAlign: `center`}}> - <p>{__(`No layers found`)}</p> - </div>); - - if (this.state.layers && this.state.layers.length > 0) { - availableLayers = []; - - const generateLayerRecord = (item, index, prefix) => { - let points = []; - item.intersectionSegments.map(item => { - points.push(`${Math.round(item[0] / 1000)}km - ${Math.round(item[1] / 1000)}km`); - }); - - return (<div className="form-group" key={`${prefix}${index}`}> - <div className="checkbox"> - <label> - <input - type="checkbox" - checked={this.state.selectedLayers.indexOf(item.id) > -1} - onChange={(event) => { - this.handleLayerSelect(event.target.checked, item); - }}/> {item.title} - </label> - </div> - <div> - {item.subtitle} - <br/> - {__(`Stationing points`) + ': ' + points.join(', ')} - </div> - </div>); - }; - - this.state.layers.filter(item => item.type !== `geology`).map((item, index) => { - availableLayers.push(generateLayerRecord(item, index, `non_geology_layer_`)); - }); - if (availableLayers.length > 0) availableLayers.push(<hr style={{margin: `10px`}} key={`layer_divider`}/>); - this.state.layers.filter(item => item.type === `geology`).map((item, index) => { - availableLayers.push(generateLayerRecord(item, index, `geology_layer_`)); - }); - } - - let existingProfilesControls = (<div style={{textAlign: `center`}}> - <p>{__(`No profiles found`)}</p> - </div>); - let projectProfilesControls = (<div style={{textAlign: `center`}}> - <p>{__(`No profiles found`)}</p> - </div>); - - let plotRows = []; - let projectProfileRows = []; - this.state.profiles.map((item, index) => { - if (this.state.profilesSearchTerm.length > 0) { - if (item.profile.title.toLowerCase().indexOf(this.state.profilesSearchTerm.toLowerCase()) === -1) { - return; - } - } - var deleteButton = item.fromProject ? null : <td style={{textAlign: `right`}}> - <button - type="button" - className="btn btn-xs btn-primary" - title={__(`Delete profile`)} - onClick={(event) => { - this.handleProfileDelete(item); - }} - style={{padding: `4px`, margin: `0px`}}> - <i className="material-icons">delete</i> - </button> - </td> - var itemHtml = <tr key={`existing_profile_${index}`}> - <td> - <div> - <div style={{float: `left`}}> - <div className="checkbox"> - <label> - <input - type="checkbox" - name="enabled_profile" - checked={this.state.activeProfiles.indexOf(item.key) > -1} - onChange={(event) => { - this.handleProfileToggle(event.target.checked, item.key); - }}/> - </label> - </div> - </div> - <div style={{float: `left`, paddingLeft: `8px`, paddingTop: `2px`}}> - {item.profile.title} - </div> - </div> - </td> - <td style={{textAlign: `center`}}> - {item.profile.compound ? utils.getChemicalName(item.profile.compound, this.props.categories) : __(`Not selected`)} - </td> - {deleteButton} - </tr>; - if (item.fromProject === true) { - projectProfileRows.push(itemHtml); - } else { - plotRows.push(itemHtml); - } - }); - - if (plotRows.length > 0) { - existingProfilesControls = (<table className="table table-striped"> - <thead style={{color: `rgb(0, 150, 136)`}}> - <tr> - <th> - <div style={{float: `left`}}><i style={{fontSize: `20px`}} className="material-icons" title={__(`Add to the dashboard`)}>grid_on</i></div> - <div style={{float: `left`, paddingLeft: `10px`}}>{__(`Title`)}</div> - </th> - <th style={{textAlign: `center`}}>{__(`Datatype`)}</th> - <th style={{textAlign: `right`, paddingRight: `10px`}}> - <i style={{fontSize: `20px`}} className="material-icons" title={__(`Delete`)}>delete</i> - </th> - </tr> - </thead> - <tbody> - {plotRows} - </tbody> - </table>); - } - - if (projectProfileRows.length > 0) { - projectProfilesControls = (<table className="table table-striped"> - <thead style={{color: `rgb(0, 150, 136)`}}> - <tr> - <th> - <div style={{float: `left`}}><i style={{fontSize: `20px`}} className="material-icons" title={__(`Add to the dashboard`)}>grid_on</i></div> - <div style={{float: `left`, paddingLeft: `10px`}}>{__(`Title`)}</div> - </th> - <th style={{textAlign: `center`}}>{__(`Datatype`)}</th> - </tr> - </thead> - <tbody> - {projectProfileRows} - </tbody> - </table>) - } - - let chemicalName = __(`Not selected`); - if (this.state.localSelectedChemical) { - chemicalName = utils.getChemicalName(this.state.localSelectedChemical, this.props.categories); - } - - let renderText = ''; - if (this.state.authenticated) { - renderText = (<div id="profile-drawing-buffer" style={{position: `relative`}}> - {overlay} - <div style={{borderBottom: `1px solid lightgray`}}> - <div style={{fontSize: `20px`, padding: `14px`}}> - <a href="javascript:void(0)" onClick={() => { - this.setState({showDrawingForm: !this.state.showDrawingForm}) - }}>{__(`Create new profile`)} - {this.state.showDrawingForm ? (<i className="material-icons">keyboard_arrow_down</i>) : (<i className="material-icons">keyboard_arrow_right</i>)} - </a> - </div> - {this.state.showDrawingForm ? (<div className="container"> - <div className="row"> - <div className="col-md-12"> - <TitleFieldComponent - onAdd={this.onNewProfileAdd} - type="browserOwned" - showIcon={false} - inputPlaceholder={this.state.newTitle} - disabled={this.state.step !== STEP_ENTER_NAME} - saveButtonText={__(`Continue`)} - customStyle={{width: `100%`}}/> - </div> - </div> - - {this.state.step !== STEP_ENTER_NAME ? (<div> - <div className="row"> - <div className="col-md-12"> - <p><strong>{__(`Select datatype`)}:</strong> {chemicalName} - <button - type="button" - disabled={this.state.step !== STEP_NOT_READY} - className="btn btn-primary btn-sm" - onClick={() => { - const selectChemicalModalPlaceholderId = `${SELECT_CHEMICAL_DIALOG_PREFIX}-placeholder`; - - if ($(`#${selectChemicalModalPlaceholderId}`).children().length > 0) { - ReactDOM.unmountComponentAtNode(document.getElementById(selectChemicalModalPlaceholderId)); - } - - try { - ReactDOM.render(<div> - <Provider store={reduxStore}> - <ChemicalSelectorModal - emptyOptionTitle={__(`Show without data type`)} - useLocalSelectedChemical={true} - localSelectedChemical={this.state.selectedChemical} - onClickControl={(selectorValue) => { - this.setState({localSelectedChemical: selectorValue}) - $('#' + SELECT_CHEMICAL_DIALOG_PREFIX).modal('hide'); - }}/> - </Provider> - </div>, document.getElementById(selectChemicalModalPlaceholderId)); - } catch (e) { - console.error(e); - } - - $('#' + SELECT_CHEMICAL_DIALOG_PREFIX).modal({backdrop: `static`}); - }}><i className="fas fa-edit" title={__(`Edit`)}></i></button> - <button - type="button" - disabled={this.state.localSelectedChemical === false} - className="btn btn-xs btn-primary" - title={__(`Delete`)} - onClick={(event) => { - this.setState({localSelectedChemical: false}); - }}> - <i className="fas fa-eraser" title={__(`Delete`)}></i> - </button> - </p> - </div> - </div> - - <div className="row"> - <div className="col-md-4" style={{paddingTop: `12px`}}> - <p><strong>{__(`Adjust buffer`)}</strong></p> - </div> - <div className="col-md-5" style={{paddingTop: `14px`}}> - <Slider - disabled={this.state.step !== STEP_NOT_READY} - value={this.state.buffer ? parseInt(this.state.buffer) : 0} - min={0} - max={500} - onChange={(value) => { - this.setState({buffer: value}); - }}/> - </div> - <div className="col-md-3"> - <input - disabled={this.state.step !== STEP_NOT_READY} - type="number" - className="form-control" - onChange={(event) => { - this.setState({buffer: event.target.value}); - }} - value={this.state.buffer}/> - </div> - </div> - - <div className="row"> - <div className="col-md-4" style={{paddingTop: `12px`}}> - <p><strong>{__(`Adjust profile bottom`)}</strong></p> - </div> - <div className="col-md-8"> - <input - disabled={this.state.step !== STEP_NOT_READY} - type="number" - className="form-control" - onChange={(event) => { - this.setState({profileBottom: event.target.value}); - }} - value={this.state.profileBottom}/> - </div> - </div> - - <div className="row"> - <div className="col-md-6" style={{textAlign: `center`}}> - {this.state.step === STEP_READY_TO_LOAD || this.state.step === STEP_BEING_DRAWN ? (<a - href="javascript:void(0)" - className="btn btn-primary" - onClick={() => { - this.setState({ - step: STEP_NOT_READY, - bufferedProfile: false - }, () => { - this.stopDrawing(); - }); - }}><i className="material-icons">block</i> {__(`Cancel`)}</a>) : (<a - href="javascript:void(0)" - className="btn btn-primary" - onClick={() => { - this.setState({step: STEP_BEING_DRAWN}, () => { - this.startDrawing(); - }); - }}><i className="material-icons">linear_scale</i> {__(`Draw profile`)}</a>)} - </div> - <div className="col-md-6" style={{textAlign: `center`}}> - <a - href="javascript:void(0)" - className="btn" - disabled={this.state.step !== STEP_READY_TO_LOAD} - onClick={() => { - this.search(); - }}>{__(`Continue`)}</a> - </div> - </div> - - {this.state.step === STEP_CHOOSE_LAYERS ? (<div> - <div className="row"> - <div className="col-md-12"> - {availableLayers} - </div> - </div> - <div className="row"> - <div className="col-md-12"> - <button - type="button" - className="btn btn-raised btn-block btn-primary btn-sm" - onClick={() => { - this.saveProfile(); - }}>{__(`Save and exit`)}</button> - </div> - </div> - </div>) : false} - </div>) : false} - </div>) : false} - </div> - <div className="container"> - <div className="row"> - <div className="col-md-6"> - <SearchFieldComponent id="measurements-search-control" onSearch={(profilesSearchTerm) => { - this.setState({ profilesSearchTerm }); - }}/> - </div> - </div> - - </div> - - <div style={{borderBottom: `1px solid lightgray`}}> - <div style={{fontSize: `20px`, padding: `14px`}}> - <a href="javascript:void(0)" onClick={() => { - this.setState({showExistingProfiles: !this.state.showExistingProfiles}) - }}>{__(`Select previously created profile`)} ({this.getProfilesLength()}) - {this.state.showExistingProfiles ? (<i className="material-icons">keyboard_arrow_down</i>) : (<i className="material-icons">keyboard_arrow_right</i>)} - </a> - </div> - {this.state.showExistingProfiles ? (<div className="container"> - <div className="row"> - <div className="col-md-12"> - {existingProfilesControls} - </div> - </div> - </div>) : false} - </div> - </div>); - } else { - renderText = (<div id="profile-drawing-buffer" style={{position: `relative`}}> - <div style={{textAlign: `center`}}> - <p>{__(`Please sign in to create / edit Profiles`)}</p> - </div> - </div>); - } - - if (projectProfileRows.length > 0) { - renderText = <div> - {renderText} - <div style={{borderBottom: `1px solid lightgray`}}> - <div style={{fontSize: `20px`, padding: `14px`}}> - <a href="javascript:void(0)" onClick={() => { - this.setState({showProjectProfiles: !this.state.showProjectProfiles}) - }}>{__(`Select Profiles from Project`)} ({this.getProjectProfilesLength()}) - {this.state.showProjectProfiles ? (<i className="material-icons">keyboard_arrow_down</i>) : (<i className="material-icons">keyboard_arrow_right</i>)} - </a> - </div> - {this.state.showProjectProfiles ? (<div className="container"> - <div className="row"> - <div className="col-md-12"> - {projectProfilesControls} - </div> - </div> - </div>) : false} - </div> - - </div> - } - return renderText; - } -} - -MenuProfilesComponent.propTypes = { - cloud: PropTypes.any.isRequired, - backboneEvents: PropTypes.any.isRequired, -}; - - -const mapStateToProps = state => ({ - selectedChemical: state.global.selectedChemical, - authenticated: state.global.authenticated, -}); - -const mapDispatchToProps = dispatch => ({ - selectChemical: (key) => dispatch(selectChemical(key)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(MenuProfilesComponent); +import React from "react"; +import PropTypes from "prop-types"; +import axios from "axios"; +import Slider from "rc-slider"; +import { Provider } from "react-redux"; +import { connect } from "react-redux"; + +import { + SELECT_CHEMICAL_DIALOG_PREFIX, + FREE_PLAN_MAX_PROFILES_COUNT, +} from "./../constants"; +import TitleFieldComponent from "./../../../../browser/modules/shared/TitleFieldComponent"; +import LoadingOverlay from "./../../../../browser/modules/shared/LoadingOverlay"; +import SearchFieldComponent from "./../../../../browser/modules/shared/SearchFieldComponent"; + +import reduxStore from "./../redux/store"; + +import { selectChemical } from "./../redux/actions"; +import ChemicalSelectorModal from "./dataselector/ChemicalSelectorModal"; +import ThemeProvider from "../themes/ThemeProvider"; +import { showSubscriptionIfFree } from "../helpers/show_subscriptionDialogue"; + +const utils = require("./../utils"); + +const wkt = require("terraformer-wkt-parser"); +const utmZone = require("./../../../../browser/modules/utmZone"); +const session = require("./../../../session/browser/index"); + +const STEP_ENTER_NAME = -1; +const STEP_NOT_READY = 0; +const STEP_BEING_DRAWN = 1; +const STEP_READY_TO_LOAD = 2; +const STEP_CHOOSE_LAYERS = 3; + +const DEFAULT_API_URL = `/api/key-value`; + +let drawnItems = new L.FeatureGroup(), + displayedItems = new L.FeatureGroup(), + embedDrawControl = false; + +/** + * Component for creating profiles + */ +class MenuProfilesComponent extends React.Component { + constructor(props) { + super(props); + + this.state = { + apiUrl: props.apiUrl ? props.apiUrl : DEFAULT_API_URL, + loading: false, + localSelectedChemical: false, + showDrawingForm: true, + showExistingProfiles: true, + showProjectProfiles: true, + boreholeNames: [], + layers: [], + selectedLayers: [], + authenticated: props.authenticated ? props.authenticated : false, + profiles: props.initialProfiles ? props.initialProfiles : [], + activeProfiles: props.initialActiveProfiles + ? props.initialActiveProfiles + : [], + profile: false, + step: STEP_ENTER_NAME, + bufferedProfile: false, + profileBottom: -100, + buffer: 100, + newTitle: "", + profilesSearchTerm: "", + }; + + this.search = this.search.bind(this); + this.startDrawing = this.startDrawing.bind(this); + this.stopDrawing = this.stopDrawing.bind(this); + this.saveProfile = this.saveProfile.bind(this); + this.handleLayerSelect = this.handleLayerSelect.bind(this); + + props.cloud.get().map.addLayer(drawnItems); + props.cloud.get().map.addLayer(displayedItems); + + this.bufferSliderRef = React.createRef(); + this.bufferValueRef = React.createRef(); + this.onNewProfileAdd = this.onNewProfileAdd.bind(this); + this.canCreateProfile = this.canCreateProfile.bind(this); + + window.menuProfilesComponentInstance = this; + } + + componentDidMount() { + let _self = this; + this.props.backboneEvents + .get() + .on(`session:authChange`, (authenticated) => { + if (_self.state.authenticated !== authenticated) { + _self.setState({ authenticated }); + } + }); + + this.displayActiveProfiles(); + } + + canCreateProfile() { + if (session.getProperties()?.["license"] === "premium") { + return true; + } else { + return this.getProfilesLength() < FREE_PLAN_MAX_PROFILES_COUNT; + } + } + + displayActiveProfiles() { + displayedItems.eachLayer((layer) => { + displayedItems.removeLayer(layer); + }); + + if (this.state.activeProfiles) { + this.state.activeProfiles.map((activeProfileKey) => { + this.state.profiles.map((profile) => { + if (profile.key === activeProfileKey) { + this.displayProfile(profile); + } + }); + }); + } + } + + setProfiles(profiles) { + this.setState({ profiles }); + } + + setActiveProfiles(activeProfiles) { + this.setState({ activeProfiles }); + } + + onNewProfileAdd(newTitle) { + if (!this.canCreateProfile()) { + showSubscription(); + return; + } + this.setState({ newTitle, step: STEP_NOT_READY }); + } + + getProjectProfilesLength() { + let count = 0; + this.state.profiles.map((item) => { + if (item.fromProject) { + count += 1; + } + }); + return count; + } + + getProfilesLength() { + let count = 0; + this.state.profiles.map((item) => { + if (!item.fromProject) { + count += 1; + } + }); + return count; + } + + saveProfile() { + let layers = []; + this.state.layers.map((item) => { + if (this.state.selectedLayers.indexOf(item.id) > -1) { + layers.push(item); + } + }); + + this.setState({ loading: true }); + this.props.onProfileCreate( + { + title: this.state.newTitle, + profile: this.state.profile, + buffer: this.state.buffer, + depth: this.state.profileBottom, + compound: this.state.localSelectedChemical, + boreholeNames: this.state.boreholeNames, + layers, + }, + true, + () => { + this.setState({ + step: STEP_ENTER_NAME, + bufferedProfile: false, + profileBottom: -100, + buffer: 100, + newTitle: "", + loading: false, + }); + } + ); + } + + handleProfileDelete(item) { + if (confirm(__(`Delete`) + " " + item.profile.title + "?")) { + this.setState({ loading: true }); + this.props.onProfileDelete(item.key, () => { + this.setState({ loading: false }); + }); + } + } + + handleLayerSelect(checked, layer) { + let layesrCopy = JSON.parse(JSON.stringify(this.state.selectedLayers)); + if (checked) { + if (layesrCopy.indexOf(layer.id) === -1) { + layesrCopy.push(layer.id); + } + } else { + if (layesrCopy.indexOf(layer.id) > -1) { + layesrCopy.splice(layesrCopy.indexOf(layer.id), 1); + } + } + + this.setState({ selectedLayers: layesrCopy }); + } + + search() { + this.setState( + { + step: STEP_NOT_READY, + layers: [], + selectedLayers: [], + }, + () => { + this.stopDrawing(); + this.setState({ loading: true }); + axios + .post(`/api/extension/watsonc/intersection`, { + data: wkt.convert(this.state.bufferedProfile), + bufferRadius: this.state.buffer, + profileDepth: this.state.profileBottom, + profile: this.state.profile, + }) + .then((response) => { + let responseCopy = JSON.parse(JSON.stringify(response.data.result)); + response.data.result.map((item, index) => { + responseCopy[index].id = btoa(item.title); + }); + + this.setState({ + step: STEP_CHOOSE_LAYERS, + loading: false, + layers: responseCopy, + boreholeNames: response.data.boreholeNames, + }); + }) + .catch((error) => { + this.setState({ loading: false }); + console.log(`Error occured`, error); + }); + } + ); + } + + clearDrawnLayers() { + drawnItems.eachLayer((layer) => { + drawnItems.removeLayer(layer); + }); + } + + startDrawing() { + this.clearDrawnLayers(); + + if (embedDrawControl) embedDrawControl.disable(); + embedDrawControl = new L.Draw.Polyline(this.props.cloud.get().map); + embedDrawControl.enable(); + + embedDrawControl._map.off("draw:created"); + embedDrawControl._map.on("draw:created", (e) => { + if (embedDrawControl) embedDrawControl.disable(); + + let coord, + layer = e.layer; + + let primitive = layer.toGeoJSON(); + if (primitive) { + if (typeof layer.getBounds !== "undefined") { + coord = layer.getBounds().getSouthWest(); + } else { + coord = layer.getLatLng(); + } + + // Get utm zone + var zone = utmZone.getZone(coord.lat, coord.lng); + var crss = { + proj: + "+proj=utm +zone=" + + zone + + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs", + unproj: "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs", + }; + + var reader = new jsts.io.GeoJSONReader(); + var writer = new jsts.io.GeoJSONWriter(); + var geom = reader.read( + reproject.reproject(primitive, "unproj", "proj", crss) + ); + var buffer4326 = reproject.reproject( + writer.write(geom.geometry.buffer(this.state.buffer)), + "proj", + "unproj", + crss + ); + + L.geoJson(buffer4326, { + color: "#ff7800", + weight: 1, + opacity: 1, + fillOpacity: 0.1, + dashArray: "5,3", + }).addTo(drawnItems); + + this.setState({ + step: STEP_READY_TO_LOAD, + bufferedProfile: buffer4326, + profile: primitive, + }); + } + }); + } + + stopDrawing() { + if (drawnItems) drawnItems.clearLayers(); + if (embedDrawControl) embedDrawControl.disable(); + } + + displayProfile(data) { + this.clearDrawnLayers(); + let profile = data.profile.profile; + + // Get utm zone + var zone = utmZone.getZone( + profile.geometry.coordinates[0][1], + profile.geometry.coordinates[0][0] + ); + var crss = { + proj: + "+proj=utm +zone=" + + zone + + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs", + unproj: "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs", + }; + + let reader = new jsts.io.GeoJSONReader(); + let writer = new jsts.io.GeoJSONWriter(); + let geom = reader.read( + reproject.reproject(profile, "unproj", "proj", crss) + ); + let buffer4326 = reproject.reproject( + writer.write(geom.geometry.buffer(data.profile.buffer)), + "proj", + "unproj", + crss + ); + + L.geoJson(buffer4326, { + color: "#ff7800", + weight: 1, + opacity: 1, + fillOpacity: 0.1, + dashArray: "5,3", + }).addTo(displayedItems); + + var profileLayer = new L.geoJSON(profile); + + profileLayer.bindTooltip(data.profile.title, { + className: "watsonc-profile-tooltip", + permanent: true, + offset: [0, 0], + }); + + profileLayer.addTo(displayedItems); + } + + handleProfileToggle(checked, profileKey) { + if (checked) { + this.props.onProfileShow(profileKey); + } else { + this.props.onProfileHide(profileKey); + } + } + + addToDashboard(item) { + this.props.onProfileAdd(item); + } + + render() { + let overlay = false; + if (this.state.loading) { + overlay = <LoadingOverlay />; + } + + let availableLayers = ( + <div style={{ textAlign: `center` }}> + <p>{__(`No layers found`)}</p> + </div> + ); + + if (this.state.layers && this.state.layers.length > 0) { + availableLayers = []; + + const generateLayerRecord = (item, index, prefix) => { + let points = []; + item.intersectionSegments.map((item) => { + points.push( + `${Math.round(item[0] / 1000)}km - ${Math.round(item[1] / 1000)}km` + ); + }); + + return ( + <div className="form-group" key={`${prefix}${index}`}> + <div className="checkbox"> + <label> + <input + type="checkbox" + checked={this.state.selectedLayers.indexOf(item.id) > -1} + onChange={(event) => { + this.handleLayerSelect(event.target.checked, item); + }} + />{" "} + {item.title} + </label> + </div> + <div> + {item.subtitle} + <br /> + {__(`Stationing points`) + ": " + points.join(", ")} + </div> + </div> + ); + }; + + this.state.layers + .filter((item) => item.type !== `geology`) + .map((item, index) => { + availableLayers.push( + generateLayerRecord(item, index, `non_geology_layer_`) + ); + }); + if (availableLayers.length > 0) + availableLayers.push( + <hr style={{ margin: `10px` }} key={`layer_divider`} /> + ); + this.state.layers + .filter((item) => item.type === `geology`) + .map((item, index) => { + availableLayers.push( + generateLayerRecord(item, index, `geology_layer_`) + ); + }); + } + + let existingProfilesControls = ( + <div style={{ textAlign: `center` }}> + <p>{__(`Ingen profiler fundet`)}</p> + </div> + ); + let projectProfilesControls = ( + <div style={{ textAlign: `center` }}> + <p>{__(`Ingen profiler fundet`)}</p> + </div> + ); + + let plotRows = []; + let projectProfileRows = []; + this.state.profiles.map((item, index) => { + if (this.state.profilesSearchTerm.length > 0) { + if ( + item.profile.title + .toLowerCase() + .indexOf(this.state.profilesSearchTerm.toLowerCase()) === -1 + ) { + return; + } + } + var deleteButton = item.fromProject ? null : ( + <td style={{ textAlign: `right` }}> + <button + type="button" + className="btn btn-xs btn-primary" + title={__(`Delete profile`)} + onClick={(event) => { + this.handleProfileDelete(item); + }} + style={{ padding: `4px`, margin: `0px` }} + > + <i className="material-icons">delete</i> + </button> + </td> + ); + var itemHtml = ( + <tr key={`existing_profile_${index}`}> + <td> + <div> + <div style={{ float: `left` }}> + <button + type="button" + name="enabled_profile" + className="btn btn-xs btn-primary" + title="Tilføj profil" + // checked={this.state.activeProfiles.indexOf(item.key) > -1} + onClick={() => this.addToDashboard(item)} + // onChange={(event) => { + // this.handleProfileToggle(event.target.checked, item.key); + // }} + style={{ padding: `0px`, margin: `0px` }} + > + <i className="material-icons">add</i> + </button> + </div> + <div + style={{ float: `left`, paddingLeft: `8px`, paddingTop: `2px` }} + > + {item.profile.title} + </div> + </div> + </td> + <td style={{ textAlign: `center` }}> + {item.profile.compound + ? utils.getChemicalName( + item.profile.compound, + this.props.categories + ) + : "Ikke valgt"} + </td> + {deleteButton} + </tr> + ); + if (item.fromProject === true) { + projectProfileRows.push(itemHtml); + } else { + plotRows.push(itemHtml); + } + }); + + if (plotRows.length > 0) { + existingProfilesControls = ( + <table className="table table-striped"> + <thead style={{ color: `rgb(0, 150, 136)` }}> + <tr> + <th> + <div style={{ float: `left` }}> + <i + style={{ fontSize: `20px` }} + className="material-icons" + title={__(`Add to the dashboard`)} + > + grid_on + </i> + </div> + <div style={{ float: `left`, paddingLeft: `10px` }}> + {__(`Title`)} + </div> + </th> + <th style={{ textAlign: `center` }}>{__(`Datatype`)}</th> + <th style={{ textAlign: `right`, paddingRight: `10px` }}> + <i + style={{ fontSize: `20px` }} + className="material-icons" + title={__(`Delete`)} + > + delete + </i> + </th> + </tr> + </thead> + <tbody>{plotRows}</tbody> + </table> + ); + } + + if (projectProfileRows.length > 0) { + projectProfilesControls = ( + <table className="table table-striped"> + <thead style={{ color: `rgb(0, 150, 136)` }}> + <tr> + <th> + <div style={{ float: `left` }}> + <i + style={{ fontSize: `20px` }} + className="material-icons" + title={__(`Add to the dashboard`)} + > + grid_on + </i> + </div> + <div style={{ float: `left`, paddingLeft: `10px` }}> + {__(`Title`)} + </div> + </th> + <th style={{ textAlign: `center` }}>{__(`Datatype`)}</th> + </tr> + </thead> + <tbody>{projectProfileRows}</tbody> + </table> + ); + } + + let chemicalName = "Ikke valgt"; + if (this.state.localSelectedChemical) { + chemicalName = utils.getChemicalName( + this.state.localSelectedChemical, + this.props.categories + ); + } + + let renderText = ""; + if (this.state.authenticated) { + renderText = ( + <div id="profile-drawing-buffer" style={{ position: `relative` }}> + {overlay} + <div style={{ borderBottom: `1px solid lightgray` }}> + <div style={{ fontSize: `20px`, padding: `14px` }}> + <a + href="javascript:void(0)" + onClick={() => { + this.setState({ + showDrawingForm: !this.state.showDrawingForm, + }); + }} + > + {__(`Create new profile`)} + {this.state.showDrawingForm ? ( + <i className="material-icons">keyboard_arrow_down</i> + ) : ( + <i className="material-icons">keyboard_arrow_right</i> + )} + </a> + </div> + {this.state.showDrawingForm ? ( + <div className="container"> + <div className="row"> + <div className="col-md-12"> + <TitleFieldComponent + onAdd={this.onNewProfileAdd} + type="browserOwned" + showIcon={false} + inputPlaceholder={this.state.newTitle} + disabled={this.state.step !== STEP_ENTER_NAME} + saveButtonText={__(`Continue`)} + customStyle={{ width: `100%` }} + /> + </div> + </div> + + {this.state.step !== STEP_ENTER_NAME ? ( + <div> + <div className="row"> + <div className="col-md-12"> + <p> + <strong>{__(`Select datatype`)}:</strong>{" "} + {chemicalName} + <button + type="button" + disabled={this.state.step !== STEP_NOT_READY} + className="btn btn-primary btn-sm" + onClick={() => { + const selectChemicalModalPlaceholderId = `${SELECT_CHEMICAL_DIALOG_PREFIX}-placeholder`; + + if ( + $( + `#${selectChemicalModalPlaceholderId}` + ).children().length > 0 + ) { + ReactDOM.unmountComponentAtNode( + document.getElementById( + selectChemicalModalPlaceholderId + ) + ); + } + + try { + ReactDOM.render( + <div> + <Provider store={reduxStore}> + <ThemeProvider> + <ChemicalSelectorModal + emptyOptionTitle={__( + `Show without data type` + )} + categories={this.props.categories} + useLocalSelectedChemical={true} + localSelectedChemical={ + this.state.selectedChemical + } + onClickControl={(selectorValue) => { + this.setState({ + localSelectedChemical: + selectorValue, + }); + $( + "#" + + SELECT_CHEMICAL_DIALOG_PREFIX + ).modal("hide"); + }} + /> + </ThemeProvider> + </Provider> + </div>, + document.getElementById( + selectChemicalModalPlaceholderId + ) + ); + } catch (e) { + console.error(e); + } + + $("#" + SELECT_CHEMICAL_DIALOG_PREFIX).modal({ + backdrop: `static`, + }); + }} + > + <i className="fas fa-edit" title={__(`Edit`)}></i> + </button> + <button + type="button" + disabled={ + this.state.localSelectedChemical === false + } + className="btn btn-xs btn-primary" + title={__(`Delete`)} + onClick={(event) => { + this.setState({ localSelectedChemical: false }); + }} + > + <i + className="fas fa-eraser" + title={__(`Delete`)} + ></i> + </button> + </p> + </div> + </div> + + <div className="row"> + <div className="col-md-4" style={{ paddingTop: `12px` }}> + <p> + <strong>{__(`Adjust buffer`)}</strong> + </p> + </div> + <div className="col-md-5" style={{ paddingTop: `14px` }}> + <Slider + disabled={this.state.step !== STEP_NOT_READY} + value={ + this.state.buffer ? parseInt(this.state.buffer) : 0 + } + min={0} + max={500} + onChange={(value) => { + this.setState({ buffer: value }); + }} + /> + </div> + <div className="col-md-3"> + <input + disabled={this.state.step !== STEP_NOT_READY} + type="number" + className="form-control" + onChange={(event) => { + this.setState({ buffer: event.target.value }); + }} + value={this.state.buffer} + /> + </div> + </div> + + <div className="row"> + <div className="col-md-4" style={{ paddingTop: `12px` }}> + <p> + <strong>{__(`Adjust profile bottom`)}</strong> + </p> + </div> + <div className="col-md-8"> + <input + disabled={this.state.step !== STEP_NOT_READY} + type="number" + className="form-control" + onChange={(event) => { + this.setState({ + profileBottom: event.target.value, + }); + }} + value={this.state.profileBottom} + /> + </div> + </div> + + <div className="row"> + <div className="col-md-6" style={{ textAlign: `center` }}> + {this.state.step === STEP_READY_TO_LOAD || + this.state.step === STEP_BEING_DRAWN ? ( + <a + href="javascript:void(0)" + className="btn btn-primary" + onClick={() => { + this.setState( + { + step: STEP_NOT_READY, + bufferedProfile: false, + }, + () => { + this.stopDrawing(); + } + ); + }} + > + <i className="material-icons">block</i>{" "} + {__(`Cancel`)} + </a> + ) : ( + <a + href="javascript:void(0)" + className="btn btn-primary" + onClick={() => { + this.setState({ step: STEP_BEING_DRAWN }, () => { + this.startDrawing(); + }); + }} + > + <i className="material-icons">linear_scale</i>{" "} + {__(`Draw profile`)} + </a> + )} + </div> + <div className="col-md-6" style={{ textAlign: `center` }}> + <a + href="javascript:void(0)" + className="btn" + disabled={this.state.step !== STEP_READY_TO_LOAD} + onClick={() => { + this.search(); + }} + > + {__(`Continue`)} + </a> + </div> + </div> + + {this.state.step === STEP_CHOOSE_LAYERS ? ( + <div> + <div className="row"> + <div className="col-md-12">{availableLayers}</div> + </div> + <div className="row"> + <div className="col-md-12"> + <button + type="button" + className="btn btn-raised btn-block btn-primary btn-sm" + onClick={() => { + this.saveProfile(); + }} + > + {__(`Save and exit`)} + </button> + </div> + </div> + </div> + ) : ( + false + )} + </div> + ) : ( + false + )} + </div> + ) : ( + false + )} + </div> + <div className="container"> + <div className="row"> + <div className="col-md-6"> + <SearchFieldComponent + id="measurements-search-control" + onSearch={(profilesSearchTerm) => { + this.setState({ profilesSearchTerm }); + }} + /> + </div> + </div> + </div> + + <div style={{ borderBottom: `1px solid lightgray` }}> + <div style={{ fontSize: `20px`, padding: `14px` }}> + <a + href="javascript:void(0)" + onClick={() => { + this.setState({ + showExistingProfiles: !this.state.showExistingProfiles, + }); + }} + > + {__(`Select previously created profile`)} ( + {this.getProfilesLength()}) + {this.state.showExistingProfiles ? ( + <i className="material-icons">keyboard_arrow_down</i> + ) : ( + <i className="material-icons">keyboard_arrow_right</i> + )} + </a> + </div> + {this.state.showExistingProfiles ? ( + <div className="container"> + <div className="row"> + <div className="col-md-12">{existingProfilesControls}</div> + </div> + </div> + ) : ( + false + )} + </div> + </div> + ); + } else { + renderText = ( + <div id="profile-drawing-buffer" style={{ position: `relative` }}> + <div style={{ textAlign: `center` }}> + <p>{__(`Log ind for at se og rette i profiler`)}</p> + </div> + </div> + ); + } + + if (projectProfileRows.length > 0) { + renderText = ( + <div> + {renderText} + <div style={{ borderBottom: `1px solid lightgray` }}> + <div style={{ fontSize: `20px`, padding: `14px` }}> + <a + href="javascript:void(0)" + onClick={() => { + this.setState({ + showProjectProfiles: !this.state.showProjectProfiles, + }); + }} + > + {__(`Select Profiles from Project`)} ( + {this.getProjectProfilesLength()}) + {this.state.showProjectProfiles ? ( + <i className="material-icons">keyboard_arrow_down</i> + ) : ( + <i className="material-icons">keyboard_arrow_right</i> + )} + </a> + </div> + {this.state.showProjectProfiles ? ( + <div className="container"> + <div className="row"> + <div className="col-md-12">{projectProfilesControls}</div> + </div> + </div> + ) : ( + false + )} + </div> + </div> + ); + } + return renderText; + } +} + +MenuProfilesComponent.propTypes = { + cloud: PropTypes.any.isRequired, + backboneEvents: PropTypes.any.isRequired, +}; + +const mapStateToProps = (state) => ({ + selectedChemical: state.global.selectedChemical, + authenticated: state.global.authenticated, +}); + +const mapDispatchToProps = (dispatch) => ({ + selectChemical: (key) => dispatch(selectChemical(key)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MenuProfilesComponent); diff --git a/browser/components/MenuTimeSeriesComponent.js b/browser/components/MenuTimeSeriesComponent.js deleted file mode 100644 index 7935f7e..0000000 --- a/browser/components/MenuTimeSeriesComponent.js +++ /dev/null @@ -1,240 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Switch from '@material-ui/core/Switch'; -import {Provider, connect} from 'react-redux'; - -import PlotComponent from './PlotComponent'; -import {isNumber} from 'util'; -import {FREE_PLAN_MAX_PROFILES_COUNT} from './../constants'; -import TitleFieldComponent from './../../../../browser/modules/shared/TitleFieldComponent'; -import SearchFieldComponent from './../../../../browser/modules/shared/SearchFieldComponent'; - -/** - * Component for managing time series - */ -class MenuTimeSeriesComponent extends React.Component { - constructor(props) { - super(props); - - this.state = { - plots: this.props.initialPlots, - activePlots: this.props.initialActivePlots, - highlightedPlot: false, - showArchivedPlots: false, - authenticated: props.authenticated ? props.authenticated : false, - plotsSearchTerm: '', - }; - this.getPlots = this.getPlots.bind(this); - this.setShowArchivedPlots = this.setShowArchivedPlots.bind(this); - this.onPlotAdd = this.onPlotAdd.bind(this); - window.menuTimeSeriesComponentInstance = this; - } - - componentDidMount() { - let _self = this; - this.props.backboneEvents.get().on(`session:authChange`, (authenticated) => { - if (_self.state.authenticated !== authenticated) { - _self.setState({authenticated}); - } - }); - } - - setPlots(plots) { - this.setState({plots}); - } - - setActivePlots(activePlots) { - this.setState({activePlots}); - } - - setHighlightedPlot(highlightedPlot) { - this.setState({highlightedPlot}) - } - - setShowArchivedPlots(showArchivedPlots) { - this.setState({showArchivedPlots}); - } - - getPlots() { - if (this.state.showArchivedPlots) { - return this.state.plots; - } else { - return this.state.plots.filter((plot) => plot.isArchived != true); - } - } - - canCreatePlot() { - if (this.props.license === 'premium') { - return true; - } else { - let plots = this.state.plots.filter((plot) => plot.fromProject != true); - return plots.length < FREE_PLAN_MAX_TIME_SERIES_COUNT; - } - } - - onPlotAdd(title) { - if (!this.canCreatePlot()) { - $('#watsonc-limits-reached-text').show(); - $('#upgrade-modal').modal('show'); - return; - } - - this.props.onPlotCreate(title); - } - - render() { - let plotsTable = []; - let projectPlotsTable = []; - this.getPlots().map((plot, index) => { - if (this.state.plotsSearchTerm.length > 0) { - if (plot.title.toLowerCase().indexOf(this.state.plotsSearchTerm.toLowerCase()) === -1) { - return; - } - } - let isChecked = (this.state.activePlots.indexOf(plot.id) > -1); - let isHighlighted = (this.state.highlightedPlot === plot.id); - let highlightingIsDisabled = (isChecked ? false : true); - let archiveButton = plot.isArchived ? - <button type="button" className="btn btn-raised btn-xs" style={{padding: `4px`, margin: `0px`}} - onClick={() => { - this.props.onPlotArchive(plot.id, false) - }}> - <i className="material-icons">unarchive</i> - </button> : - <button type="button" className="btn btn-raised btn-xs" style={{padding: `4px`, margin: `0px`}} - onClick={() => { - this.props.onPlotArchive(plot.id, true) - }}> - <i className="material-icons">archive</i> - </button>; - if (plot.fromProject) { - archiveButton = null; - } - - let deleteButton = plot.fromProject ? null : - <button type="button" className="btn btn-raised btn-xs" onClick={() => { - this.props.onPlotDelete(plot.id, plot.title); - }} style={{padding: `4px`, margin: `0px`}}> - <i className="material-icons">delete</i> - </button>; - let itemHtml = <tr key={`borehole_plot_control_${index}`}> - <td> - <div className="form-group"> - <div className="checkbox"> - <label> - <input type="checkbox" checked={isChecked} onChange={(event) => { - event.target.checked ? this.props.onPlotShow(plot.id) : this.props.onPlotHide(plot.id) - }}/> - </label> - </div> - </div> - </td> - <td>{plot.title}</td> - <td> - <div className="form-group"> - {archiveButton} - </div> - </td> - <td> - {deleteButton} - </td> - </tr>; - - if (plot.fromProject === true) { - projectPlotsTable.push(itemHtml) - } - plotsTable.push(itemHtml); - - }); - - var showArchivedPlotsButton = <div> - Show Archived - <Switch checked={this.state.showArchivedPlots} onChange={() => { - this.setShowArchivedPlots(!this.state.showArchivedPlots) - }}/> - </div>; - if (Array.isArray(plotsTable) && plotsTable.length > 0) { - plotsTable = (<table className="table table-striped table-hover"> - <thead> - <tr style={{color: `rgb(0, 150, 136)`}}> - <td style={{width: `40px`}}><i className="material-icons">border_all</i></td> - <td style={{width: `70%`}}>{__(`Title`)}</td> - <td><i className="fas fa-map-marked-alt fas-material-adapt"></i></td> - <td><i className="material-icons">delete</i></td> - </tr> - </thead> - <tbody>{plotsTable}</tbody> - </table>); - } else if (projectPlotsTable.length === 0) { - plotsTable = (<p>{__(`No time series were created yet`)}</p>); - } else { - plotsTable = null; - } - if (Array.isArray(projectPlotsTable) && projectPlotsTable.length > 0) { - projectPlotsTable = ( - <div> - <div style={{fontSize: `20px`, padding: `14px`}}> - {__('Select Time Series from Project')} - </div> - <table className="table table-striped table-hover"> - <thead> - <tr style={{color: `rgb(0, 150, 136)`}}> - <td style={{width: `40px`}}><i className="material-icons">border_all</i></td> - <td style={{width: `70%`}}>{__(`Title`)}</td> - </tr> - </thead> - <tbody>{projectPlotsTable}</tbody> - </table> - </div>); - } else { - projectPlotsTable = null; - } - - var addTimeSeriesComponent = this.state.authenticated ? <div> - <h4>{__(`Timeseries`)} - <TitleFieldComponent - saveButtonText={__(`Save`)} - layout="dense" - onAdd={this.onPlotAdd} type="userOwned"/> - </h4> - </div> : <div style={{position: `relative`}}> - <div style={{textAlign: `center`}}> - <p>{__(`Please sign in to create / edit Time series`)}</p> - </div> - </div>; - - return ( - <div> - {addTimeSeriesComponent} - <div style={{display: `flex`}}> - <SearchFieldComponent id="measurements-search-control" onSearch={(plotsSearchTerm) => { - this.setState({plotsSearchTerm}); - }}/> - - <div style={{ - textAlign: 'right', - marginRight: '30px', - marginLeft: 'auto' - }}>{showArchivedPlotsButton}</div> - </div> - <div>{plotsTable}</div> - <div> - {projectPlotsTable} - </div> - </div> - ); - } -} - -const mapStateToProps = state => ({ - authenticated: state.global.authenticated -}) - -const mapDispatchToProps = dispatch => ({}) - -MenuTimeSeriesComponent.propTypes = { - initialPlots: PropTypes.array.isRequired, - initialActivePlots: PropTypes.array.isRequired, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(MenuTimeSeriesComponent); diff --git a/browser/components/ModalComponent.js b/browser/components/ModalComponent.js deleted file mode 100644 index f7c1864..0000000 --- a/browser/components/ModalComponent.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {Provider} from 'react-redux'; -import reduxStore from '../redux/store'; - -import withDragDropContext from './withDragDropContext'; -import ModalFeatureComponent from './ModalFeatureComponent'; - -/** - * Creates borehole parameters display and visualization panel - */ -class ModalComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - activeTabIndex: 0 - } - } - - setPlots(plots) { - this.setState({ plots }); - } - - render() { - let tabs = false; - if (this.props.features.length > 0) { - let tabControls = []; - this.props.features.map((item, index) => { - let name; - if (typeof item.properties.alias !== "undefined") { - try { - name = item.properties.alias; - } catch (e) { - name = item.properties.boreholeno; - } - } else { - name = item.properties.boreholeno; - } - tabControls.push(<li key={`modal_tab_${index}`} className={index === this.state.activeTabIndex ? `active` : ``}> - <a href="javascript:void(0)" onClick={() => { this.setState({activeTabIndex: index})}}>{name}</a> - </li>); - }); - - tabs = (<ul className="nav nav-tabs watsonc-modal-tabs" style={{marginBottom: `15px`}}>{tabControls}</ul>) - } - - return (<div style={{ height: `inherit` }}> - {tabs} - <div style={{ height: (tabs === false ? `inherit` : `calc(100% - 39px)`)}}> - <Provider store={reduxStore}> - <ModalFeatureComponent - key={`item_${this.state.activeTabIndex}`} - feature={this.props.features[this.state.activeTabIndex]} - {...this.props}/> - </Provider> - </div> - </div>); - } -} - -ModalComponent.propTypes = { - categories: PropTypes.object.isRequired, - features: PropTypes.array.isRequired, - names: PropTypes.object.isRequired, - limits: PropTypes.object.isRequired, - initialPlots: PropTypes.array.isRequired, - initialActivePlots: PropTypes.array.isRequired, - onPlotShow: PropTypes.func.isRequired, - onPlotHide: PropTypes.func.isRequired, - onPlotAdd: PropTypes.func.isRequired, - onAddMeasurement: PropTypes.func.isRequired, - onDeleteMeasurement: PropTypes.func.isRequired -}; - -export default withDragDropContext(ModalComponent); diff --git a/browser/components/ModalFeatureComponent.js b/browser/components/ModalFeatureComponent.js deleted file mode 100644 index 081e816..0000000 --- a/browser/components/ModalFeatureComponent.js +++ /dev/null @@ -1,458 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; -import Switch from '@material-ui/core/Switch'; - -import withDragDropContext from './withDragDropContext'; -import ModalMeasurementComponent from './ModalMeasurementComponent'; -import ModalPlotComponent from './ModalPlotComponent'; -import TitleFieldComponent from './../../../../browser/modules/shared/TitleFieldComponent'; -import SearchFieldComponent from './../../../../browser/modules/shared/SearchFieldComponent'; - -const evaluateMeasurement = require('./../evaluateMeasurement'); -const measurementIcon = require('./../measurementIcon'); - -/** - * Creates borehole parameters display and visualization panel - */ -class ModalFeatureComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - plots: this.props.initialPlots, - measurementsSearchTerm: ``, - plotsSearchTerm: ``, - activePlots: this.props.initialActivePlots, - } - this.listRef = React.createRef(); - this.onPlotAdd = this.onPlotAdd.bind(this); - this.handleHidePlot = this.handleHidePlot.bind(this); - this.handleShowPlot = this.handleShowPlot.bind(this); - this.hasSelectAll = this.hasSelectAll.bind(this); - this.setSelectAll = this.setSelectAll.bind(this); - } - - getSnapshotBeforeUpdate(prevProps, prevState) { - const list = this.listRef.current; - const scroll = list.scrollHeight - list.scrollTop; - this.props.setModalScroll(scroll); - } - - componentDidMount() { - let {selectedChemical} = this.props; - // Simulating the separate group for water level - let categories = JSON.parse(JSON.stringify(this.props.categories)); - let selectedCategory = null; - for (let category in categories) { - for (let itemId in categories[category]) { - if ((itemId + '') === (selectedChemical + '')) { - selectedCategory = category; - } - } - } - try { - let selectedCategoryKey = 'show' + selectedCategory.trim() + 'Measurements'; - this.setState({[selectedCategoryKey]: true}); - } catch (e) { - console.info(e.message); - // Hack to open group when Pesticid Overblik is chosen - this.setState({"showPesticider og nedbrydningsprodMeasurements": true}); - } - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (this.props.modalScroll) { - const list = this.listRef.current; - list.scrollTop = list.scrollHeight - this.props.modalScroll; - } - } - - hasSelectAll() { - let categories = JSON.parse(JSON.stringify(this.props.categories)); - let hasSelectAll = true; - for (let category in categories) { - if (!hasSelectAll) { - break; - } - let selectedCategoryKey = 'show' + category.trim() + 'Measurements' - let isCategorySelected = this.state[selectedCategoryKey]; - hasSelectAll = hasSelectAll && isCategorySelected; - } - return hasSelectAll; - } - - handleHidePlot(plot) { - let activePlots = this.state.activePlots.filter((activePlot) => { - return activePlot.id != plot.id; - }) - this.setState({activePlots}); - this.props.onPlotHide(plot.id); - } - - handleShowPlot(plot) { - let activePlots = this.state.activePlots; - activePlots.push(plot); - this.setState({activePlots}); - this.props.onPlotShow(plot.id); - } - - setSelectAll(selectAll) { - let categories = JSON.parse(JSON.stringify(this.props.categories)); - let stateUpdate = {} - for (let category in categories) { - let selectedCategoryKey = 'show' + category.trim() + 'Measurements'; - stateUpdate[selectedCategoryKey] = selectAll; - } - this.setState(stateUpdate); - } - - setPlots(plots) { - this.setState({plots}); - } - - canCreatePlot() { - if (this.props.license === 'premium') { - return true; - } else { - let plots = this.state.plots.filter((plot) => plot.fromProject !== true); - return plots.length < FREE_PLAN_MAX_TIME_SERIES_COUNT; - } - } - - onPlotAdd(title) { - if (!this.canCreatePlot()) { - $('#watsonc-limits-reached-text').show(); - $('#upgrade-modal').modal('show'); - return; - } - this.props.onPlotAdd(title); - - } - - render() { - - // Simulating the separate group for water level - let categories = JSON.parse(JSON.stringify(this.props.categories)); - categories[`Vandstand`] = {}; - categories[`Vandstand`][`watlevmsl`] = `Water level`; - // Detect measurements from feature properties - let plottedProperties = []; - for (let key in this.props.feature.properties) { - try { - let data = JSON.parse(this.props.feature.properties[key]); - if (typeof data === `object` && data !== null && `boreholeno` in data && `unit` in data && `title` in data - && `measurements` in data && `timeOfMeasurement` in data) { - // Regular properties ("measurements" and "timeOfMeasurement" exist) - let isPlottableProperty = true; - if (Array.isArray(data.measurements) === false) { - data.measurements = JSON.parse(data.measurements); - } - - // Checking if number of measurements corresponds to the number of time measurements for each intake - data.measurements.map((measurements, intakeIndex) => { - if (data.measurements[intakeIndex].length !== data.timeOfMeasurement[intakeIndex].length) { - console.warn(`${data.title} property has not corresponding number of measurements and time measurements for intake ${intakeIndex + 1}`); - isPlottableProperty = false; - } - }); - - if (isPlottableProperty) { - for (let i = 0; i < data.measurements.length; i++) { - plottedProperties.push({ - key, - intakeIndex: i, - boreholeno: data.boreholeno, - title: data.title, - unit: data.unit - }); - } - } - } else if (typeof data === `object` && data !== null && `title` in data && `data` in data) { - for (let key in data.data) { - for (let i = 0; i < data.data[key].data.length; i++) { - plottedProperties.push({ - custom: true, - key: data.key + ':' + key, - intakeIndex: i, - boreholeno: this.props.feature.properties.boreholeno ? this.props.feature.properties.boreholeno : ``, - title: data.data[key].data[i].name, - data: data.data[key] - }); - } - } - } - } catch (e) { - } - } - - // Preparing measurements - let measurementsText = __(`Data series`); - if (this.state.measurementsSearchTerm.length > 0) { - measurementsText = __(`Found data series`); - } - - /** - * Creates measurement control - * - * @returns {Boolean|Object} - */ - const createMeasurementControl = (item, key) => { - let display = true; - if (this.state.measurementsSearchTerm.length > 0) { - if (item.title.toLowerCase().indexOf(this.state.measurementsSearchTerm.toLowerCase()) === -1) { - display = false; - } - } - - let control = false; - if (display) { - let json; - // Checking if the item is the custom one - if (item.key.indexOf(':') > -1) { - json = item; - } else { - try { - json = JSON.parse(this.props.feature.properties[item.key]); - } catch (e) { - console.error(item); - throw new Error(`Unable to parse measurements data`); - } - } - - let intakeName = `#` + (parseInt(item.intakeIndex) + 1); - if (`intakes` in json && Array.isArray(json.intakes) && json.intakes[item.intakeIndex] !== null) { - intakeName = json.intakes[item.intakeIndex] + ''; - } - - let icon = false; - let measurementData = null; - if (!item.custom) { - measurementData = evaluateMeasurement(json, this.props.limits, item.key, item.intakeIndex); - icon = measurementIcon.generate(measurementData.maxColor, measurementData.latestColor); - } - - control = (<ModalMeasurementComponent - key={key} - icon={icon} - onAddMeasurement={this.props.onAddMeasurement} - maxMeasurement={measurementData === null ? null : Math.round((measurementData.maxMeasurement) * 100) / 100} - latestMeasurement={measurementData === null ? null : Math.round((measurementData.latestMeasurement) * 100) / 100} - latestMeasurementRelative={measurementData === null ? null : Math.round((measurementData.latestMeasurement / measurementData.chemicalLimits[1]) * 100) / 100} - chemicalLimits={measurementData === null ? null : measurementData.chemicalLimits} - detectionLimitReachedForMax={measurementData === null ? null : measurementData.detectionLimitReachedForMax} - detectionLimitReachedForLatest={measurementData === null ? null : measurementData.detectionLimitReachedForLatest} - gid={this.props.feature.properties.boreholeno} - itemKey={item.key} - intakeIndex={item.intakeIndex} - intakeName={intakeName} - unit={item.unit} - title={item.title}/>); - } - - return control; - }; - - - let propertiesControls = []; - if (Object.keys(categories).length > 0) { - let numberOfDisplayedCategories = 0; - for (let categoryName in categories) { - let measurementsThatBelongToCategory = Object.keys(categories[categoryName]).map(e => categories[categoryName][e]); - let measurementControls = []; - plottedProperties = plottedProperties.filter((item, index) => { - if (measurementsThatBelongToCategory.indexOf(item.title) !== -1) { - // Measurement is in current category - let control = createMeasurementControl(item, ('measurement_' + index)); - if (control) { - measurementControls.push(control); - } - - return false; - } else { - return true; - } - }); - if (measurementControls.length > 0) { - measurementControls.sort(function (a, b) { - return (b.props.detectionLimitReachedForLatest ? 0 : b.props.latestMeasurementRelative) - (a.props.detectionLimitReachedForLatest ? 0 : a.props.latestMeasurementRelative) - }) - let key = 'show' + categoryName.trim() + 'Measurements' - // Category has at least one displayed measurement - numberOfDisplayedCategories++; - propertiesControls.push(<div key={`category_` + numberOfDisplayedCategories}> - <div style={{fontSize: '20px'}}><a href="javascript:void(0)" onClick={() => { - this.setState({[key]: !this.state[key]}) - }}><h5>{categoryName.trim()}{this.state[key] ? ( - <i className="material-icons">keyboard_arrow_down</i>) : ( - <i className="material-icons">keyboard_arrow_right</i>)}</h5></a></div> - {this.state[key] ? (<div>{measurementControls}</div>) : false} - </div>); - } - } - - // Placing uncategorized measurements in separate category - let uncategorizedMeasurementControls = []; - plottedProperties.slice().map((item, index) => { - let control = createMeasurementControl(item, ('measurement_' + index)); - plottedProperties.splice(index, 1); - if (control) { - uncategorizedMeasurementControls.push(control); - } - }); - - if (uncategorizedMeasurementControls.length > 0) { - uncategorizedMeasurementControls.sort(function (a, b) { - return (b.props.detectionLimitReachedForLatest ? 0 : b.props.latestMeasurementRelative) - (a.props.detectionLimitReachedForLatest ? 0 : a.props.latestMeasurementRelative) - }) - // Category has at least one displayed measurement - numberOfDisplayedCategories++; - propertiesControls.push(<div key={`uncategorized_category_0`}> - <div> - <h5>{__(`Uncategorized`)}</h5> - </div> - <div>{uncategorizedMeasurementControls}</div> - </div>); - } - } else { - plottedProperties.map((item, index) => { - let control = createMeasurementControl(item, (`measurement_` + index)); - if (control) { - propertiesControls.push(control); - } - }); - } - - // Preparing plots - let plotsText = __(`Time series`); - if (this.state.plotsSearchTerm.length > 0) { - plotsText = __(`Found time series`); - } - - let plotsControls = (<div> - <p>{__(`No timeseries were created yet`)}</p> - <div style={{ - textAlign: `center`, - position: `absolute`, - top: `50%`, - color: `gray` - }}> - <p>{__(`Create a new table and then drag your desired data series into the box - and you're off`)}</p> - </div> - </div>); - - if (this.state.plots && this.state.plots.length > 0) { - plotsControls = []; - let activePlotIds = this.state.activePlots.map((plot) => { - return plot?.id; - }); - activePlotIds = activePlotIds.filter((id) => { - return !!id; - }) - this.state.plots.map((plot) => { - let display = true; - if (this.state.plotsSearchTerm.length > 0) { - if (plot.title.toLowerCase().indexOf(this.state.plotsSearchTerm.toLowerCase()) === -1) { - display = false; - } - } - - if (display) { - plotsControls.push(<ModalPlotComponent - key={`plot_container_` + plot.id} - plot={plot} - isActive={activePlotIds.indexOf(plot.id) > -1} - onPlotShow={this.handleShowPlot} - onPlotHide={this.handleHidePlot} - onDeleteMeasurement={this.props.onDeleteMeasurement} - dataSource={this.props.dataSource}/>); - } - }); - } - - let borproUrl; - try { - borproUrl = this.props.feature.properties.boreholeno.replace(/\s/g, ''); - } catch (e) { - borproUrl = ""; - } - - return (<div style={{height: `inherit`}}> - <div> - <div className="measurements-modal_left-column"> - <div style={{display: 'flex', height: '50px'}}> - <div style={{width: '30px', height: '30px', marginLeft: '25px'}}> - <a target="_blank" - href={`http://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${this.props.feature.properties.boreholeno}`}> - <img style={{width: '30px', height: '30px'}} - src="https://mapcentia-www.s3-eu-west-1.amazonaws.com/calypso/icons/geus.ico"/><br/> - <span style={{fontSize: '70%'}}>Jupiter</span> - </a> - </div> - <div style={{width: '30px', height: '30px', marginLeft: '30px'}}> - <a target="_blank" href={`http://borpro.dk/borejournal.asp?dguNr=${borproUrl}`}> - <img style={{width: '30px', height: '30px'}} - src="https://mapcentia-www.s3-eu-west-1.amazonaws.com/calypso/icons/borpro.ico"/><br/> - <span style={{fontSize: '70%'}}>Borpro</span> - </a> - </div> - <div style={{width: '80px', height: '30px', marginLeft: 'auto', marginRight: '60px'}}> - <Switch checked={this.hasSelectAll()} onChange={(name, isChecked) => { - this.setSelectAll(isChecked); - }}/> - Fold ind/ud - </div> - </div> - <div>{measurementsText}</div> - <div className="form-group"> - <SearchFieldComponent id="measurements-search-control" onSearch={(measurementsSearchTerm) => { - this.setState({measurementsSearchTerm}); - }}/> - </div> - </div> - <div className="measurements-modal_right-column"> - <div>{plotsText}</div> - <div style={{display: `flex`}}> - <div className="form-group"> - <SearchFieldComponent id="plots-search-control" onSearch={(plotsSearchTerm) => { - this.setState({plotsSearchTerm}); - }}/> - </div> - <div className="form-group"> - <TitleFieldComponent - id="new-plot-control" - saveIcon={(<i className="material-icons">add</i>)} - inputPlaceholder={__(`Create new`)} - onAdd={(title) => { - this.onPlotAdd(title) - }} - type="userOwned" - customStyle={{width: `100%`}}/> - </div> - </div> - </div> - </div> - <div style={{height: `calc(100% - 74px)`, display: `flex`}}> - <div className="measurements-modal_left-column measurements-modal_scrollable">{propertiesControls}</div> - <div className="measurements-modal_right-column measurements-modal_scrollable" - ref={this.listRef}>{plotsControls}</div> - </div> - </div>); - } -} - -ModalFeatureComponent.propTypes = { - categories: PropTypes.object.isRequired, - feature: PropTypes.object.isRequired, - names: PropTypes.object.isRequired, - limits: PropTypes.object.isRequired, - initialPlots: PropTypes.array.isRequired, - onPlotAdd: PropTypes.func.isRequired, - onAddMeasurement: PropTypes.func.isRequired, - onDeleteMeasurement: PropTypes.func.isRequired -}; - -const mapStateToProps = state => ({ - selectedChemical: state.global.selectedChemical -}) - -export default connect(mapStateToProps)(withDragDropContext(ModalFeatureComponent)); diff --git a/browser/components/ModalMeasurementComponent.js b/browser/components/ModalMeasurementComponent.js deleted file mode 100644 index 73b393d..0000000 --- a/browser/components/ModalMeasurementComponent.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {DragSource} from 'react-dnd'; - - -/** - * Measurement component - */ -class ModalMeasurementComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - const isDragging = this.props.isDragging; - - let circleIcon = false; - if (this.props.icon) { - circleIcon = (<img src={this.props.icon} alt={this.props.title} - style={{width: `12px`, height: `12px`, marginTop: `-2px`}}/>); - } - - if (this.props.chemicalLimits === null) { - return this.props.connectDragSource(<div - title={__(`Drag and drop measurement to add it to time series`)} - className="btn btn-sm btn-primary js-plotted-property" - data-gid="{this.props.boreholeno}" - data-key="{this.props.itemKey}" - data-intake-index="{this.props.intakeIndex}" - style={{ - padding: `4px`, - margin: `1px`, - zIndex: `1000`, - backgroundColor: (isDragging ? `darkgreen` : ``), - color: (isDragging ? `white` : ``), - width: '100%', - textAlign: 'left' - - }}> - <i className="fa fa-arrows-alt"></i> {circleIcon} {this.props.title} ({this.props.intakeName}) - </div>); - } else { - return this.props.connectDragSource(<div - title={__(`Drag and drop measurement to add it to time series`)} - className="btn btn-sm btn-primary js-plotted-property" - data-gid="{this.props.boreholeno}" - data-key="{this.props.itemKey}" - data-intake-index="{this.props.intakeIndex}" - style={{ - padding: `4px`, - margin: `1px`, - zIndex: `1000`, - backgroundColor: (isDragging ? `darkgreen` : ``), - color: (isDragging ? `white` : ``), - width: '100%', - textAlign: 'left' - - }}> - <div> - <div> - <i className="fa fa-arrows-alt"></i> {circleIcon} {this.props.title} ({this.props.intakeName}) - </div> - <div style={{color: 'gray', 'fontSize': 'smaller', 'paddingLeft': '15px'}}> - Historisk: {this.props.detectionLimitReachedForMax ? "< " : ""}{this.props.maxMeasurement === 0 ? "-" : this.props.maxMeasurement} {this.props.maxMeasurement === 0 ? "" : <span style={{textTransform: "lowercase"}}>{this.props.unit}</span>} | - Seneste: {this.props.detectionLimitReachedForLatest ? "< " : ""}{this.props.latestMeasurement} <span style={{textTransform: "lowercase"}}>{this.props.unit}</span> - </div> - </div> - </div>); - } - } -} - -const measurementSource = { - beginDrag(props) { - return { - gid: props.gid, - itemKey: props.itemKey, - intakeIndex: props.intakeIndex, - onAddMeasurement: props.onAddMeasurement - }; - } -}; - -const collect = (connect, monitor) => { - return { - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - } -}; - -ModalMeasurementComponent.propTypes = { - itemKey: PropTypes.string.isRequired, - intakeIndex: PropTypes.number.isRequired, - intakeName: PropTypes.string.isRequired, - onAddMeasurement: PropTypes.func.isRequired, -}; - -export default DragSource(`MEASUREMENT`, measurementSource, collect)(ModalMeasurementComponent); diff --git a/browser/components/ModalPlotComponent.js b/browser/components/ModalPlotComponent.js deleted file mode 100644 index 99941ab..0000000 --- a/browser/components/ModalPlotComponent.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DropTarget } from 'react-dnd'; - -const utils = require('./../utils'); - -/** - * Plot component - */ -class ModalPlotComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let removeButtons = []; - if (this.props.plot?.measurements?.length) { - for (let i = 0; i < this.props.plot.measurements.length; i++) { - let measurement = this.props.plot.measurements[i]; - let measurementDisplayTitle = measurement; - let splitMeasurementId = measurement.split(':'); - - let customGraph = -1, key, intakeIndex; - if (splitMeasurementId.length === 3) { - customGraph = false; - key = splitMeasurementId[1]; - intakeIndex = splitMeasurementId[2]; - } else if (splitMeasurementId.length === 4) { - customGraph = true; - key = splitMeasurementId[1] + ':' + splitMeasurementId[2]; - intakeIndex = splitMeasurementId[3]; - } else { - throw new Error(`Invalid measurement key (${measurement})`); - } - - let boreholeno = splitMeasurementId[0]; - if (this.props.dataSource && this.props.dataSource.length > 0) { - this.props.dataSource.map(item => { - if (item.properties.boreholeno === boreholeno) { - if (customGraph) { - let json = JSON.parse(item.properties[splitMeasurementId[1]]).data[splitMeasurementId[2]]; - let intakeName = `#` + (parseInt(splitMeasurementId[3]) + 1); - if (`intakes` in json && Array.isArray(json.intakes) && json.intakes[parseInt(splitMeasurementId[3])] !== null) { - intakeName = json.intakes[parseInt(splitMeasurementId[3])]; - } - - measurementDisplayTitle = (`${item.properties.boreholeno}, ${json.data[0].name} (${intakeName})`); - return false; - } else { - let json = JSON.parse(item.properties[splitMeasurementId[1]]); - let intakeName = `#` + (parseInt(splitMeasurementId[2]) + 1); - if (`intakes` in json && Array.isArray(json.intakes) && json.intakes[parseInt(splitMeasurementId[2])] !== null) { - intakeName = json.intakes[parseInt(splitMeasurementId[2])]; - } - - let title = utils.getMeasurementTitle(item); - measurementDisplayTitle = (`${title}, ${json.title} (${intakeName})`); - return false; - } - } - }); - } - - const onDelete = () => { - this.props.onDeleteMeasurement(this.props.plot.id, boreholeno, key, intakeIndex); - }; - - removeButtons.push(<div - key={`remove_measurement_` + i + `_` + splitMeasurementId[1] + `_` + splitMeasurementId[2]}> - <button - title={__(`Remove from time series`)} - type="button" - className="btn btn-sm btn-primary" - data-plot-id="{this.props.plot.id}" - data-gid="{boreholeno}" - data-key="{splitMeasurementId[1]}" - data-intake-index="{splitMeasurementId[2]}" - onClick={onDelete} - style={{padding: `4px`, margin: `1px`}}> - <i className="fa fa-remove"></i> {measurementDisplayTitle} - </button> - </div>); - } - } - - const isOver = this.props.isOver; - return this.props.connectDropTarget(<div - className="well well-sm js-plot" - data-id="{this.props.plot.id}" - style={{ - marginBottom: `18px`, - boxShadow: `0 4px 12px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19)`, - backgroundColor: (isOver ? `darkgreen` : ``), - color: (isOver ? `white` : ``), - }}> - <div style={{display: 'flex'}}> - <div>{this.props.plot.title}</div> - <div style={{marginLeft: 'auto'}}> - {__(`Dashboard`)} <input type="checkbox" checked={this.props.isActive} onChange={(event) => { - event.target.checked ? this.props.onPlotShow(this.props.plot) : this.props.onPlotHide(this.props.plot)}}/> - </div> - </div> - <div>{removeButtons}</div> - </div>); - } -} - -const plotTarget = { - drop(props, monitor) { - let item = monitor.getItem(); - item.onAddMeasurement(props.plot.id, item.gid, item.itemKey, item.intakeIndex); - } -}; - -function collect(connect, monitor) { - return { - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver() - }; -} - -ModalPlotComponent.propTypes = { - onDeleteMeasurement: PropTypes.func.isRequired -}; - -export default DropTarget(`MEASUREMENT`, plotTarget, collect)(ModalPlotComponent); diff --git a/browser/components/PlotComponent.js b/browser/components/PlotComponent.js deleted file mode 100644 index 5af0e4c..0000000 --- a/browser/components/PlotComponent.js +++ /dev/null @@ -1,304 +0,0 @@ -/* - * @author Martin Høgh <mh@mapcentia.com> - * @copyright 2013-2018 MapCentia ApS - * @license http://www.gnu.org/licenses/#AGPL GNU AFFERO GENERAL PUBLIC LICENSE 3 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import axios from 'axios'; -import dayjs from 'dayjs'; -//import createPlotlyComponent from 'react-plotly.js/factory'; -//import Plotly from 'plotly.js-basic-dist'; -import Plot from 'react-plotly.js'; - -//const Plot = createPlotlyComponent(Plotly); - -import {LIMIT_CHAR} from '../constants'; -import LoadingOverlay from './../../../../browser/modules/shared/LoadingOverlay'; -import SortableHandleComponent from './SortableHandleComponent'; - -const utils = require('./../utils'); - -/** - * Creates single plot with multiple measurements displayed on it - */ -class MenuPanelPlotComponent extends React.Component { - constructor(props) { - super(props); - - this.state = { - loading: false - }; - } - - download() { - let data = []; - this.props.plotMeta.measurements.map((measurementLocationRaw, index) => { - if (measurementLocationRaw in this.props.plotMeta.measurementsCachedData && - this.props.plotMeta.measurementsCachedData[measurementLocationRaw]) { - let measurementLocation = measurementLocationRaw.split(':'); - if (measurementLocation.length === 3) { - let key = measurementLocation[1]; - let intakeIndex = parseInt(measurementLocation[2]); - - let feature = this.props.plotMeta.measurementsCachedData[measurementLocationRaw].data; - let measurementData = JSON.parse(feature.properties[key]); - if (Array.isArray(measurementData.measurements) === false) { - measurementData.measurements = JSON.parse(measurementData.measurements); - } - let formatedDates = measurementData.timeOfMeasurement[intakeIndex].map(x => x.replace("T", " ")); - data.push({ - name: (`${feature.properties.boreholeno} - ${measurementData.title} (${measurementData.unit})`), - x: formatedDates, - y: measurementData.measurements[intakeIndex], - }); - } else if (measurementLocation.length === 4) { - let key = measurementLocation[1]; - let customSpecificator = measurementLocation[2]; - - if ([`daily`, `weekly`, `monthly`].indexOf(customSpecificator) === -1) { - throw new Error(`The custom specificator (${customSpecificator}) is invalid`); - } - - let feature = this.props.plotMeta.measurementsCachedData[measurementLocationRaw].data; - let measurementData = JSON.parse(feature.properties[key]); - let measurementDataCopy = JSON.parse(JSON.stringify(measurementData.data)); - data.push(measurementDataCopy[customSpecificator].data[0]); - } else { - throw new Error(`Invalid key and intake notation: ${measurementLocationRaw}`); - } - - } else { - console.error(`Plot does not contain measurement ${measurementLocationRaw}`); - } - }); - - this.setState({loading: true}); - axios.post('/api/extension/watsonc/download-plot', { - title: this.props.plotMeta.title, - data - }, {responseType: 'arraybuffer'}).then(response => { - const filename = this.props.plotMeta.title.replace(/\s+/g, '_').toLowerCase() + '.xlsx'; - const url = window.URL.createObjectURL(new Blob([response.data], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})); - - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', filename); - document.body.appendChild(link); - link.click(); - window.URL.revokeObjectURL(url); - - this.setState({loading: false}); - }).catch(error => { - console.error(error); - alert(`Error occured while generating plot XSLS file`); - this.setState({loading: false}); - }) - - } - - render() { - let plot = (<p className="text-muted">{__(`At least one y axis has to be provided`)}</p>); - let data = []; - if (this.props.plotMeta.measurements && this.props.plotMeta.measurements.length > 0) { - let colors = ['rgb(19,128,196)', 'rgb(16,174,140)', 'rgb(235,96,29)', 'rgb(247,168,77)', 'rgb(119,203,231)', `black`] - - let minTime = false; - let maxTime = false; - - let yAxis2LayoutSettings = false; - this.props.plotMeta.measurements.map((measurementLocationRaw, index) => { - if (this.props.plotMeta.measurementsCachedData && measurementLocationRaw in this.props.plotMeta.measurementsCachedData && - this.props.plotMeta.measurementsCachedData[measurementLocationRaw] && - this.props.plotMeta.measurementsCachedData[measurementLocationRaw].data - ) { - let measurementLocation = measurementLocationRaw.split(':'); - - let feature = this.props.plotMeta.measurementsCachedData[measurementLocationRaw].data; - if (measurementLocation.length === 3) { - let key = measurementLocation[1]; - let intakeIndex = parseInt(measurementLocation[2]); - let createdAt = this.props.plotMeta.measurementsCachedData[measurementLocationRaw].created_at; - let measurementData = JSON.parse(feature.properties[key]); - /* - let localMinTime = measurementData.timeOfMeasurement[intakeIndex][0]; - if (minTime === false) { - minTime = localMinTime; - } else { - if (dayjs(localMinTime).isBefore(minTime)) { - minTime = localMinTime; - } - } - - let localMaxTime = measurementData.timeOfMeasurement[intakeIndex][measurementData.timeOfMeasurement[intakeIndex].length - 1]; - if (maxTime === false) { - maxTime = localMaxTime; - } else { - if (dayjs(localMaxTime).isAfter(maxTime)) { - maxTime = localMaxTime; - } - } - */ - - let textValues = []; - if (measurementData.attributes && Array.isArray(measurementData.attributes[intakeIndex]) && measurementData.attributes[intakeIndex].length > 0) { - let xValues = [], yValues = []; - - measurementData.attributes[intakeIndex].map((item, index) => { - if (item === LIMIT_CHAR) { - xValues.push(measurementData.timeOfMeasurement[intakeIndex][index]); - yValues.push(measurementData.measurements[intakeIndex][index]); - textValues.push(measurementData.measurements[intakeIndex][index] + ' ' + LIMIT_CHAR); - } else { - textValues.push(measurementData.measurements[intakeIndex][index]); - } - }); - - if (xValues.length > 0) { - data.push({ - x: xValues, - y: yValues, - type: 'scattergl', - mode: 'markers', - hoverinfo: 'none', - showlegend: false, - marker: { - color: 'rgba(17, 157, 255, 0)', - size: 20, - line: { - color: 'rgb(231, 0, 0)', - width: 3 - } - }, - }); - } - } else { // Calypso stations - measurementData.measurements[intakeIndex].map((item, index) => { - textValues.push(Math.round(measurementData.measurements[intakeIndex][index] * 100) / 100); - }); - } - - let title = utils.getMeasurementTitle(feature); - let plotData = { - name: (`${title} (${measurementData.intakes ? measurementData.intakes[intakeIndex] : (intakeIndex + 1)}) - ${measurementData.title} (${measurementData.unit})`), - x: measurementData.timeOfMeasurement[intakeIndex], - y: measurementData.measurements[intakeIndex], - type: 'scattergl', - mode: 'lines+markers', - hoverinfo: 'text', - marker: { - color: colors[index] - } - }; - - if (textValues.length > 0) plotData.hovertext = textValues; - data.push(plotData); - } else if (measurementLocation.length === 4) { - let key = measurementLocation[1]; - let customSpecificator = measurementLocation[2]; - - if ([`daily`, `weekly`, `monthly`].indexOf(customSpecificator) === -1) { - throw new Error(`The custom specificator (${customSpecificator}) is invalid`); - } - - let measurementData = JSON.parse(feature.properties[key]); - let measurementDataCopy = JSON.parse(JSON.stringify(measurementData.data)); - data.push(measurementDataCopy[customSpecificator].data[0]); - - let range = [0, 0]; - for (let key in measurementDataCopy) { - if (measurementDataCopy[key].layout.yaxis2.range) { - if (measurementDataCopy[key].layout.yaxis2.range[0] < range[0]) range[0] = measurementDataCopy[key].layout.yaxis2.range[0]; - if (measurementDataCopy[key].layout.yaxis2.range[1] > range[1]) range[1] = measurementDataCopy[key].layout.yaxis2.range[1]; - } - - yAxis2LayoutSettings = measurementDataCopy[key].layout.yaxis2; - } - - yAxis2LayoutSettings.range = range; - yAxis2LayoutSettings.showgrid = false; - } else { - throw new Error(`Invalid key and intake notation: ${measurementLocationRaw}`); - } - } else { - console.error(`Plot does not contain measurement ${measurementLocationRaw}`); - } - }); - - let layout = { - displayModeBar: false, - margin: { - l: 30, - r: (yAxis2LayoutSettings ? 30 : 5), - b: 30, - t: 5, - pad: 4 - }, - xaxis: { - autorange: true, - margin: 0, - type: 'date' - }, - yaxis: { - autorange: true, - }, - showlegend: true, - legend: { - orientation: "h", - y: -0.2 - }, - autosize: true - }; - - if (yAxis2LayoutSettings) { - layout.yaxis2 = yAxis2LayoutSettings; - } - - plot = (<Plot - data={data} - layout={layout} - onLegendDoubleClick={() => false} - style={{width: "100%", height: `${this.props.height - 60}px`}}/>); - } - - return (<div style={{maxHeight: ($(document).height() * 0.4 + 40) + 'px'}}> - {this.state.loading ? <LoadingOverlay/> : false} - <div style={{height: `40px`}}> - <div style={{float: `left`}}> - <h4>{this.props.plotMeta.title}</h4> - </div> - <div style={{float: `right`}}> - <a - className="btn btn-primary" - href="javascript:void(0)" - disabled={data.length === 0} - title={__(`Download`) + ` ` + this.props.plotMeta.title} - onClick={() => { - this.download() - }} - style={{padding: `0px`, marginLeft: `10px`}}> - <i className="fa fa-download"></i> {__(`Download`)}</a> - <SortableHandleComponent title={this.props.plotMeta.title}/> - <a - className="btn" - href="javascript:void(0)" - title={__(`Remove`) + ` ` + this.props.plotMeta.title} - onClick={() => { - this.props.onDelete(this.props.plotMeta.id) - }} - style={{padding: `0px`, marginLeft: `20px`}}> - <i className="fa fa-remove"></i> {__(`Remove`)}</a> - </div> - </div> - <div style={{height: `${this.props.height - 50}px`, border: `1px solid lightgray`}}>{plot}</div> - </div>); - } -} - -MenuPanelPlotComponent.propTypes = { - onDelete: PropTypes.func.isRequired, - plotMeta: PropTypes.object.isRequired -}; - -export default MenuPanelPlotComponent; diff --git a/browser/components/ProfileComponent.js b/browser/components/ProfileComponent.js deleted file mode 100644 index 0aa13f1..0000000 --- a/browser/components/ProfileComponent.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -//import createPlotlyComponent from 'react-plotly.js/factory'; -//import Plotly from 'plotly.js-basic-dist'; -//const Plot = createPlotlyComponent(Plotly); -import Plot from 'react-plotly.js'; - -import SortableHandleComponent from './SortableHandleComponent'; - -/** - * Creates single profile with multiple measurements displayed on it - */ -class ProfileComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let dataCopy = JSON.parse(JSON.stringify(this.props.plotMeta.profile.data.data).replace(/%28/g, '(').replace(/%29/g, ')')); - dataCopy.map((item, index) => { - if (!dataCopy[index].mode) dataCopy[index].mode = 'lines'; - }); - - let plot = (<p className="text-muted">{__(`At least one y axis has to be provided`)}</p>); - if (this.props.plotMeta) { - let layoutCopy = JSON.parse(JSON.stringify(this.props.plotMeta.profile.data.layout)); - layoutCopy.margin = { - l: 50, - r: 5, - b: 45, - t: 5, - pad: 1 - }; - layoutCopy.autosize = true; - - plot = (<Plot - data={dataCopy} - useResizeHandler={true} - onClick={this.props.onClick} - config={{modeBarButtonsToRemove: ['autoScale2d']}} - layout={layoutCopy} - style={{width: "100%", height: `${this.props.height - 60}px`}}/>); - } - - return (<div> - <div style={{height: `40px`}}> - <div style={{float: `left`}}> - <h4>{this.props.plotMeta.profile.title}</h4> - </div> - <div style={{float: `right`}}> - <a - className="btn btn-primary" - href="javascript:void(0)" - title={__(`Change datatype`) + ` ` + this.props.plotMeta.profile.title} - onClick={() => { this.props.onChangeDatatype(this.props.plotMeta.key)}} - style={{padding: `0px`}}> - <i className="fa fa-edit"></i> {__(`Change datatype`)} - </a> <SortableHandleComponent title={this.props.plotMeta.profile.title}/> <a - className="btn" - href="javascript:void(0)" - title={__(`Remove`) + ` ` + this.props.plotMeta.profile.title} - onClick={() => { this.props.onDelete(this.props.plotMeta.key)}} - style={{padding: `0px`, marginLeft: `20px`}}> - <i className="fa fa-remove"></i> {__(`Remove`)} - </a> - </div> - </div> - <div style={{height: `${this.props.height - 50}px`, border: `1px solid lightgray`}}>{plot}</div> - </div>); - } -} - -ProfileComponent.propTypes = { - onDelete: PropTypes.func.isRequired, - onChangeDatatype: PropTypes.func.isRequired, - plotMeta: PropTypes.object.isRequired -}; - -export default ProfileComponent; diff --git a/browser/components/SortableHandleComponent.js b/browser/components/SortableHandleComponent.js deleted file mode 100644 index 4db77b8..0000000 --- a/browser/components/SortableHandleComponent.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import {sortableHandle} from 'react-sortable-hoc'; - -const SortableHandleComponent = (props) => { - return (<a - className="btn btn-primary" - href="javascript:void(0)" - title={__(`Move`) + ` ` + props.title} - style={{padding: `0px`, marginLeft: `20px`}}> - <i className="fa fa-bars"></i> {__(`Move`)} - </a>); -} - -export default sortableHandle(SortableHandleComponent); \ No newline at end of file diff --git a/browser/components/SortablePlotComponent.js b/browser/components/SortablePlotComponent.js deleted file mode 100644 index 5110a1b..0000000 --- a/browser/components/SortablePlotComponent.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {sortableElement} from 'react-sortable-hoc'; -import {VIEW_ROW} from './../constants'; - -import PlotComponent from './PlotComponent'; - -/** - * Wrapper for making a Plot component sortable inside of Plots grid - */ -const SortablePlotComponent = (props) => { - return (<li className={props.viewMode === VIEW_ROW ? `list-group-item col-sm-12 col-md-12 col-lg-12` : `list-group-item col-sm-12 col-md-12 col-lg-6`} style={{ - height: `${props.height}px`, - padding: `0px 16px 0px 16px` - }}> - <div> - <PlotComponent - onDelete={(id) => { props.handleDelete(id)}} - height={props.height} - viewMode={props.viewMode} - plotMeta={props.meta}/> - </div> - </li>); -} - -SortablePlotComponent.propTypes = { - handleDelete: PropTypes.func.isRequired, - meta: PropTypes.object.isRequired, -}; - -export default sortableElement(SortablePlotComponent); \ No newline at end of file diff --git a/browser/components/SortablePlotsGridComponent.js b/browser/components/SortablePlotsGridComponent.js deleted file mode 100644 index 0a3668f..0000000 --- a/browser/components/SortablePlotsGridComponent.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import {sortableContainer} from 'react-sortable-hoc'; - -const SortablePlotsGridComponent = sortableContainer(({children}) => { - return (<ul className="list-group row" style={{marginBottom: `0px`}}>{children}</ul>); -}); - -export default SortablePlotsGridComponent; \ No newline at end of file diff --git a/browser/components/SortableProfileComponent.js b/browser/components/SortableProfileComponent.js deleted file mode 100644 index 7904c6b..0000000 --- a/browser/components/SortableProfileComponent.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {sortableElement} from 'react-sortable-hoc'; -import {VIEW_ROW} from './../constants'; - -import ProfileComponent from './ProfileComponent'; - -/** - * Wrapper for making a Profile component sortable inside of Plots&Profiles grid - */ -const SortableProfileComponent = (props) => { - return (<li className={props.viewMode === VIEW_ROW ? `list-group-item col-sm-12 col-md-12 col-lg-12` : `list-group-item col-sm-12 col-md-12 col-lg-6`} style={{ - height: `${props.height}px`, - padding: `0px 16px 0px 16px` - }}> - <div> - <ProfileComponent - onDelete={(id) => { props.handleDelete(id)}} - onClick={props.handleClick} - height={props.height} - onChangeDatatype={(id) => { props.handleChangeDatatype(id)}} - plotMeta={props.meta}/> - </div> - </li>); -} - -SortableProfileComponent.propTypes = { - handleDelete: PropTypes.func.isRequired, - handleClick: PropTypes.func.isRequired, - handleChangeDatatype: PropTypes.func.isRequired, - meta: PropTypes.object.isRequired, -}; - -export default sortableElement(SortableProfileComponent); \ No newline at end of file diff --git a/browser/components/SubscriptionDialogue.js b/browser/components/SubscriptionDialogue.js new file mode 100644 index 0000000..4d021ab --- /dev/null +++ b/browser/components/SubscriptionDialogue.js @@ -0,0 +1,437 @@ +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import Title from "./shared/title/Title"; +import Icon from "./shared/icons/Icon"; +import CloseButton from "./shared/button/CloseButton"; +import { hexToRgbA } from "../helpers/colors"; +import { DarkTheme } from "../themes/DarkTheme"; +import { Align } from "./shared/constants/align"; +import { Variants } from "./shared/constants/variants"; +import { Size } from "./shared/constants/size"; +import ThemeProvider from "../themes/ThemeProvider"; +import reduxStore from "../redux/store"; +import { Provider } from "react-redux"; +import { setDashboardMode } from "../redux/actions"; +import { LOGIN_MODAL_DIALOG_PREFIX } from "../constants"; +const session = require("./../../../session/browser/index"); + +function SubscriptionDialogue(props) { + return ( + <ThemeProvider> + <Provider store={reduxStore}> + <Root> + <ModalHeader> + <Grid container> + <Grid container item xs={10}> + <Title + text={__("Få mere ud af Calypso")} + color={DarkTheme.colors.headings} + /> + </Grid> + <Grid container justify="flex-end" item xs={2}> + <CloseButton onClick={props.onCloseButtonClick} /> + </Grid> + </Grid> + </ModalHeader> + <ModalBody> + <Grid container spacing={8}> + <Grid container item xs={6}> + <PlanPlaceholder> </PlanPlaceholder> + <PlanItem> + <LabelContainer> + <Title + level={5} + text={__("Ubegrænset adgang til data")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <Title + level={7} + text={__( + "Data fra Jupiter, pesticider og online stationer samlet ét sted" + )} + color={DarkTheme.colors.gray[3]} + /> + </PlanItem> + {/* <PlanItem> + <LabelContainer> + <Title level={5} text={__('Antal brugere')} color={DarkTheme.colors.headings} /> + </LabelContainer> + <Title level={7} text={__('Arbejd sammen med dine kollegaer direkte i Calypso')} color={DarkTheme.colors.gray[3]} /> + </PlanItem> */} + <PlanItem> + <LabelContainer> + <Title + level={5} + text={__("Dashboard med grafvisning af data over tid")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <Title + level={7} + text={__( + "Gør det nemt at sammenligne data på tværs af boringer" + )} + color={DarkTheme.colors.gray[3]} + /> + </PlanItem> + <PlanItem> + <LabelContainer> + <Title + level={5} + text={__("Profilværktøj med geologi og kemi")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <Title + level={7} + text={__( + "Grafisk visning af geologiske snit med jordlag og boringer" + )} + color={DarkTheme.colors.gray[3]} + /> + </PlanItem> + <PlanItem> + <LabelContainer> + <Title + level={5} + text={__("Gem dashboards")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <Title + level={7} + text={__("Gem dashboards med valgte datakilder og grafer")} + color={DarkTheme.colors.gray[3]} + /> + </PlanItem> + <PlanItem> + <LabelContainer> + <Title + level={5} + text={__("Brugerdefinerede lag")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <Title + level={7} + text={__("Mulighed for at få egne data ind i private lag")} + color={DarkTheme.colors.gray[3]} + /> + </PlanItem> + </Grid> + <Grid container item xs={2}> + <Plan className="short-title"> + <PlanItemTitle className="short-title"> + <LabelContainer> + <Title + level={3} + text={__("BASIS")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <LabelContainer> + <Title + level={7} + text={__( + "Prøv og bliv fortrolig med Calypso - ganske gratis" + )} + color={DarkTheme.colors.gray[3]} + align={Align.Center} + /> + </LabelContainer> + </PlanItemTitle> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + {/* <PlanItem> + <PlanFeature> + <Title level={6} text={__('1 bruger')} color={DarkTheme.colors.gray[3]} /> + </PlanFeature> + </PlanItem> */} + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("1 graf")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("1 profil")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <PlanFeature></PlanFeature> + </PlanItem> + <PlanItem> + <PlanFeature></PlanFeature> + </PlanItem> + </Plan> + </Grid> + <Grid container item xs={2}> + <Plan className="short-title"> + <PlanItemTitle className="short-title"> + <LabelContainer> + <Title + level={3} + text={__("Premium")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <LabelContainer> + <Title + level={7} + text={__("200 kr. pr. måned")} + color={DarkTheme.colors.gray[3]} + align={Align.Center} + /> + </LabelContainer> + </PlanItemTitle> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + {/* <PlanItem> + <PlanFeature> + <Title level={6} text={__('1 bruger')} color={DarkTheme.colors.gray[3]} /> + </PlanFeature> + </PlanItem> */} + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("Ubegrænset")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("Ubegrænset")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + <PlanItem> + <PlanFeature></PlanFeature> + </PlanItem> + </Plan> + </Grid> + <Grid container item xs={2}> + <Plan> + <PlanItemTitle> + <Title + level={5} + text={__("Calypso for")} + color={DarkTheme.colors.gray[3]} + /> + <LabelContainer> + <Title + level={3} + text={__("Virksomheder")} + color={DarkTheme.colors.headings} + /> + </LabelContainer> + <LabelContainer> + <Title + level={7} + text={__("Tag kontakt")} + color={DarkTheme.colors.gray[3]} + align={Align.Center} + /> + </LabelContainer> + </PlanItemTitle> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + {/* <PlanItem> + <PlanFeature> + <Title level={6} text={__('5 brugere')} color={DarkTheme.colors.gray[3]} /> + </PlanFeature> + </PlanItem> */} + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("Ubegrænset")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <PlanFeature> + <Title + level={6} + text={__("Ubegrænset")} + color={DarkTheme.colors.gray[3]} + /> + </PlanFeature> + </PlanItem> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + <PlanItem> + <IconContainer> + <Icon name="check-mark-solid" size={12} /> + </IconContainer> + </PlanItem> + </Plan> + </Grid> + </Grid> + <Grid container> + <Grid container item xs={6}></Grid> + <Grid container item xs={2}> + {!props.session.isAuthenticated() && ( + <SelectPlanButton + onClick={() => { + $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("show"); + $("#upgrade-modal").modal("hide"); + }} + > + {__("Log ind")}{" "} + </SelectPlanButton> + )} + </Grid> + <Grid container item xs={2}> + {props.session?.getProperties()?.license !== "premium" && ( + <SelectPlanButton + onClick={() => + window.open( + "https://admin.calypso.watsonc.dk/login?new=1", + "_blank" + ) + } + > + {__("Vælg premium")}{" "} + </SelectPlanButton> + )} + </Grid> + <Grid container item xs={2}> + <SelectPlanButton + onClick={() => + window.open("https://calypso.watsonc.dk/kontakt/", "_blank") + } + > + {__("Kontakt")}{" "} + </SelectPlanButton> + </Grid> + </Grid> + </ModalBody> + </Root> + </Provider> + </ThemeProvider> + ); +} + +const Root = styled.div` + background: ${({ theme }) => hexToRgbA(theme.colors.primary[1], 0.96)}; + border-radius: ${({ theme }) => `${theme.layout.borderRadius.large}px`}; + color: ${({ theme }) => `${theme.colors.headings}`}; +`; + +const ModalHeader = styled.div` + padding: ${({ theme }) => + `${theme.layout.gutter}px ${theme.layout.gutter}px 0 ${theme.layout.gutter}px`}; +`; + +const ModalBody = styled.div` + padding: ${({ theme }) => `${theme.layout.gutter}px`}; +`; + +const LabelContainer = styled.div` + display: block; +`; + +const Plan = styled.div` + width: 100%; + border: 3px solid ${({ theme }) => `${theme.colors.primary[3]}`}; + border-radius: ${({ theme }) => `12px 12px 0px 0px`}; + &.short-title { + margin-top: 16px; + } + &:hover { + border: 3px solid ${({ theme }) => `${theme.colors.interaction[4]}`}; + } +`; + +const PlanPlaceholder = styled.div` + height: 80px; + width: 100%; +`; + +const PlanItemTitle = styled.div` + width: 100%; + height: 80px; + text-align: center; + background: ${({ theme }) => `${theme.colors.primary[3]}`}; + padding: ${({ theme }) => `${theme.layout.gutter / 4}px`}; + border-radius: ${({ theme }) => + `${theme.layout.gutter / 4}px ${theme.layout.gutter / 4}px 0px 0px`}; + &.short-title { + height: 64px; + } +`; + +const PlanFeature = styled.div``; + +const PlanItem = styled.div` + height: 80px; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + border-bottom: 1px solid ${({ theme }) => `${theme.colors.gray[2]}`}; + padding-bottom: ${({ theme }) => `${theme.layout.gutter / 4}px`}; + ${PlanFeature} { + margin: auto; + } + &:last-child { + border-bottom: 0; + } +`; + +const IconContainer = styled.div` + height: 16px; + width: 16px; + border-radius: 50%; + background: ${({ theme }) => `${theme.colors.interaction[4]}`}; + color: #000; + margin: auto; + text-align: center; +`; + +const SelectPlanButton = styled.button` + background: ${({ theme }) => `${theme.colors.interaction[4]}`}; + color: #000; + border: none; + border-radius: ${({ theme }) => `${theme.layout.borderRadius.small}px`}; + text-align: center; + width: 100%; + height: 50px; + margin: auto; + margin-top: ${({ theme }) => `${theme.layout.gutter / 2}px`}; + margin-left: 6px; +`; + +export default SubscriptionDialogue; diff --git a/browser/components/TopBar.js b/browser/components/TopBar.js new file mode 100644 index 0000000..5ad672d --- /dev/null +++ b/browser/components/TopBar.js @@ -0,0 +1,119 @@ +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import { getLogo } from "../utils"; +import Button from "./shared/button/Button"; +import { Variants } from "./shared/constants/variants"; +import SearchBox from "./shared/inputs/Searchbox"; +import UserProfileButton from "./shared/userProfileButton/UserProfileButton"; +import { showSubscription } from "../helpers/show_subscriptionDialogue"; +import { useState, useEffect } from "react"; +import useInterval from "./shared/hooks/useInterval"; +import { LOGIN_MODAL_DIALOG_PREFIX } from "../constants"; + +const FREE = "FREE"; +const NOTLOGGEDIN = "NOTLOGGEDIN"; +const PREMIUM = "PREMIUM"; + +function TopBar(props) { + const [status, setStatus] = useState(NOTLOGGEDIN); + const [stopPoll, setStopPoll] = useState(false); + + useInterval( + () => { + if (props.session.isStatusChecked()) { + setStopPoll(true); + if (props.session.getProperties() !== null) { + setStatus( + props.session.getProperties()["license"] === "premium" + ? PREMIUM + : FREE + ); + } else { + setStatus(NOTLOGGEDIN); + } + } + }, + stopPoll ? null : 1000 + ); + + useEffect(() => { + props.backboneEvents.get().on("refresh:meta", () => { + console.log("whaat"); + console.log(props.session.getProperties()); + if (props.session.getProperties() !== null) { + setStatus( + props.session.getProperties()["license"] === "premium" + ? PREMIUM + : FREE + ); + } else { + setStatus(NOTLOGGEDIN); + } + }); + }, []); + + return ( + <Row container justify="center" alignItems="center"> + <Grid item xs={1}> + <Logo src={getLogo()} alt="Calypso logo" title="Calypso logo" /> + </Grid> + <Grid item xs={2}> + <div className="js-layer-slide-breadcrumbs"> + <button type="button" className="navbar-toggle" id="burger-btn"> + <i className="fa fa-database"></i> Vælg data + </button> + </div> + </Grid> + <Grid item xs={6}> + <div id="place-search"> + <div className="places"> + <input + id="custom-search" + className="custom-search typeahead" + type="text" + placeholder="Søg på IoT-stationer, adresse, matrikel- eller DGU nummer" + /> + <span + id="searchclear" + className="glyphicon glyphicon-remove-circle" + onClick={() => + (document.getElementById("custom-search").value = "") + } + ></span> + </div> + </div> + </Grid> + <Grid container item xs={2} justify="flex-end"> + {status === FREE && ( + <Button + text="Opgrader Calypso" + variant={Variants.Primary} + onClick={showSubscription} + /> + )} + {status === NOTLOGGEDIN && ( + <Button + text="Log ind" + variant={Variants.Primary} + onClick={() => $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("show")} + /> + )} + </Grid> + <Grid container item xs={1} justify="center"> + <UserProfileButton /> + </Grid> + </Row> + ); +} + +const Row = styled(Grid)` + background-color: ${({ theme }) => theme.colors.primary[4]}; + height: ${({ theme }) => theme.layout.gutter * 2}px; +`; + +const Logo = styled.img` + height: ${({ theme }) => theme.layout.gutter}px; + margin-left: ${({ theme }) => theme.layout.gutter / 2}px; +`; + +export default TopBar; diff --git a/browser/components/dashboardshell/CardListItem.js b/browser/components/dashboardshell/CardListItem.js new file mode 100644 index 0000000..b3d5788 --- /dev/null +++ b/browser/components/dashboardshell/CardListItem.js @@ -0,0 +1,209 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import Icon from "../shared/icons/Icon"; +import Title from "../shared/title/Title"; + +const utils = require("../../utils"); + +const options = [ + { index: 0, text: "Døgnmiddel", window: "day", func: "mean" }, + { index: 1, text: "Ugemiddel", window: "week", func: "mean" }, + { index: 2, text: "Månedmiddel", window: "month", func: "mean" }, + + { index: 3, text: "Døgnsum", window: "day", func: "sum" }, + { index: 4, text: "Ugesum", window: "week", func: "sum" }, + { index: 5, text: "Månedsum", window: "month", func: "sum" }, +]; + +function CardListItem(props) { + const [name, setName] = useState(""); + const [infoForDeletion, setInfoForDeletion] = useState({}); + const [useSumInsteadOfMean, setUseSumInsteadOfMean] = useState(false); + + const aggregateValue = options.find((elem) => { + return ( + elem.window === props.aggregate?.window && + elem.func === props.aggregate?.func + ); + }); + + const selectValue = aggregateValue ? aggregateValue.index : ""; + + useEffect(() => { + if (props.measurement) { + let splitMeasurement = props.measurement.split(":"); + let measurementLength = splitMeasurement.length; + let key = null; + let feature = null; + let intakeIndex = null; + let boreholeno = splitMeasurement[0]; + let idx = null; + if (measurementLength === 3) { + key = splitMeasurement[1]; + intakeIndex = splitMeasurement[2]; + } else if (measurementLength === 4) { + key = splitMeasurement[1] + ":" + splitMeasurement[2]; + intakeIndex = splitMeasurement[3]; + } + if ( + props.plot.measurementsCachedData && + props.plot.measurementsCachedData[props.measurement] + ) { + feature = props.plot.measurementsCachedData[props.measurement].data; + let measurementData = JSON.parse(feature.properties[key]); + + idx = measurementData?.ts_id.findIndex( + (elem) => elem.toString() == intakeIndex + ); + // const NAME = measurementData.title + // ? `${measurementData.locname} ${measurementData.title}, ${measurementData.parameter}` + // : `${measurementData.locname}, ${measurementData.parameter}`; + setName(measurementData?.data[idx]?.name); + + if (measurementData?.data[idx]?.tstype_id === 4) { + setUseSumInsteadOfMean(true); + } + // setName(`${measurementData.title} (${measurementData.unit})`); + } + setInfoForDeletion({ + plotId: props.plot.id, + boreholeno, + key, + intakeIndex, + }); + } + }, [props.measurement, props.plot]); + + return ( + <Root> + <Grid container> + {/* <Grid container item xs={6}> + <CardListLabel> + <Title level={6} text={name} marginLeft={8} /> + </CardListLabel> + </Grid> */} + <Grid container item xs={11}> + <Select + value={selectValue} + onChange={(e) => { + if (e.target.value === "") { + props.deleteAggregate(props.measurement); + } else { + props.setAggregate( + props.measurement, + options[e.target.value].window, + options[e.target.value].func + ); + } + }} + > + <option value="" hidden> + {name} + </option> + {useSumInsteadOfMean + ? options + .filter((elem) => elem.func === "sum") + .map((elem) => { + return ( + <option value={elem.index} key={elem.index}> + {name + " - " + elem.text} + </option> + ); + }) + : options + .filter((elem) => elem.func === "mean") + .map((elem) => { + return ( + <option value={elem.index} key={elem.index}> + {name + " - " + elem.text} + </option> + ); + })} + </Select> + {/* <button + onClick={() => { + console.log(props); + props.setAggregate(props.measurement, "day", "mean"); + }} + > + Tryk + </button> */} + </Grid> + <Grid container item xs={1} justify="flex-end"> + <RemoveIconContainer + onClick={() => { + props.deleteAggregate(props.measurement); + props.onDeleteMeasurement( + infoForDeletion.plotId, + infoForDeletion.boreholeno, + infoForDeletion.key, + infoForDeletion.intakeIndex + ); + }} + > + <Icon name="cross" size={16} /> + </RemoveIconContainer> + </Grid> + </Grid> + </Root> + ); +} + +const RemoveIconContainer = styled.div` + width: 18px; + height: 18px; + margin-top: 3px; + border: 1px solid ${(props) => props.theme.colors.gray[2]}; + border-radius: 50%; + display: none; + cursor: pointer; +`; + +const Root = styled.div` + width: 100%; + padding: ${(props) => props.theme.layout.gutter / 8}px; + vertical-align: middle; + height: ${(props) => props.theme.layout.gutter}px; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + &:hover { + background: ${(props) => props.theme.colors.gray[4]}; + ${RemoveIconContainer} { + display: block; + } + } +`; + +const CardListLabel = styled.div` + vertical-align: middle; + display: inline-block; + // margin-left: ${(props) => props.theme.layout.gutter / 4}px; +`; + +const Select = styled.select` + width: 100%; + height: ${(props) => + props.theme.layout.gutter - props.theme.layout.gutter / 8 + 2}px; + background: transparent; + padding-left: 5px; + font: ${(props) => props.theme.fonts.label}; + border: none; + margin-left: 10px; + word-wrap: break-word; + text-overflow: inherit; + white-space: normal; + color: black; + + option { + color: black; + background: transparent; + display: flex; + white-space: pre; + min-height: 20px; + padding: 0px 2px 1px; + } + + s +`; + +export default CardListItem; diff --git a/browser/components/dashboardshell/ChemicalSelector.js b/browser/components/dashboardshell/ChemicalSelector.js new file mode 100644 index 0000000..f47a62b --- /dev/null +++ b/browser/components/dashboardshell/ChemicalSelector.js @@ -0,0 +1,224 @@ +import { useState, useEffect } from "react"; +import { connect } from "react-redux"; +import Collapse from "@material-ui/core/Collapse"; +import styled from "styled-components"; +import Searchbox from "../shared/inputs/Searchbox"; +import ChemicalsListItem from "./ChemicalsListItem"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Grid from "@material-ui/core/Grid"; +import { DarkTheme } from "../../themes/DarkTheme"; + +function ChemicalSelector(props) { + const [chemicalsList, setChemicalsList] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [loadingData, setLoadingData] = useState(false); + + props.backboneEvents.get().on("watsonc:clearChemicalList", () => { + setChemicalsList([]); + }); + + useEffect(() => { + // setLoadingData(true); + if (!props.feature || !props.feature.properties) { + setChemicalsList([]); + return; + } + let currentgroup = null; + + let param2group = {}; + let tmp = Object.entries(props.categories).map((elem) => { + return { parameter: Object.values(elem[1]), group: elem[0] }; + }); + tmp.forEach((prop) => { + prop.parameter.forEach((param) => { + param2group[param] = prop.group; + }); + }); + + const createMeasurementControl = (item, key) => { + return new Promise(function (resolve, reject) { + const relation = item.feature.properties.relation; + const loc_id = item.feature.properties.loc_id; + const isJupiter = relation.includes("._"); + fetch( + `/api/sql/jupiter?q=SELECT gid,trace,ts_id,locname,loc_id,ts_name,parameter,unit FROM ${relation} WHERE loc_id='${loc_id}'&base64=false&lifetime=60&srs=4326` + ).then( + (res) => { + res.json().then((json) => { + if (!res.ok) { + reject(json); + } + let properties = json.features[0].properties; + let controls = []; + if (properties.ts_name.length < 10) { + controls.push( + <ChemicalsListItem + label={"Alle parametre"} + circleColor={DarkTheme.colors.denotive.warning} + key={"allParameters" + "_" + loc_id} + onAddMeasurement={props.onAddMeasurement} + description={""} + gid={loc_id} + itemKey={properties.ts_id.map( + (elem) => properties.locname + "_" + elem + )} + intakeIndex={properties.ts_id.map((elem) => elem)} + feature={properties} + /> + ); + } + // console.log(properties); + + properties.ts_name.forEach((prop, index) => { + properties.relation = relation; + let intakeName = `#` + properties.ts_id[index]; + let icon = false; + if (isJupiter) { + let group = param2group[properties.parameter[index]]; + if (group != currentgroup) { + controls.push( + <Grid container key={`${index}-title`}> + <Grid container item xs={10}> + <Title + text={param2group[properties.parameter[index]]} + level={4} + marginTop={16} + color="#FFFFFF" + /> + </Grid> + </Grid> + ); + currentgroup = group; + } + } + + controls.push( + <ChemicalsListItem + label={ + properties.ts_name[index] + ? properties.parameter[index] + + " (" + + properties.unit[index] + + ") - " + + properties.ts_name[index] + : properties.parameter[index] + + " (" + + properties.unit[index] + + ")" + } + circleColor={DarkTheme.colors.denotive.warning} + key={properties.loc_id + "_" + properties.ts_id[index]} + onAddMeasurement={props.onAddMeasurement} + description={ + properties.parameter[index] + + ", (" + + properties.unit[index] + + ")" + } + icon={icon} + gid={loc_id} + itemKey={properties.locname + "_" + properties.ts_id[index]} + intakeIndex={properties.ts_id[index]} + intakeName={intakeName} + unit={properties.unit[index]} + title={properties.ts_name[index]} + feature={properties} + /> + ); + }); + resolve(controls); + }); + }, + (err) => { + alert(err.message); + } + ); + }); + }; + + createMeasurementControl(props) + .then((controls) => { + setLoadingData(false); + // controls.unshift(allChems) + setChemicalsList(controls); + }) + .catch((json) => { + alert(json.message); + setLoadingData(false); + }); + }, [props.categories, props.feature]); + + return ( + <Root> + <SearchboxContainer> + {loadingData && ( + <div + style={{ + justifyContent: "center", + left: "50%", + top: "50%", + position: "absolute", + zIndex: 9000, + }} + > + <CircularProgress + style={{ color: DarkTheme.colors.interaction[4] }} + /> + </div> + )} + <Searchbox + placeholder={__("Søg efter dataparameter")} + onChange={(value) => setSearchTerm(value)} + /> + </SearchboxContainer> + <ChemicalsList> + {chemicalsList.filter((item, index) => { + let searchTermLower = searchTerm.toLowerCase(); + if ( + searchTerm.length && + item.props?.label && + item.props.label.toLowerCase().indexOf(searchTermLower) === -1 + ) { + return false; + } else { + return true; + } + })} + </ChemicalsList> + </Root> + ); +} + +const Root = styled.div` + background: ${(props) => props.theme.colors.primary[2]}; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + height: 100%; + width: 100%; + padding: ${(props) => props.theme.layout.gutter / 2}px; +`; + +const SearchboxContainer = styled.div` + position: relative; + width: 100%; + // padding: ${(props) => props.theme.layout.gutter / 2}px 0px; +`; + +const ChemicalsList = styled.div` + width: 100%; + padding: ${(props) => props.theme.layout.gutter / 4}px; +`; + +const ChemicalsListTitle = styled.div` + color: ${(props) => props.theme.colors.headings}; + margin: 10px 0; + cursor: pointer; +`; + +const mapStateToProps = (state) => ({ + categories: state.global.categories, + limits: state.global.limits, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(ChemicalSelector); diff --git a/browser/components/dashboardshell/ChemicalsListItem.js b/browser/components/dashboardshell/ChemicalsListItem.js new file mode 100644 index 0000000..95fa322 --- /dev/null +++ b/browser/components/dashboardshell/ChemicalsListItem.js @@ -0,0 +1,118 @@ +import { useState, useEffect } from "react"; +import { useDrag } from "react-dnd"; +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import { DarkTheme } from "../../themes/DarkTheme"; + +function ChemicalsListItem(props) { + const [{ isDragging }, drag] = useDrag(() => ({ + type: "MEASUREMENT", + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + item: { + gid: props.gid, + itemKey: props.itemKey, + intakeIndex: props.intakeIndex, + onAddMeasurement: props.onAddMeasurement, + feature: props.feature, + }, + })); + + const [descriptionText, setDescriptionText] = useState(""); + + useEffect(() => { + // TODO brug nyt felt + let description = ""; + setDescriptionText("ohyeah"); + }, [ + props.detectionLimitReachedForMax, + props.maxMeasurement, + props.detectionLimitReachedForLatest, + props.latestMeasurement, + props.unit, + ]); + return ( + <Root + title={__(`Drag and drop measurement to add it to time series`)} + ref={drag} + data-gid="{props.gid}" + data-key="{props.key}" + data-intake-index="{props.intakeIndex}" + > + <Grid container> + <Grid container item xs={1}> + <IconContainer> + <Icon name="drag-handle-solid" size={24} /> + </IconContainer> + </Grid> + {/*<Grid container item xs={1} justify="center">*/} + {/* <CircleImage src={props.icon} />*/} + {/*</Grid>*/} + <Grid container item xs={9}> + <LabelRow> + <Title level={6} text={props.label} /> + </LabelRow> + {/* <LabelRow> + <Title + level={6} + text={props.description} + color={DarkTheme.colors.gray[3]} + /> + </LabelRow> */} + </Grid> + </Grid> + </Root> + ); +} + +const Root = styled.div` + color: ${(props) => props.theme.colors.headings}; + margin-top: ${(props) => props.theme.layout.gutter / 8}px; + padding: ${(props) => props.theme.layout.gutter / 8}px 0; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + cursor: move; + &:hover { + background: ${(props) => props.theme.colors.primary[5]}; + & svg { + color: ${(props) => props.theme.colors.primary[2]}; + } + } +`; + +const IconContainer = styled.div` + color: ${(props) => props.theme.colors.gray[4]}; +`; + +const CircleImage = styled.img` + height: 11px; + width: 11px; + border-radius: 50%; + display: inline-block; + margin-top: ${(props) => props.theme.layout.gutter / 4}px; +`; + +const LabelRow = styled.div` + width: 100%; + display: block; +`; + +const measurementSource = { + beginDrag(props) { + return { + gid: props.gid, + itemKey: props.itemKey, + intakeIndex: props.intakeIndex, + onAddMeasurement: props.onAddMeasurement, + }; + }, +}; + +const collect = (connect, monitor) => { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }; +}; + +export default ChemicalsListItem; diff --git a/browser/components/dashboardshell/DashboardContent.js b/browser/components/dashboardshell/DashboardContent.js new file mode 100644 index 0000000..281bbcc --- /dev/null +++ b/browser/components/dashboardshell/DashboardContent.js @@ -0,0 +1,743 @@ +import { useState, useEffect, useContext, useCallback } from "react"; +import { connect } from "react-redux"; +import styled from "styled-components"; +import { hexToRgbA } from "../../helpers/colors"; +import Grid from "@material-ui/core/Grid"; +import { DarkTheme } from "../../themes/DarkTheme"; +import ButtonGroup from "../shared/button/ButtonGroup"; +import Button from "../shared/button/Button"; +import { Variants } from "../shared/constants/variants"; +import { Align } from "../shared/constants/align"; +import arrayMove from "array-move"; +import SortableList from "../shared/list/SortableList"; +import Icon from "../shared/icons/Icon"; +import ChemicalsListItem from "./ChemicalsListItem"; +import GraphCard from "./GraphCard"; +import ChemicalSelector from "./ChemicalSelector"; +import ProjectContext from "../../contexts/project/ProjectContext"; +import ProjectList from "../dataselector/ProjectList"; +import MapDecorator from "../decorators/MapDecorator"; +import { getNewPlotId } from "../../helpers/common"; +import Collapse from "@material-ui/core/Collapse"; +import ExpandLess from "@material-ui/icons/ExpandLess"; +import ExpandMore from "@material-ui/icons/ExpandMore"; +import Title from "../shared/title/Title"; +import Searchbox from "../shared/inputs/Searchbox"; +import reduxStore from "../../redux/store"; +import { addBoreholeFeature } from "../../redux/actions"; +import Backdrop from "@material-ui/core/Backdrop"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import _ from "lodash"; +import usePrevious from "../shared/hooks/usePrevious"; + +const DASHBOARD_ITEM_PLOT = 0; +const DASHBOARD_ITEM_PROFILE = 1; +const session = require("./../../../../session/browser/index"); + +function DashboardContent(props) { + const [selectedBoreholeIndex, setSelectedBoreholeIndex] = useState(0); + const [selectedBorehole, setSelectedBorehole] = useState(null); + const [dashboardItems, setDashboardItems] = useState([]); + const [groups, setGroups] = useState([]); + const [myStations, setMyStations] = useState([]); + const projectContext = useContext(ProjectContext); + const [searchTerm, setSearchTerm] = useState(""); + const [loadingData, setLoadingData] = useState(false); + const [filteredMystations, setFilteredMystations] = useState([]); + const [filteredBorehole, setFilteredBorehole] = useState( + props.boreholeFeatures.map((item, index) => { + return { ...item, index: index }; + }) + ); + + const prevCount = usePrevious(props.boreholeFeatures.length); + + const deleteFromDashboard = (index) => { + const newBoreholes = props.boreholeFeatures; + newBoreholes.splice(index, 1); + reduxStore.dispatch(clearBoreholeFeatures()); + newBoreholes.forEach((feature) => { + reduxStore.dispatch(addBoreholeFeature(feature)); + }); + }; + + const [open, setOpen] = useState({}); + const handleClick = (group) => { + return () => + setOpen((prev_state) => { + return { ...prev_state, [group]: !prev_state[group] }; + }); + }; + + const handlePlotSort = ({ oldIndex, newIndex }) => { + let allPlots = arrayMove(props.getAllPlots(), oldIndex, newIndex); + let activePlots = projectContext.activePlots; + activePlots = arrayMove( + activePlots.map((plot) => plot.id), + oldIndex, + newIndex + ); + props.setPlots(allPlots, activePlots); + // props.onActivePlotsChange(activePlots, allPlots, projectContext); + setDashboardItems(arrayMove(dashboardItems, oldIndex, newIndex)); + }; + + const handleRemoveProfile = (key) => { + let activeProfiles = projectContext.activeProfiles; + activeProfiles = activeProfiles.filter((profile) => profile.key !== key); + activeProfiles = activeProfiles.map((profile) => profile.key); + + props.setProfiles(props.getAllProfiles(), activeProfiles); + }; + + const handleRemovePlot = (id) => { + let activePlots = props.activePlots; + let allPlots = props.getAllPlots(); + activePlots = activePlots.filter((plot) => plot.id !== id); + allPlots = allPlots.filter((plot) => plot.id !== id); + activePlots = activePlots.map((plot) => plot.id); + // props.onActivePlotsChange(activePlots, allPlots, projectContext); + props.setPlots(allPlots, activePlots); + }; + + const handleDrop = (id, item) => { + setLoadingData(true); + let plot = props.getAllPlots().filter((p) => { + if (p.id === id) return true; + })[0]; + $.ajax({ + url: `/api/sql/jupiter?q=SELECT * FROM ${item.feature.relation} WHERE loc_id='${item.feature.loc_id}'&base64=false&lifetime=60&srs=4326`, + method: "GET", + dataType: "json", + }).then( + (response) => { + let ts_ids = []; + let item_keys = []; + if (!Array.isArray(item.intakeIndex)) { + ts_ids.push(item.intakeIndex); + item_keys.push(item.itemKey); + } else { + ts_ids = item.intakeIndex; + item_keys = item.itemKey; + } + ts_ids.forEach((ts_id, index) => { + const itidx = item.feature.ts_id.indexOf(ts_id); + let measurementsData = { + data: { + properties: { + _0: JSON.stringify({ + unit: item.feature.unit[itidx], + title: item.feature.ts_name[itidx], + locname: item.feature.locname, + intakes: [1], + boreholeno: item.feature.loc_id, + data: response.features[0].properties.data, + trace: item.feature.trace, + relation: item.feature.relation, + parameter: item.feature.parameter[itidx], + ts_id: item.feature.ts_id, + ts_name: item.feature.ts_name, + }), + boreholeno: item.feature.loc_id, + numofintakes: 1, + }, + }, + }; + item.onAddMeasurement( + plot.id, + item.gid, + item_keys[index], + ts_id, + measurementsData, + item.feature.relation + ); + }); + + setLoadingData(false); + }, + (jqXHR) => { + console.error(`Error occured while getting data`); + } + ); + }; + + useEffect(() => { + window.Calypso = { + render: (id, popupType, trace, data) => { + data.properties.relation = popupType; // add relation name to feature properties + ReactDOM.render( + <ThemeProvider> + <MapDecorator + trace={trace} + data={data} + getAllPlots={props.getAllPlots} + setPlots={props.setPlots} + relation={popupType} + onActivePlotsChange={props.onActivePlotsChange} + setDashboardMode={props.setDashboardMode} + setLoadingData={setLoadingData} + /> + </ThemeProvider>, + document.getElementById(`pop_up_${id}`) + ); + }, + }; + }); + + useEffect(() => { + const dashboardItemsCopy = []; + + props.getDashboardItems().map((item, index) => { + if (item.type === DASHBOARD_ITEM_PROFILE) { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PROFILE, + item: { ...item.item, title: item.item.profile.title }, + plotsIndex: index, + }); + } else { + dashboardItemsCopy.push({ + type: DASHBOARD_ITEM_PLOT, + item: item.item, + plotsIndex: index, + }); + } + }); + setDashboardItems(dashboardItemsCopy); + }, [props.activePlots, props.activeProfiles]); + + const get_my_stations = () => { + if (session.getProperties()?.organisation.id && session.getUserName()) { + $.ajax({ + url: `/api/sql/watsonc?q=SELECT loc_id, locname, groupname, relation FROM calypso_stationer.calypso_my_stations_v2 WHERE user_id in (${ + session.getProperties()?.organisation.id + }, ${session.getUserName()}) &base64=false&lifetime=60&srs=4326`, + method: "GET", + dataType: "json", + }).then((response) => { + function getArray(object) { + return Object.keys(object).reduce(function (r, k) { + object[k].forEach(function (a, i) { + r[i] = r[i] || {}; + r[i][k] = a; + }); + return r; + }, []); + } + var features = response.features; + var myStations = []; + + features.forEach((element) => { + myStations = myStations.concat(getArray(element.properties)); + }); + + myStations = _.uniqWith(myStations, _.isEqual); + + myStations = myStations.sort((a, b) => + a.locname.localeCompare(b.locname) + ); + + setMyStations( + myStations.map((elem) => { + return { properties: elem }; + }) + ); + }); + } else { + setMyStations([]); + } + }; + + useEffect(() => { + get_my_stations(); + props.backboneEvents.get().on("refresh:meta", () => get_my_stations()); + }, []); + + useEffect(() => { + setOpen( + groups.map((elem) => { + return { [elem]: false }; + }) + ); + }, [groups]); + + useEffect(() => { + const filt = myStations + .map((item, index) => { + return { ...item, index: index }; + }) + .filter((elem) => + elem.properties.locname.toLowerCase().includes(searchTerm.toLowerCase()) + ); + setFilteredMystations(filt); + const grp = filt + .map((item) => item.properties.groupname) + .sort(function (a, b) { + // equal items sort equally + if (a === b) { + return 0; + } + // nulls sort after anything else + else if (a === null) { + return 1; + } else if (b === null) { + return -1; + } + // otherwise, if we're ascending, lowest sorts first + else { + return a < b ? -1 : 1; + } + }); + + setGroups([...new Set(grp)]); + setFilteredBorehole( + props.boreholeFeatures + .map((item, index) => { + return { ...item, index: index }; + }) + .filter((elem) => + elem.properties.locname + .toLowerCase() + .includes(searchTerm.toLowerCase()) + ) + ); + }, [searchTerm, props.boreholeFeatures, myStations]); + + useEffect(() => { + if (selectedBoreholeIndex === null) { + setSelectedBorehole(null); + } + if (selectedBoreholeIndex >= props.boreholeFeatures.length) { + setSelectedBorehole( + myStations[selectedBoreholeIndex - props.boreholeFeatures.length] + ); + } else { + setSelectedBorehole(props.boreholeFeatures[selectedBoreholeIndex]); + } + props.onPlotsChange(); + }, [selectedBoreholeIndex]); + + useEffect(() => { + if (props.boreholeFeatures.length > prevCount) { + setSelectedBoreholeIndex(props.boreholeFeatures.length - 1); + } + if (props.boreholeFeatures.length === 1) { + setSelectedBoreholeIndex(0); + } + if (props.boreholeFeatures.length === 0) { + setSelectedBoreholeIndex(null); + } + props.onPlotsChange(); + }, [props.boreholeFeatures]); + + const handleTitleChange = (id) => { + return (title) => { + var allPlots = props.getAllPlots(); + const index = allPlots.findIndex((plot) => plot.id === id); + + if (index < 0) { + return; + } + + allPlots[index] = { + ...allPlots[index], + title: title, + }; + let activePlots = allPlots.map((plot) => plot.id); + props.setPlots(allPlots, activePlots); + }; + }; + + return ( + <Root> + {props.dashboardContent === "charts" ? ( + <Grid container style={{ height: "100%" }}> + <Grid item xs={4} style={{ height: "100%" }}> + <DashboardList> + <Grid container style={{ height: "100%" }}> + <Grid + container + item + xs={5} + style={{ height: "100%", overflow: "auto" }} + > + <BoreholesList> + <SearchboxContainer> + <Searchbox + placeholder={__("Søg efter datakilder")} + onChange={(value) => setSearchTerm(value)} + /> + </SearchboxContainer> + <DashboardListTitle key={"selected_data_sourced"}> + <Icon + name="pin-location-solid" + size={16} + strokeColor={DarkTheme.colors.headings} + /> + <Title + level={4} + color={DarkTheme.colors.headings} + text={__("Valgte datakilder")} + marginLeft={8} + /> + </DashboardListTitle> + {filteredBorehole + ? filteredBorehole.map((item, index) => { + let name = item.properties.locname; + index = item.index; + let id = item.properties.loc_id + "_" + index; + const isJupiter = + item.properties.relation.includes("._"); + return ( + <DashboardListItem + onClick={() => setSelectedBoreholeIndex(index)} + active={selectedBoreholeIndex === index} + key={id} + > + <Icon + name={isJupiter ? "drill" : "water-wifi-solid"} + size={16} + strokeColor={ + isJupiter + ? DarkTheme.colors.interaction[4] + : DarkTheme.colors.headings + } + onClick={() => + isJupiter + ? window.open( + `https://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${name.replace( + /\s+/g, + "" + )}`, + "_blank", + "noopener,noreferrer" + ) + : null + } + /> + <Title + level={6} + text={name} + marginLeft={8} + width="70%" + /> + <RemoveIconContainer + onClick={() => deleteFromDashboard(index)} + > + <Icon + name="cross" + size={16} + strokeColor={DarkTheme.colors.headings} + /> + </RemoveIconContainer> + </DashboardListItem> + ); + }) + : null} + <DashboardListTitle key={"user_data_sourced"}> + <Icon + name="avatar" + size={16} + strokeColor={DarkTheme.colors.headings} + /> + <Title + level={4} + color={DarkTheme.colors.headings} + text={__("Mine stationer")} + marginLeft={8} + /> + </DashboardListTitle> + + {groups.map((group) => { + return ( + <div key={group}> + {group !== null ? ( + <div> + <DashboardListItem + onClick={handleClick(group)} + key={group + "1"} + > + {open[group] ? <ExpandLess /> : <ExpandMore />} + <Title level={5} text={group} marginLeft={4} /> + </DashboardListItem> + <Collapse + in={open[group]} + timeout="auto" + unmountOnExit + > + {filteredMystations.map((item, index) => { + let name = item.properties.locname; + index = item.index; + return item.properties.groupname === group ? ( + <DashboardListItem + onClick={() => + setSelectedBoreholeIndex( + index + props.boreholeFeatures.length + ) + } + active={ + selectedBoreholeIndex === + index + props.boreholeFeatures.length + } + key={item.properties.loc_id} + > + <Icon + name="water-wifi-solid" + size={16} + strokeColor={DarkTheme.colors.headings} + paddingLeft={16} + /> + <Title + level={6} + text={name} + marginLeft={16} + /> + </DashboardListItem> + ) : null; + })} + </Collapse> + </div> + ) : ( + <div> + {filteredMystations.map((item, index) => { + let name = item.properties.locname; + index = item.index; + return item.properties.groupname === group ? ( + <DashboardListItem + onClick={() => + setSelectedBoreholeIndex( + index + props.boreholeFeatures.length + ) + } + active={ + selectedBoreholeIndex === + index + props.boreholeFeatures.length + } + key={item.properties.loc_id} + > + <Icon + name="water-wifi-solid" + size={16} + strokeColor={DarkTheme.colors.headings} + // paddingLeft={16} + /> + <Title + level={6} + text={name} + marginLeft={8} + /> + </DashboardListItem> + ) : null; + })} + </div> + )} + </div> + ); + })} + </BoreholesList> + </Grid> + <Grid + container + item + xs={7} + style={{ + height: "100%", + overflow: "auto", + background: DarkTheme.colors.primary[2], + }} + > + <ChemicalSelector + feature={selectedBorehole} + limits={limits} + categories={props.categories} + onAddMeasurement={props.onAddMeasurement} + backboneEvents={props.backboneEvents} + /> + </Grid> + </Grid> + </DashboardList> + </Grid> + <Grid + container + item + xs={8} + style={{ height: "100%", overflow: "auto" }} + id="chartsContainer" + > + <ChartsContainer> + {loadingData && ( + <div + style={{ + display: "flex", + justifyContent: "center", + left: "50%", + top: "50%", + position: "absolute", + zIndex: 9000, + }} + > + <CircularProgress + style={{ color: DarkTheme.colors.interaction[4] }} + /> + </div> + )} + <SortableList axis="xy" onSortEnd={handlePlotSort} useDragHandle> + {dashboardItems.map((dashboardItem, index) => { + let id = dashboardItem.item.id; + if (dashboardItem.type === DASHBOARD_ITEM_PLOT) { + return ( + <GraphCard + plot={dashboardItem.item} + index={index} + order={index} + getDashboardItems={props.getDashboardItems} + setItems={props.setItems} + key={id} + id={id} + onDeleteMeasurement={props.onDeleteMeasurement} + cardType="plot" + onRemove={() => handleRemovePlot(id)} + onDrop={(item) => handleDrop(id, item)} + onChange={handleTitleChange(dashboardItem.item.id)} + /> + ); + } else if (dashboardItem.type === DASHBOARD_ITEM_PROFILE) { + return ( + <GraphCard + plot={dashboardItem.item} + cloud={props.cloud} + index={index} + order={index} + key={dashboardItem.item.key} + cardType="profile" + onRemove={() => + handleRemoveProfile(dashboardItem.item.key) + } + onChange={handleTitleChange(index)} + /> + ); + } + })} + </SortableList> + </ChartsContainer> + </Grid> + </Grid> + ) : props.dashboardContent === "projects" ? ( + <Grid container> + <Grid container item xs={6}> + <ProjectsContainer> + <ProjectList {...props} showBackButton={true} /> + </ProjectsContainer> + </Grid> + </Grid> + ) : null} + </Root> + ); +} + +const Root = styled.div` + height: calc(100% - ${(props) => props.theme.layout.gutter * 2}px); + width: 100%; + background-color: ${(props) => + hexToRgbA(props.theme.colors.primary[1], 0.92)}; + // overflow-y: auto; +`; + +const DashboardList = styled.div` + background-color: ${(props) => props.theme.colors.primary[1]}; + padding: ${(props) => props.theme.layout.gutter / 2}px + ${(props) => props.theme.layout.gutter}px; + width: 100%; + height: 100%; + // overflow-y: auto; +`; + +const SearchboxContainer = styled.div` + width: 90%; + padding: ${(props) => props.theme.layout.gutter / 2}px 0px; +`; + +const BoreholesList = styled.div` + width: 100%; + height: 100%; + // overflow-y: auto; +`; + +const DashboardListTitle = styled.div` + margin-top: ${(props) => props.theme.layout.gutter / 4}px; + width: 100%; + color: ${(props) => props.theme.colors.headings}; +`; + +const SelectedList = styled.div` + display: flex; + flex-direction: row; +`; + +const RemoveIconContainer = styled.div` + width: 18px; + height: 18px; + margin-top: 3px; + margin-right: 5px; + border: 1px solid ${(props) => props.theme.colors.gray[2]}; + border-radius: 50%; + display: none; + cursor: pointer; + float: right; +`; + +const DashboardListItem = styled.div` + margin-top: ${(props) => props.theme.layout.gutter / 8}px; + margin-bottom: ${(props) => props.theme.layout.gutter / 8}px; + height: ${(props) => props.theme.layout.gutter}px; + padding: ${(props) => props.theme.layout.gutter / 8}px 0px; + width: 100%; + color: ${(props) => + props.active ? props.theme.colors.headings : props.theme.colors.gray[4]}; + background-color: ${(props) => + props.active ? props.theme.colors.primary[2] : "transparent"}; + cursor: pointer; + display: inline-block; + + &:hover { + background-color: ${(props) => props.theme.colors.primary[2]}; + color: ${(props) => props.theme.colors.headings}; + + ${RemoveIconContainer} { + display: block; + } + } +`; + +const FavoritterList = styled.div` + margin-top: ${(props) => props.theme.layout.gutter}px; + height: auto; +`; + +const FavoritterListTitle = styled.div` + color: ${(props) => props.theme.colors.primary[5]}; + margin-top: ${(props) => props.theme.layout.gutter / 2}px; +`; + +const ChartsContainer = styled.ul` + padding-top: 0px; + padding-bottom: 0px; + width: 100%; + padding-left: ${(props) => props.theme.layout.gutter / 4}px; + padding-right: ${(props) => props.theme.layout.gutter / 4}px; + height: 100%; + position: relative; +`; + +const ProjectsContainer = styled.div` + padding: ${(props) => props.theme.layout.gutter / 2}px; + width: 80%; +`; + +const mapStateToProps = (state) => ({ + boreholeFeatures: state.global.boreholeFeatures, + dashboardContent: state.global.dashboardContent, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardContent); diff --git a/browser/components/dashboardshell/DashboardHeader.js b/browser/components/dashboardshell/DashboardHeader.js new file mode 100644 index 0000000..16553b5 --- /dev/null +++ b/browser/components/dashboardshell/DashboardHeader.js @@ -0,0 +1,332 @@ +import { useContext, useState, useEffect } from "react"; +import { connect } from "react-redux"; +import styled, { css } from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import Icon from "../shared/icons/Icon"; +import Title from "../shared/title/Title"; +import { Align } from "../shared/constants/align"; +import { Variants } from "../shared/constants/variants"; +import { Size } from "../shared/constants/size"; +import { + showSubscription, + showSubscriptionIfFree, +} from "./../../helpers/show_subscriptionDialogue"; +import Button from "../shared/button/Button"; +import ButtonGroup from "../shared/button/ButtonGroup"; +import { DarkTheme } from "../../themes/DarkTheme"; +import ProjectContext from "../../contexts/project/ProjectContext"; +import { getNewPlotId } from "../../helpers/common"; +import base64url from "base64url"; +import reduxStore from "../../redux/store"; +import { clearBoreholeFeatures } from "../../redux/actions"; + +const session = require("./../../../../session/browser/index"); + +function DashboardHeader(props) { + const [showSaveButtons, setShowSaveButtons] = useState(true); + const [dashboardTitle, setDashboardTitle] = useState(null); + const [dashboardId, setDashboardId] = useState(null); + const [saving, setSaving] = useState(false); + const projectContext = useContext(ProjectContext); + + useEffect(() => { + let canShowSaveButtons = true; + if ( + props.dashboardMode === "minimized" || + props.dashboardContent === "projects" + ) { + canShowSaveButtons = false; + } + setShowSaveButtons(canShowSaveButtons); + }, [props.dashboardMode, props.dashboardContent]); + + useEffect(() => { + props.backboneEvents.get().on("statesnapshot:apply", (snapshot) => { + setDashboardTitle(snapshot.title); + setDashboardId(snapshot.id); + }); + }, []); + + const addNewPlot = () => { + let allPlots = props.getAllPlots(); + + if (showSubscriptionIfFree(allPlots.length > 0)) return; + + if (props.dashboardMode === "minimized") { + props.setDashboardMode("half"); + } + + document.getElementById("chartsContainer").scrollTop = 0; + + let activePlots = projectContext.activePlots; + let newPlotId = getNewPlotId(allPlots); + let plotData = { + id: `plot_${newPlotId}`, + title: `Graf ${newPlotId}`, + measurements: [], + measurementsCachedData: {}, + relations: {}, + }; + activePlots.unshift(plotData); + allPlots.unshift(plotData); + activePlots = activePlots.map((plot) => plot.id); + props.setPlots(allPlots, activePlots); + // props.onActivePlotsChange(activePlots, allPlots, projectContext); + }; + + const save = () => { + if (showSubscriptionIfFree()) return; + + let title; + if (!dashboardTitle) { + title = prompt("Navn på dashboard"); + if (title) { + createSnapshot(title); + } + } else { + updateSnapShot(); + } + }; + + const saveAs = () => { + if (showSubscriptionIfFree()) return; + let title = prompt("Navn på dashboard"); + if (title) { + createSnapshot(title); + } + }; + + const clearDashboard = () => { + if (confirm("Er du sikker på, at du vil fjerne alt fra Dashboard?")) { + reduxStore.dispatch(clearBoreholeFeatures()); + props.setItems([]); + props.backboneEvents.get().trigger("watsonc:clearChemicalList"); + } + }; + + const createSnapshot = (title) => { + setSaving(true); + props.state.getState().then((state) => { + state.map = props.anchor.getCurrentMapParameters(); + state.meta = getSnapshotMeta(); + let data = { + title: title, + anonymous: false, + snapshot: state, + database: vidiConfig.appDatabase, + schema: vidiConfig.appSchema, + host: props.urlparser.hostname, + }; + $.ajax({ + url: `/api/state-snapshots` + "/" + vidiConfig.appDatabase, + method: "POST", + contentType: "text/plain; charset=utf-8", + dataType: "text", + data: base64url(JSON.stringify(data)), + }) + .then((response) => { + props.backboneEvents.get().trigger("statesnapshot:refresh"); + try { + const id = JSON.parse(response).id; + setDashboardId(id); + setDashboardTitle(title); + } catch (e) { + console.error(e.message); + } + setSaving(false); + }) + .catch((error) => { + console.error(error); + setSaving(false); + }); + }); + }; + + const updateSnapShot = () => { + setSaving(true); + props.state.getState().then((state) => { + state.map = props.anchor.getCurrentMapParameters(); + state.meta = getSnapshotMeta(); + let data = { + title: dashboardTitle, + snapshot: state, + }; + $.ajax({ + url: + `/api/state-snapshots` + + "/" + + vidiConfig.appDatabase + + "/" + + dashboardId, + method: "PUT", + contentType: "text/plain; charset=utf-8", + dataType: "text", + data: base64url(JSON.stringify(data)), + }) + .then((response) => { + props.backboneEvents.get().trigger("statesnapshot:refresh"); + setSaving(false); + }) + .catch((error) => { + console.error(error); + setSaving(false); + }); + }); + }; + + const getSnapshotMeta = () => { + let result = {}; + let queryParameters = props.urlparser.urlVars; + if (`config` in queryParameters && queryParameters.config) { + result.config = queryParameters.config; + } + + if (`tmpl` in queryParameters && queryParameters.tmpl) { + result.tmpl = queryParameters.tmpl; + } + return result; + }; + + return ( + <Root> + <Grid container> + <Grid container item xs={10}> + <Icon + name="dashboard" + strokeColor={DarkTheme.colors.headings} + size={32} + /> + <Title + text={__("Dashboard")} + level={4} + color={DarkTheme.colors.headings} + marginLeft={8} + /> + <Title + text={dashboardTitle || __("Ikke gemt")} + level={5} + color={DarkTheme.colors.primary[5]} + marginLeft={8} + /> + <ButtonGroup + align={Align.Center} + spacing={2} + marginLeft={8} + marginTop={1} + marginRight={8} + variant={"contained"} + > + <Button + text={"Gem"} + onClick={() => save()} + variant={ + saving || !dashboardId + ? Variants.PrimaryDisabled + : Variants.Primary + } + disabled={saving || !dashboardId} + /> + <Button + text={"Gem\u00A0som"} + onClick={() => saveAs()} + variant={saving ? Variants.PrimaryDisabled : Variants.Primary} + disabled={saving} + /> + <Button + text={"Ryd\u00A0dashboard"} + onClick={() => clearDashboard()} + variant={saving ? Variants.PrimaryDisabled : Variants.Primary} + /> + </ButtonGroup> + </Grid> + {/* <Grid container item xs={8} justify="left"></Grid> */} + <Grid container item xs={2} justify="flex-end"> + <ButtonGroup + align={Align.Center} + spacing={2} + marginTop={1} + marginRight={8} + variant={"contained"} + > + <Button + text={"Ny graf"} + // size={Size.Medium} + onClick={() => addNewPlot()} + variant={saving ? Variants.PrimaryDisabled : Variants.Primary} + /> + </ButtonGroup> + <IconsLayout> + <IconContainer + onClick={() => props.setDashboardMode("minimized")} + active={props.dashboardMode === "minimized"} + > + <Icon name="dashboard-minimized-solid" size={16} /> + </IconContainer> + <IconContainer + onClick={() => props.setDashboardMode("half")} + active={props.dashboardMode === "half"} + > + <Icon name="dashboard-half-solid" size={16} /> + </IconContainer> + <IconContainer + onClick={() => props.setDashboardMode("full")} + active={props.dashboardMode === "full"} + > + <Icon name="dashboard-full-solid" size={16} /> + </IconContainer> + </IconsLayout> + </Grid> + </Grid> + </Root> + ); +} + +const Root = styled.div` + height: ${(props) => props.theme.layout.gutter * 2}px; + background: ${(props) => props.theme.colors.primary[2]}; + padding: ${(props) => props.theme.layout.gutter / 2}px; + border-radius: ${(props) => props.theme.layout.gutter / 2}px + ${(props) => props.theme.layout.gutter / 2}px 0 0; +`; + +const IconsLayout = styled.div` + height: 32px; + border: 1px solid ${(props) => props.theme.colors.primary[3]}; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + padding: 2px; +`; + +const IconContainer = styled.div` + display: inline-block; + height: 100%; + width: ${(props) => props.theme.layout.gutter}px; + padding-left: ${(props) => props.theme.layout.gutter / 4}px; + padding-top: ${(props) => props.theme.layout.gutter / 8}px; + cursor: pointer; + color: ${(props) => props.theme.colors.gray[3]}; + + &:hover { + background-color: ${(props) => props.theme.colors.primary[3]}; + color: ${(props) => props.theme.colors.headings}; + } + + ${({ active, theme }) => { + const styles = { + true: css` + color: ${theme.colors.interaction[4]}; + `, + }; + return styles[active]; + }} +`; + +const mapStateToProps = (state) => ({ + dashboardMode: state.global.dashboardMode, + dashboardContent: state.global.dashboardContent, +}); + +const mapDispatchToProps = (dispatch) => ({ + setDashboardMode: (key) => dispatch(setDashboardMode(key)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader); diff --git a/browser/components/dashboardshell/DashboardPlotCard.js b/browser/components/dashboardshell/DashboardPlotCard.js new file mode 100644 index 0000000..d92d3d5 --- /dev/null +++ b/browser/components/dashboardshell/DashboardPlotCard.js @@ -0,0 +1,164 @@ +import { useEffect, useState } from "react"; +import styled from "styled-components"; +import { useDrop } from "react-dnd"; +import Grid from "@material-ui/core/Grid"; +import Icon from "../shared/icons/Icon"; +import Title from "../shared/title/Title"; +import Button from "../shared/button/Button"; +import { Variants } from "../shared/constants/variants"; +import { LIMIT_CHAR } from "../../constants"; +import { Size } from "../shared/constants/size"; +import CheckBox from "../shared/inputs/CheckBox"; +import PlotComponent from "./PlotComponent"; +import CardListItem from "./CardListItem"; + +const utils = require("../../utils"); + +function DashboardPlotCard(props) { + const [plotData, setPlotData] = useState([]); + const [yAxis2LayoutSettings, setYAxis2LayoutSettings] = useState(null); + const [aggregate, setAggregate] = useState(props.plot.aggregate || []); + const [collectedProps, drop] = useDrop(() => ({ + accept: "MEASUREMENT", + drop: props.onDrop, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + })); + const handleSetAggregate = (idx, window, func) => { + let ind = aggregate.findIndex((elem) => elem.idx === idx); + if (ind === -1) { + setAggregate((prev) => { + prev.push({ idx: idx, window: window, func: func }); + return [...prev]; + }); + } else { + setAggregate((prev) => { + prev[ind] = { idx: idx, window: window, func: func }; + return [...prev]; + }); + } + + const newDashboardItems = props.getDashboardItems().map((elem) => { + if (elem.item.id === props.plot.id) { + elem.item.aggregate = aggregate; + } + return elem; + }); + + props.setItems(newDashboardItems.map((elem) => elem.item)); + }; + + const handleDeleteAggregate = (idx) => { + let ind = aggregate.findIndex((elem) => elem.idx === idx); + if (ind === -1) { + return; + } else { + setAggregate((prev) => { + prev.splice(ind, 1); + return [...prev]; + }); + } + }; + + useEffect(() => { + let data = []; + if ( + props.plot && + props.plot.measurements && + props.plot.measurements.length > 0 + ) { + props.plot.measurements.map((measurementLocationRaw, index) => { + if ( + props.plot.measurementsCachedData && + measurementLocationRaw in props.plot.measurementsCachedData && + props.plot.measurementsCachedData[measurementLocationRaw] && + props.plot.measurementsCachedData[measurementLocationRaw].data + ) { + let measurementLocation = measurementLocationRaw.split(":"); + let feature = + props.plot.measurementsCachedData[measurementLocationRaw].data; + let key = measurementLocation[1]; + let measurementData = JSON.parse(feature.properties[key]); + let intakeIndex = measurementData.ts_id + .map((elem) => elem.toString()) + .indexOf(measurementLocation[2]); + // Merge trace and data + const plotInfoMergedWithTrace = { + ...measurementData.data[intakeIndex], + ...measurementData.trace[intakeIndex], + }; + data.push(plotInfoMergedWithTrace); + } else { + console.info( + `Plot does not contain measurement ${measurementLocationRaw}` + ); + } + }); + } + setPlotData(data); + setYAxis2LayoutSettings(yAxis2LayoutSettings); + }, [props.plot]); + + return ( + <DashboardPlotContent + height={props.fullscreen ? "90vh" : "100%"} + ref={drop} + > + <Grid container style={{ height: "100%" }}> + <Grid container item xs={2}> + <CardList> + {props.plot?.measurements?.map((measurement, index) => { + return ( + <CardListItem + measurement={measurement} + plot={props.plot} + onDeleteMeasurement={props.onDeleteMeasurement} + getDashboardItems={props.getDashboardItems} + setItems={props.setItems} + key={measurement} + aggregate={aggregate.find((elem) => elem.idx === measurement)} + setAggregate={handleSetAggregate} + deleteAggregate={handleDeleteAggregate} + /> + ); + })} + </CardList> + </Grid> + <Grid container item xs={10}> + <PlotContainer> + <PlotComponent + viewMode={0} + height={props.fullscreen ? "100%" : 370} + index={props.index} + onDelete={() => console.log("Testing")} + plotMeta={props.plot} + plotData={plotData} + aggregate={aggregate} + yAxis2LayoutSettings={yAxis2LayoutSettings} + /> + </PlotContainer> + </Grid> + </Grid> + </DashboardPlotContent> + ); +} + +const DashboardPlotContent = styled.div` + padding: ${(props) => props.theme.layout.gutter / 2}px; + height: ${(props) => props.height}; +`; + +const CardList = styled.div` + height: 100%; + width: 95%; + vertical-align: middle; +`; + +const PlotContainer = styled.div` + width: 100%; + margin-left: 10px; +`; + +export default DashboardPlotCard; diff --git a/browser/components/dashboardshell/DashboardProfileCard.js b/browser/components/dashboardshell/DashboardProfileCard.js new file mode 100644 index 0000000..a3b928f --- /dev/null +++ b/browser/components/dashboardshell/DashboardProfileCard.js @@ -0,0 +1,43 @@ +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import ProfileComponent from "./ProfileComponent"; + +function DashboardProfileCard(props) { + return ( + <DashboardPlotContent height={props.fullscreen ? "90vh" : "100%"}> + <Grid container style={{ height: "100%" }}> + <Grid container item xs={12}> + <PlotContainer> + <ProfileComponent + height={props.fullscreen ? "100%" : 370} + index={props.index} + onClick={() => console.log("Testing")} + plotMeta={props.plot} + onChangeDatatype={(id) => { + console.log("Test"); + }} + /> + </PlotContainer> + </Grid> + </Grid> + </DashboardPlotContent> + ); +} + +const DashboardPlotContent = styled.div` + padding: ${(props) => props.theme.layout.gutter / 2}px; + height: ${(props) => props.height}; +`; + +const CardList = styled.div` + height: 100%; + width: 95%; + vertical-align: middle; +`; + +const PlotContainer = styled.div` + width: 100%; + margin-left: 10px; +`; + +export default DashboardProfileCard; diff --git a/browser/components/dashboardshell/DashboardShell.js b/browser/components/dashboardshell/DashboardShell.js new file mode 100644 index 0000000..1e36c98 --- /dev/null +++ b/browser/components/dashboardshell/DashboardShell.js @@ -0,0 +1,51 @@ +import { useState, useContext } from 'react'; +import {connect} from 'react-redux'; +import styled, { css } from 'styled-components'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import DashboardHeader from './DashboardHeader'; +import DashboardContent from './DashboardContent'; +import ProjectContext from '../../contexts/project/ProjectContext'; + +function DashboardShell(props) { + return (<DndProvider backend={HTML5Backend}> + <ProjectContext.Consumer> + {context => { + return <Root mode={props.dashboardMode}> + <DashboardHeader {...props} {...context} setActivePlots={context.setActivePlots} /> + <DashboardContent {...props} {...context} /> + </Root> + }} + </ProjectContext.Consumer> + </DndProvider>) +} + +const Root = styled.div` + width: 100%; + height: 75vh; + transition: height 0.5s linear; + ${({ mode, theme }) => { + const styles = { + full: css ` + height: 75vh; + `, + half: css ` + height: 50vh; + `, + minimized: css ` + height: ${theme.layout.gutter*2}px; + ` + } + return styles[mode]; + }} +` + +const mapStateToProps = state => ({ + dashboardMode: state.global.dashboardMode +}) + +const mapDispatchToProps = dispatch => ({ + setDashboardMode: (key) => dispatch(setDashboardMode(key)), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardShell); diff --git a/browser/components/dashboardshell/GraphCard.js b/browser/components/dashboardshell/GraphCard.js new file mode 100644 index 0000000..e4c282a --- /dev/null +++ b/browser/components/dashboardshell/GraphCard.js @@ -0,0 +1,335 @@ +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import Icon from "../shared/icons/Icon"; +import Title from "../shared/title/Title"; +import DashboardPlotCard from "./DashboardPlotCard"; +import DashboardProfileCard from "./DashboardProfileCard"; +import { sortableElement } from "react-sortable-hoc"; +import SortHandleComponent from "./SortHandleComponent"; +import PlotApi from "../../api/plots/PlotApi"; +import { useState, useEffect } from "react"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import moment from "moment"; + +const utmZone = require("./../../../../../browser/modules/utmZone"); +let displayedItems = new L.FeatureGroup(); + +function GraphCard(props) { + const [fullscreen, setFullscreen] = useState(false); + const [graphName, setGraphName] = useState( + props.cardType === "profile" ? props.plot.profile.title : props.plot.title + ); + const [profileShown, setProfileShown] = useState(false); + + useEffect(() => { + if (props.cardType === "profile") { + props.cloud.get().map.addLayer(displayedItems); + } + + return () => { + displayedItems.eachLayer((layer) => { + displayedItems.removeLayer(layer); + }); + }; + }, []); + + const download = () => { + const regexExp = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; + if (!props.plot) { + return; + } + let data = []; + props.plot.measurements.map((measurementLocationRaw, index) => { + if ( + measurementLocationRaw in props.plot.measurementsCachedData && + props.plot.measurementsCachedData[measurementLocationRaw] + ) { + let measurementLocation = measurementLocationRaw.split(":"); + if (measurementLocation.length === 3) { + let key = measurementLocation[1]; + let ts_id = regexExp.test(measurementLocation[2]) + ? measurementLocation[2] + : parseInt(measurementLocation[2]); + let feature = + props.plot.measurementsCachedData[measurementLocationRaw].data; + let measurementData = JSON.parse(feature.properties[key]); + let index = measurementData.ts_id.indexOf(ts_id); + + var x = []; + var y = []; + measurementData.data[index].y.map((elem, idx) => { + if (elem != null) { + x.push( + moment(measurementData.data[index].x[idx]).format( + "YYYY-MM-DD HH:mm:ss" + ) + ); + y.push(elem.toString().replace(".", ",")); + } + }); + + // let formatedDates = measurementData.data[index].x.map((elem) => + // moment(elem).format("YYYY-MM-DD HH:mm:ss") + // ); + data.push({ + title: measurementData.title, + unit: measurementData.unit, + name: measurementData.data[index].name, + x: x, + y: y, + }); + } + } else { + console.error( + `Plot does not contain measurement ${measurementLocationRaw}` + ); + } + }); + const plotApi = new PlotApi(); + + plotApi + .downloadPlot({ + title: props.plot.title, + data, + }) + .then((response) => { + const filename = + props.plot.title.replace(/\s+/g, "_").toLowerCase() + ".xlsx"; + const url = window.URL.createObjectURL( + new Blob([response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }) + ); + + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + document.body.appendChild(link); + link.click(); + window.URL.revokeObjectURL(url); + }) + .catch((error) => { + console.error(error); + alert(`Error occured while generating plot XSLS file`); + }); + }; + + const toggle_profile = () => { + if (!profileShown) { + let data = props.plot; + let profile = data.profile.profile; + + // Get utm zone + var zone = utmZone.getZone( + profile.geometry.coordinates[0][1], + profile.geometry.coordinates[0][0] + ); + var crss = { + proj: + "+proj=utm +zone=" + + zone + + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs", + unproj: "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs", + }; + + let reader = new jsts.io.GeoJSONReader(); + let writer = new jsts.io.GeoJSONWriter(); + let geom = reader.read( + reproject.reproject(profile, "unproj", "proj", crss) + ); + let buffer4326 = reproject.reproject( + writer.write(geom.geometry.buffer(data.profile.buffer)), + "proj", + "unproj", + crss + ); + + L.geoJson(buffer4326, { + color: "#ff7800", + weight: 1, + opacity: 1, + fillOpacity: 0.1, + dashArray: "5,3", + }).addTo(displayedItems); + + var profileLayer = new L.geoJSON(profile); + + profileLayer.bindTooltip(data.profile.title, { + className: "watsonc-profile-tooltip", + permanent: true, + offset: [0, 0], + }); + + profileLayer.addTo(displayedItems); + setProfileShown(true); + } else { + displayedItems.eachLayer((layer) => { + displayedItems.removeLayer(layer); + }); + setProfileShown(false); + } + }; + + const handleFullScreen = () => { + setFullscreen(true); + }; + + const handleClose = () => { + setFullscreen(false); + }; + + return ( + <li> + <Root> + <DashboardPlotHeader> + <Grid container> + <Grid container item xs={3}> + <HeaderActionItem> + <HeaderSvg> + <Icon name="analytics-board-graph-line" size={16} /> + </HeaderSvg> + {/* <Title level={5} text={props.plot.title} marginLeft={4} /> */} + <Input + value={graphName} + onChange={(e) => setGraphName(e.target.value)} + onBlur={(e) => props.onChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.target.blur(); + } + }} + disabled={props.cardType === "plot" ? false : true} + /> + </HeaderActionItem> + </Grid> + <Grid container item xs={2}> + {/*<Button*/} + {/* onClick={() => {}}*/} + {/* text={__("Gem")}*/} + {/*/>*/} + </Grid> + <Grid container item xs={7} justify="flex-end"> + {props.cardType === "plot" ? ( + <HeaderActionItem onClick={download}> + <IconContainer> + <Icon name="arrow-down" size={16} /> + </IconContainer> + <Title marginLeft={8} level={6} text={__("Download")} /> + </HeaderActionItem> + ) : ( + <HeaderActionItem onClick={toggle_profile}> + <IconContainer> + <Icon name="earth-layers" size={16} /> + </IconContainer> + <Title marginLeft={8} level={6} text={__("Vis profil")} /> + </HeaderActionItem> + )} + <SortHandleComponent /> + <HeaderActionItem onClick={handleFullScreen}> + <IconContainer> + <Icon name="full-screen" size={16} /> + </IconContainer> + <Title marginLeft={8} level={6} text={__("Fuld skærm")} /> + </HeaderActionItem> + <CloseButton onClick={props.onRemove}> + <Icon name="cross" size={24} /> + </CloseButton> + </Grid> + </Grid> + </DashboardPlotHeader> + {props.cardType === "plot" ? ( + <DashboardPlotCard {...props} /> + ) : ( + <DashboardProfileCard {...props} /> + )} + </Root> + <Dialog + PaperProps={{ sx: { maxHeight: "90vh" } }} + fullWidth={true} + maxWidth={false} + open={fullscreen} + onClose={handleClose} + > + <DialogActions> + <Button onClick={handleClose} style={{ border: "1px solid" }}> + <Icon name="cross" size={24} /> + </Button> + </DialogActions> + {props.cardType === "plot" ? ( + <DashboardPlotCard {...{ ...props, fullscreen: true }} /> + ) : ( + <DashboardProfileCard {...{ ...props, fullscreen: true }} /> + )} + </Dialog> + </li> + ); +} + +const Root = styled.div` + background: ${(props) => props.theme.colors.gray[5]}; + margin-top: ${(props) => props.theme.layout.gutter / 4}px; + border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; + height: ${(props) => props.theme.layout.gutter * 12.5}px; + width: 100%; +`; + +const DashboardPlotHeader = styled.div` + background: ${(props) => props.theme.colors.headings}; + height: ${(props) => props.theme.layout.gutter * 1.5}px; + width: 100%; + color: ${(props) => props.theme.colors.primary[2]}; + padding: ${(props) => (props.theme.layout.gutter * 3) / 8}px + ${(props) => props.theme.layout.gutter / 2}px; + border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; +`; + +const HeaderSvg = styled.div` + display: inline-block; + padding: ${(props) => props.theme.layout.gutter / 8}px; + vertical-align: middle; +`; + +const HeaderActionItem = styled.div` + margin-right: ${(props) => props.theme.layout.gutter / 2}px; + vertical-align: middle; + cursor: pointer; +`; + +const IconContainer = styled.div` + height: ${(props) => (props.theme.layout.gutter * 3) / 4}px; + width: ${(props) => (props.theme.layout.gutter * 3) / 4}px; + background: ${(props) => props.theme.colors.gray[4]}; + display: inline-block; + border-radius: 50%; + padding: ${(props) => props.theme.layout.gutter / 8}px; + vertical-align: middle; +`; + +const CloseButton = styled.div` + display: inline-block; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + border: 1px solid ${(props) => props.theme.colors.gray[4]}; + height: ${(props) => (props.theme.layout.gutter * 3) / 4}px; + cursor: pointer; +`; + +const Input = styled.input` + display: inline-block; + font-weight: normal; + margin: 0; + line-height: 1.3; + box-shadow: none; + border: 0; + text-align: ${(props) => props.align}; + margin-left: ${(props) => props.marginLeft || 0}px; + margin-top: ${(props) => props.theme.layout.gutter / 2}px; + font: ${(props) => props.theme.fonts.subbody}; + &:focus { + border: 1px solid; + } +`; + +export default sortableElement(GraphCard); diff --git a/browser/components/dashboardshell/PlotComponent.js b/browser/components/dashboardshell/PlotComponent.js new file mode 100644 index 0000000..35991b8 --- /dev/null +++ b/browser/components/dashboardshell/PlotComponent.js @@ -0,0 +1,317 @@ +import Plot from "react-plotly.js"; +// import Plotly from "plotly.js"; +import { useState, useEffect } from "react"; +import { aggregate } from "../../helpers/aggregate.js"; +import moment from "moment"; +import _ from "lodash"; +// import { Colorscales } from "../shared/constants/colorscales"; +const config = require("../../../../../config/config.js"); + +function next_color() { + let colorscale = + JSON.parse(JSON.stringify(config?.extensionConfig?.watsonc?.colorScales)) || + {}; + return (custom_colorscale) => { + return colorscale[custom_colorscale].shift(); + }; +} + +var autoscale = { + width: 500, + // height: "1em", + // viewBox: "0 0 1000 1000", + path: "M447.1 319.1v135.1c0 13.26-10.75 23.1-23.1 23.1h-135.1c-12.94 0-24.61-7.781-29.56-19.75c-4.906-11.1-2.203-25.72 6.937-34.87l30.06-30.06L224 323.9l-71.43 71.44l30.06 30.06c9.156 9.156 11.91 22.91 6.937 34.87C184.6 472.2 172.9 479.1 160 479.1H24c-13.25 0-23.1-10.74-23.1-23.1v-135.1c0-12.94 7.781-24.61 19.75-29.56C23.72 288.8 27.88 288 32 288c8.312 0 16.5 3.242 22.63 9.367l30.06 30.06l71.44-71.44L84.69 184.6L54.63 214.6c-9.156 9.156-22.91 11.91-34.87 6.937C7.798 216.6 .0013 204.9 .0013 191.1v-135.1c0-13.26 10.75-23.1 23.1-23.1h135.1c12.94 0 24.61 7.781 29.56 19.75C191.2 55.72 191.1 59.87 191.1 63.1c0 8.312-3.237 16.5-9.362 22.63L152.6 116.7l71.44 71.44l71.43-71.44l-30.06-30.06c-9.156-9.156-11.91-22.91-6.937-34.87c4.937-11.95 16.62-19.75 29.56-19.75h135.1c13.26 0 23.1 10.75 23.1 23.1v135.1c0 12.94-7.781 24.61-19.75 29.56c-11.1 4.906-25.72 2.203-34.87-6.937l-30.06-30.06l-71.43 71.43l71.44 71.44l30.06-30.06c9.156-9.156 22.91-11.91 34.87-6.937C440.2 295.4 447.1 307.1 447.1 319.1z", + // path: "M224 376V512H24C10.7 512 0 501.3 0 488v-464c0-13.3 10.7-24 24-24h336c13.3 0 24 10.7 24 24V352H248c-13.2 0-24 10.8-24 24zm76.45-211.36-96.42-95.7c-6.65-6.61-17.39-6.61-24.04 0l-96.42 95.7C73.42 174.71 80.54 192 94.82 192H160v80c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16v-80h65.18c14.28 0 21.4-17.29 11.27-27.36zM377 407 279.1 505c-4.5 4.5-10.6 7-17 7H256v-128h128v6.1c0 6.3-2.5 12.4-7 16.9z", + ascent: 500, + descent: -50, + // transform: "matrix(1 0 0 -1 0 850)", +}; + +function PlotComponent(props) { + const [layoutState, setLayoutState] = useState( + JSON.parse(JSON.stringify(config?.extensionConfig?.watsonc?.plotLayout)) || + {} + ); + const [plotDataState, setPlotDataState] = useState(props.plotData); + const [configState, setConfigState] = useState({}); + const [minmaxRange, setminmaxRange] = useState([0, 1]); + // let layout = + + const changeLayout = (minmax) => { + // console.log(gd.layout); + + return () => + setLayoutState((prev) => { + prev.xaxis = { + ...prev.xaxis, + autorange: false, + range: [minmax[0], minmax[1]], + }; + prev.yaxis = { + ...prev.yaxis, + autorange: true, + }; + prev.yaxis2 = { + ...prev.yaxis2, + autorange: true, + }; + prev.yaxis3 = { + ...prev.yaxis3, + autorange: true, + }; + return { ...prev }; + }); + }; + + let yaxisTitles = + JSON.parse(JSON.stringify(config?.extensionConfig?.watsonc?.yaxisTitles)) || + {}; + + useEffect(() => { + let layout = JSON.parse( + JSON.stringify(config?.extensionConfig?.watsonc?.plotLayout) + ); + + let num_types = [ + ...new Set( + props.plotData + .sort((a, b) => { + if (Object.hasOwn(a, "yaxis") && Object.hasOwn(b, "yaxis")) { + return 0; + } + if (Object.hasOwn(a, "yaxis")) { + return 1; + } + if (Object.hasOwn(b, "yaxis")) { + return -1; + } + // a must be equal to b + return 0; + }) + .map((elem) => elem.tstype_id) + ), + ]; + + const give_color = next_color(); + let plotData = props.plotData.map((elem) => { + if (typeof elem.custom_colorscale != "undefined") { + var color = give_color(elem.custom_colorscale); + return { + ...elem, + line: { + ...elem.line, + color: typeof color == "undefined" ? null : color, + }, + }; + } else { + return elem; + } + }); + + if (!num_types.includes(undefined)) { + if (num_types.length == 1) { + layout.yaxis = { + ...layout.yaxis, + title: { + ...layout.yaxis.title, + text: yaxisTitles[num_types[0]], + }, + }; + + plotData = plotData.map((elem) => { + return { + yaxis: "y1", + ...elem, + }; + }); + } else if (num_types.length == 2) { + plotData = plotData.map((elem) => { + return { + yaxis: elem.tstype_id === num_types[0] ? "y1" : "y3", + ...elem, + }; + }); + + layout.yaxis = { + ...layout.yaxis, + title: { + ...layout.yaxis.title, + text: yaxisTitles[num_types[0]], + }, + }; + layout.yaxis3 = { + ...layout.yaxis3, + title: { + ...layout.yaxis3.title, + text: yaxisTitles[num_types[1]], + }, + }; + } else if (num_types.length > 2) { + plotData = plotData.map((elem) => { + return { + yaxis: + elem.tstype_id === num_types[0] + ? "y1" + : elem.tstype_id === num_types[1] + ? "y2" + : "y3", + ...elem, + }; + }); + layout.xaxis = { + ...layout.xaxis, + domain: [0, 0.9], + }; + layout.yaxis = { + ...layout.yaxis, + title: { + ...layout.yaxis.title, + text: yaxisTitles[num_types[0]], + }, + }; + layout.yaxis2 = { + ...layout.yaxis2, + title: { + ...layout.yaxis2.title, + text: yaxisTitles[num_types[1]], + }, + position: 0.9, + anchor: "free", + }; + layout.yaxis3 = { + ...layout.yaxis3, + title: { + ...layout.yaxis3.title, + text: num_types.length == 3 ? yaxisTitles[num_types[2]] : "Øvrige", + }, + position: 0.97, + anchor: "free", + }; + } + } + + if (plotData.length > 0) { + let xmin = moment.min( + plotData + .filter( + (elem) => + elem.xaxis != "x2" && + typeof elem.x != "undefined" && + (elem.tstype_id != 4 || plotData.length == 1) + ) + .map((elem) => moment(elem.x[0])) + ); + + let xmax = moment.max( + plotData + .filter( + (elem) => + elem.xaxis != "x2" && + typeof elem.x != "undefined" && + (elem.tstype_id != 4 || plotData.length == 1) + ) + .map((elem) => moment(elem.x.slice(-1)[0])) + ); + var diff = xmax.diff(xmin); + xmin = xmin.subtract(diff * 0.02); + + xmin = xmin.format("YYYY-MM-DD HH:mm:ss.SSS"); + xmax = xmax.format("YYYY-MM-DD HH:mm:ss.SSS"); + setminmaxRange([xmin, xmax]); + layout.xaxis = { + ...layout.xaxis, + range: [xmin, xmax], + }; + + var resetAxes = { + name: "myAutoscale", + title: "Autoscale", + icon: autoscale, + click: changeLayout([xmin, xmax]), + }; + + var conf = { + responsive: true, + modeBarButtons: [ + ["toImage", "zoom2d", "pan2d", "zoomIn2d", "zoomOut2d"], + [resetAxes], + ["hoverClosestCartesian", "hoverCompareCartesian"], + ], + // modeBarButtonsToAdd: [resetAxes], + // modeBarButtonsToRemove: ["resetScale2d", "autoScale2d", "toggleSpikelines"], + doubleClick: false, + }; + } + for (const agg of props.aggregate) { + let ind = props.plotMeta.measurements.findIndex( + (elem) => elem === agg.idx + ); + if (ind != -1 && plotData[ind]) { + var grouped = aggregate( + plotData[ind].x, + plotData[ind].y, + agg.window, + agg.func + ); + plotData[ind] = { + ...plotData[ind], + ...grouped, + }; + // plotData[ind].x = grouped.x; + // plotData[ind].y = grouped.y; + // if (agg.func === 'sum') { + // plotData[ind].width = grouped.width; + // } + } + } + setConfigState(conf); + setLayoutState(layout); + setPlotDataState(plotData); + // setTriggerAggregate((prev) => !prev); + }, [props.plotData, props.yAxis2LayoutSettings, props.aggregate]); + + // useEffect(() => { + // let plotData = plotDataState; + // for (const [key, value] of Object.entries(props.aggregate)) { + // var grouped = aggregate( + // props.plotData[key].x, + // props.plotData[key].y, + // value, + // _.meanBy + // ); + // plotData[key].x = grouped.x; + // plotData[key].y = grouped.y; + // console.log(plotData); + // } + // setPlotDataState([...plotData]); + // }, [props.aggregate, triggerAggregate]); + + return ( + //<div style={{ maxHeight: $(document).height() * 0.4 + 40 + "px" }}> + <div + style={{ + height: + typeof props.height == "string" + ? props.height + : `${props.height - 50}px`, + border: `1px solid lightgray`, + }} + > + <Plot + data={plotDataState} + layout={layoutState} + config={configState} + onLegendDoubleClick={(param) => + console.log("Legend double clicked", param) + } + useResizeHandler={true} + onDoubleClick={changeLayout(minmaxRange)} + onLegendClick={(param) => console.log("Legend clicked", param)} + style={{ width: "100%", height: `100%` }} + /> + </div> + //</div> + ); +} + +export default PlotComponent; diff --git a/browser/components/dashboardshell/ProfileComponent.js b/browser/components/dashboardshell/ProfileComponent.js new file mode 100644 index 0000000..f35d993 --- /dev/null +++ b/browser/components/dashboardshell/ProfileComponent.js @@ -0,0 +1,64 @@ +import Plot from "react-plotly.js"; +import { useState, useEffect } from "react"; + +function ProfileComponent(props) { + const [data, setData] = useState(props.plotMeta.profile.data.data); + const [layout, setLayout] = useState(props.plotMeta.profile.data.layout); + + const [plot, setPlot] = useState( + <p className="text-muted">{__(`At least one y axis has to be provided`)}</p> + ); + + useEffect(() => { + if (props.plotMeta) { + let dataCopy = JSON.parse( + JSON.stringify(props.plotMeta.profile.data.data) + .replace(/%28/g, "(") + .replace(/%29/g, ")") + ); + dataCopy.map((item, index) => { + if (!dataCopy[index].mode) dataCopy[index].mode = "lines"; + }); + let layoutCopy = JSON.parse( + JSON.stringify(props.plotMeta.profile.data.layout) + ); + layoutCopy.margin = { + l: 50, + r: 5, + b: 45, + t: 5, + pad: 1, + }; + layoutCopy.autosize = true; + setData(dataCopy); + setLayout(layoutCopy); + } + }, [props.plotMeta]); + + return ( + <div + style={{ + height: + typeof props.height == "string" + ? props.height + : `${props.height - 50}px`, + border: `1px solid lightgray`, + }} + > + <Plot + data={data} + useResizeHandler={true} + onClick={props.onClick} + config={{ + modeBarButtonsToRemove: ["autoScale2d"], + responsive: true, + doubleClick: false, + }} + layout={layout} + style={{ width: "100%", height: "100%" }} + /> + </div> + ); +} + +export default ProfileComponent; diff --git a/browser/components/dashboardshell/SortHandleComponent.js b/browser/components/dashboardshell/SortHandleComponent.js new file mode 100644 index 0000000..cd0b11f --- /dev/null +++ b/browser/components/dashboardshell/SortHandleComponent.js @@ -0,0 +1,31 @@ +import styled from 'styled-components'; +import Title from '../shared/title/Title'; +import {sortableHandle} from 'react-sortable-hoc'; + +const SortHandleComponent = (props) => { + return (<HeaderActionItem> + <IconContainer> + <Icon name="drag-handle" size={16} /> + </IconContainer> + <Title marginLeft={8} level={6} text={__('Flyt')} /> + </HeaderActionItem>) +}; + +const HeaderActionItem = styled.div` + margin-right: ${props => props.theme.layout.gutter/2}px; + vertical-align: middle; + cursor: pointer; +`; + + +const IconContainer = styled.div` + height: ${props => props.theme.layout.gutter*3/4}px; + width: ${props => props.theme.layout.gutter*3/4}px; + background: ${props => props.theme.colors.gray[4]}; + display: inline-block; + border-radius: 50%; + padding: ${props => props.theme.layout.gutter/8}px; + vertical-align: middle; +`; + +export default sortableHandle(SortHandleComponent); diff --git a/browser/components/dataselector/ChemicalSelectorModal.js b/browser/components/dataselector/ChemicalSelectorModal.js new file mode 100644 index 0000000..ec44d48 --- /dev/null +++ b/browser/components/dataselector/ChemicalSelectorModal.js @@ -0,0 +1,168 @@ +import { useState, useEffect } from "react"; +import { connect } from "react-redux"; +import styled from "styled-components"; +import { selectChemical } from "../../redux/actions"; +import Title from "../shared/title/Title"; +import CloseButton from "../shared/button/CloseButton"; +import PropTypes from "prop-types"; +import Grid from "@material-ui/core/Grid"; +import Card from "../shared/card/Card"; +import CheckBoxList from "../shared/list/CheckBoxList"; +import RadioButtonList from "../shared/list/RadioButtonList"; +import Button from "../shared/button/Button"; +import ButtonGroup from "../shared/button/ButtonGroup"; +import ProjectList from "./ProjectList"; +import PredefinedDatasourceViews from "./PredefinedDatasourceViews"; +import { Variants } from "../shared/constants/variants"; +import { Size } from "../shared/constants/size"; +import { Align } from "../shared/constants/align"; +import { hexToRgbA } from "../../helpers/colors"; +import { WATER_LEVEL_KEY } from "../../constants"; +import { DarkTheme } from "../../themes/DarkTheme"; +import MetaApi from "../../api/meta/MetaApi"; +import Searchbox from "../shared/inputs/Searchbox"; + +ChemicalSelectorModal.propTypes = { + text: PropTypes.string, + state: PropTypes.object, + categories: PropTypes.object, + onApply: PropTypes.func, +}; + +function ChemicalSelectorModal(props) { + const [allParameters, setAllParameters] = useState([ + // { + // label: __("Vandstand"), + // value: WATER_LEVEL_KEY, + // group: __("Vandstand"), + // }, + ]); + const [parameters, setParameters] = useState([ + // { + // label: __("Vandstand"), + // value: WATER_LEVEL_KEY, + // group: __("Vandstand"), + // }, + ]); + const [parameterSearchTerm, setParameterSearchTerm] = useState(""); + const [selectedParameter, setSelectedParameter] = useState({ + label: __("Vandstand"), + value: WATER_LEVEL_KEY, + group: __("Vandstand"), + }); + + useEffect(() => { + const notSelected = { + label: __("Ikke valgt"), + value: false, + group: __("Ikke valgt"), + }; + let chemicals = [notSelected]; + for (let key in props.categories[LAYER_NAMES[0]]) { + if (key == "Vandstand") { + continue; + } + for (let key2 in props.categories[LAYER_NAMES[0]][key]) { + var label = props.categories[LAYER_NAMES[0]][key][key2]; + chemicals.push({ label: label, value: key2, group: key }); + } + } + setAllParameters([...chemicals]); + setSelectedParameter(notSelected); + }, [props.categories]); + + const onChemicalSelect = (param) => { + setSelectedParameter(param); + props.selectChemical(param.value); + }; + + useEffect(() => { + const params = allParameters.filter((parameter) => { + return ( + parameterSearchTerm.length == 0 || + parameter.label + .toLowerCase() + .indexOf(parameterSearchTerm.toLowerCase()) > -1 + ); + }); + setParameters(params); + }, [parameterSearchTerm, allParameters]); + + const applyParameter = () => { + props.onCloseButtonClick ? props.onCloseButtonClick() : null; + }; + + return ( + <Root> + <ModalHeader> + <Grid container> + <Grid container item xs={10}> + <Title + text={__("Vælg datakilde")} + color={DarkTheme.colors.headings} + /> + </Grid> + <Grid container justify="flex-end" item xs={2}> + <CloseButton + onClick={() => $("#watsonc-select-chemical-dialog").modal("hide")} + /> + </Grid> + </Grid> + </ModalHeader> + <ModalBody> + <Card> + <Searchbox + value={parameterSearchTerm} + placeholder={__("Søg efter måleparameter")} + onChange={(value) => setParameterSearchTerm(value)} + /> + <RadioButtonList + listItems={parameters} + onChange={onChemicalSelect} + selectedParameter={selectedParameter} + /> + </Card> + <ButtonGroup align={Align.Center}> + <Button + text={__("Start")} + variant={Variants.Primary} + onClick={() => { + props.onClickControl(selectedParameter.value); + }} + size={Size.Large} + /> + </ButtonGroup> + </ModalBody> + </Root> + ); +} + +const Root = styled.div` + background: ${({ theme }) => hexToRgbA(theme.colors.primary[1], 1)}; + border-radius: ${({ theme }) => `${theme.layout.borderRadius.large}px`}; + color: ${({ theme }) => `${theme.colors.headings}`}; +`; + +const ModalHeader = styled.div` + padding: ${({ theme }) => + `${theme.layout.gutter}px ${theme.layout.gutter}px 0 ${theme.layout.gutter}px`}; +`; + +const ModalBody = styled.div` + padding: ${({ theme }) => `${theme.layout.gutter}px`}; +`; + +const GridContainer = styled.div` + padding-top: ${(props) => props.theme.layout.gutter / 2}px; +`; + +const mapStateToProps = (state) => ({}); + +const mapDispatchToProps = (dispatch) => ({ + selectChemical: (key) => dispatch(selectChemical(key)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ChemicalSelectorModal); diff --git a/browser/components/dataselector/DataSelectorDialogue.js b/browser/components/dataselector/DataSelectorDialogue.js new file mode 100644 index 0000000..ae71c78 --- /dev/null +++ b/browser/components/dataselector/DataSelectorDialogue.js @@ -0,0 +1,294 @@ +import { useState, useEffect } from "react"; +import { connect } from "react-redux"; +import styled from "styled-components"; +import { selectChemical } from "../../redux/actions"; +import Title from "../shared/title/Title"; +import CloseButton from "../shared/button/CloseButton"; +import PropTypes from "prop-types"; +import Grid from "@material-ui/core/Grid"; +import Card from "../shared/card/Card"; +import CheckBoxList from "../shared/list/CheckBoxList"; +import RadioButtonList from "../shared/list/RadioButtonList"; +import Button from "../shared/button/Button"; +import ButtonGroup from "../shared/button/ButtonGroup"; +import ProjectList from "./ProjectList"; +import PredefinedDatasourceViews from "./PredefinedDatasourceViews"; +import { Variants } from "../shared/constants/variants"; +import { Size } from "../shared/constants/size"; +import { Align } from "../shared/constants/align"; +import { hexToRgbA } from "../../helpers/colors"; +import { + CUSTOM_LAYER_NAME, + USER_LAYER_NAME, + WATER_LEVEL_KEY, +} from "../../constants"; +import { DarkTheme } from "../../themes/DarkTheme"; +import MetaApi from "../../api/meta/MetaApi"; +import Searchbox from "../shared/inputs/Searchbox"; +import { showSubscriptionIfFree } from "../../helpers/show_subscriptionDialogue"; + +DataSelectorDialogue.propTypes = { + text: PropTypes.string, + state: PropTypes.object, + categories: PropTypes.object, + onApply: PropTypes.func, +}; + +const session = require("./../../../../session/browser/index"); + +function DataSelectorDialogue(props) { + const [allParameters, setAllParameters] = useState([]); + const [showProjectsList, setShowProjectsList] = useState(false); + const [parameters, setParameters] = useState([]); + const [selectedDataSources, setSelectedDataSources] = useState([]); + const [selectedParameter, setSelectedParameter] = useState(); + const [dataSources, setDataSources] = useState([]); + const [parameterSearchTerm, setParameterSearchTerm] = useState(""); + const [filter, setFilter] = useState({}); + + useEffect(() => { + const waterLevelParameter = { + label: __("Vandstand"), + value: WATER_LEVEL_KEY, + group: __("Vandstand"), + }; + let chemicals = [waterLevelParameter]; + for (let key in props.categories[LAYER_NAMES[0]]) { + if (key == "Vandstand") { + continue; + } + for (let key2 in props.categories[LAYER_NAMES[0]][key]) { + var label = props.categories[LAYER_NAMES[0]][key][key2]; + chemicals.push({ label: label, value: key2, group: key }); + } + } + setAllParameters([...chemicals]); + setSelectedParameter(waterLevelParameter); + }, [props.categories]); + + const onChemicalSelect = (param) => { + setSelectedParameter(param); + props.selectChemical(param.value); + }; + + const loadDataSources = () => { + const api = new MetaApi(); + api.getMetaData("calypso_stationer").then((response) => { + let datasources = []; + response.map((elem) => { + if (elem.group == CUSTOM_LAYER_NAME) { + if ( + elem.privileges.hasOwnProperty( + session.getProperties()?.organisation.id + ) + ) { + datasources.push({ ...elem, group: USER_LAYER_NAME }); + } + } else { + datasources.push(elem); + } + }); + console.log(datasources); + setDataSources(datasources); + }); + }; + const applyLayer = (layer, chem = false) => { + props.onApply({ + layers: [layer], + chemical: chem, + filters: filter, + }); + props.onCloseButtonClick(); + }; + + useEffect(() => { + loadDataSources(); + props.backboneEvents.get().on("refresh:meta", () => loadDataSources()); + }, []); + + useEffect(() => { + $.ajax({ + url: `/api/sql/watsonc?q=SELECT loc_id, locname, groupname, relation FROM calypso_stationer.calypso_my_stations_v2 WHERE user_id in (${ + session.getProperties()?.organisation.id + }, ${session.getUserName()}) &base64=false&lifetime=60&srs=4326`, + method: "GET", + dataType: "json", + }).then((response) => { + var features = response.features; + var myStations = []; + + features.forEach((element) => { + myStations = myStations.concat(element.properties.loc_id); + }); + + let filter = { + match: "any", + columns: [ + { + fieldname: "loc_id", + expression: "=", + value: "ANY( ARRAY[" + myStations.join(", ") + "])", + restriction: false, + }, + ], + }; + + let filters = { "calypso_stationer.all_stations": filter }; + setFilter(filters); + }); + }, []); + + useEffect(() => { + const params = allParameters.filter((parameter) => { + return ( + parameterSearchTerm.length == 0 || + parameter.label + .toLowerCase() + .indexOf(parameterSearchTerm.toLowerCase()) > -1 + ); + }); + setParameters(params); + }, [parameterSearchTerm, allParameters]); + + const applyParameter = () => { + const layers = selectedDataSources.map((source) => { + return source.value; + }); + + props.onApply({ + layers: layers, + chemical: selectedParameter ? selectedParameter.value : false, + filters: filter, + }); + props.onCloseButtonClick(); + }; + + return ( + <Root> + <ModalHeader> + <Grid container> + <Grid container item xs={10}> + <Title text={props.titleText} color={DarkTheme.colors.headings} /> + </Grid> + <Grid container justify="flex-end" item xs={2}> + <CloseButton onClick={props.onCloseButtonClick} /> + </Grid> + </Grid> + </ModalHeader> + <ModalBody> + {showProjectsList ? ( + <ProjectList + onStateSnapshotApply={props.onCloseButtonClick} + {...props} + /> + ) : ( + <div> + <div> + <Title text={__("Quick Access")} level={3} /> + <PredefinedDatasourceViews + applyLayer={applyLayer} + setSelectedDataSources={setSelectedDataSources} + setSelectedParameter={setSelectedParameter} + /> + </div> + <GridContainer> + <Grid container spacing={32}> + <Grid container item md={6}> + <Card> + <Title text={__("Datakilder")} level={3} /> + <CheckBoxList + listItems={dataSources} + onChange={setSelectedDataSources} + selectedItems={selectedDataSources} + /> + </Card> + </Grid> + <Grid container item md={6}> + {selectedDataSources.findIndex( + (item) => item.value == "v:system.all" + ) > -1 ? ( + <Card> + <Searchbox + value={parameterSearchTerm} + placeholder={__("Søg efter måleparameter")} + onChange={(value) => setParameterSearchTerm(value)} + /> + <RadioButtonList + listItems={parameters} + onChange={onChemicalSelect} + selectedParameter={selectedParameter} + /> + </Card> + ) : null} + </Grid> + </Grid> + </GridContainer> + </div> + )} + {showProjectsList ? ( + <ButtonGroup align={Align.Center}> + <Button + text={__("Vælg datakilder og lag")} + variant={Variants.None} + onClick={() => setShowProjectsList(false)} + size={Size.Large} + /> + </ButtonGroup> + ) : ( + <ButtonGroup align={Align.Center} spacing={2}> + <Button + text={__("Abn eksisterende dashboard")} + variant={Variants.None} + onClick={() => { + if (showSubscriptionIfFree()) return; + setShowProjectsList(!showProjectsList); + }} + size={Size.Large} + /> + <Button + text={__("Start")} + variant={ + selectedDataSources.length === 0 + ? Variants.PrimaryDisabled + : Variants.Primary + } + onClick={() => applyParameter()} + size={Size.Large} + disabled={selectedDataSources.length === 0} + /> + </ButtonGroup> + )} + </ModalBody> + </Root> + ); +} + +const Root = styled.div` + background: ${({ theme }) => hexToRgbA(theme.colors.primary[1], 1)}; + border-radius: ${({ theme }) => `${theme.layout.borderRadius.large}px`}; + color: ${({ theme }) => `${theme.colors.headings}`}; +`; + +const ModalHeader = styled.div` + padding: ${({ theme }) => + `${theme.layout.gutter}px ${theme.layout.gutter}px 0 ${theme.layout.gutter}px`}; +`; + +const ModalBody = styled.div` + padding: ${({ theme }) => `${theme.layout.gutter}px`}; +`; + +const GridContainer = styled.div` + padding-top: ${(props) => props.theme.layout.gutter / 4}px; +`; + +const mapStateToProps = (state) => ({}); + +const mapDispatchToProps = (dispatch) => ({ + selectChemical: (key) => dispatch(selectChemical(key)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DataSelectorDialogue); diff --git a/browser/components/dataselector/PredefinedDatasourceViews.js b/browser/components/dataselector/PredefinedDatasourceViews.js new file mode 100644 index 0000000..cad573a --- /dev/null +++ b/browser/components/dataselector/PredefinedDatasourceViews.js @@ -0,0 +1,59 @@ +import IconButton from "../shared/button/IconButton"; +import { LAYER_NAMES } from "../../constants"; + +function PredefinedDatasourceViews(props) { + return ( + <div> + <IconButton + icon="cleaning-spray" + label={__("Pesticider")} + onClick={() => { + props.setSelectedDataSources([ + { + label: "Pesticidoverblik", + group: "Grundvand, analyser", + value: "calypso_stationer.pesticidoverblik", + }, + ]); + props.applyLayer("calypso_stationer.pesticidoverblik"); + }} + /> + <IconButton + icon="no3-solid" + label={__("Nitrat")} + onClick={() => { + props.applyLayer(LAYER_NAMES[0], "246"); + props.setSelectedDataSources([ + { + label: "Jupiter boringer", + group: "Grundvand", + value: "v:system.all", + }, + ]); + props.setSelectedParameter({ + label: "Nitrat", + group: "Kemiske hovedbestanddele", + value: "246", + }); + }} + /> + <IconButton + icon="water-wifi-solid" + label={__("Mine stationer")} + onClick={() => { + props.applyLayer("calypso_stationer.all_stations"); + props.setSelectedDataSources([ + { + label: "Mine stationer", + group: "Brugerspecifikke lag", + value: "calypso_stationer.all_stations", + }, + ]); + }} + /> + {/*<IconButton icon="lab-flask-experiment" label={__('Mine favoritter')}/>*/} + </div> + ); +} + +export default PredefinedDatasourceViews; diff --git a/browser/components/dataselector/ProjectList.js b/browser/components/dataselector/ProjectList.js new file mode 100644 index 0000000..4042b79 --- /dev/null +++ b/browser/components/dataselector/ProjectList.js @@ -0,0 +1,169 @@ +import { useState, useEffect } from "react"; +import { connect } from "react-redux"; +import ProjectsApi from "../../api/projects/ProjectsApi"; +import Title from "../shared/title/Title"; +import styled from "styled-components"; +import Grid from "@material-ui/core/Grid"; +import { DarkTheme } from "../../themes/DarkTheme"; +import { Align } from "../shared/constants/align"; +import { Variants } from "../shared/constants/variants"; +import { Size } from "../shared/constants/size"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Button from "../shared/button/Button"; +import ProjectListItem from "./ProjectListItem"; +import Searchbox from "../shared/inputs/Searchbox"; +import base64url from "base64url"; + +function ProjectList(props) { + const [projects, setProjects] = useState([]); + const [allProjects, setAllProjects] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [hoveredItem, setHoveredItem] = useState(); + const [searchTerm, setSearchTerm] = useState(""); + + const getProjectParameters = (project) => { + let parameters = []; + let queryParameters = props.urlparser.urlVars; + parameters.push(`state=${project.id}`); + let config = null; + + if (project.snapshot && project.snapshot.meta) { + if (project.snapshot.meta.config) { + config = project.snapshot.meta.config; + } + + if (project.snapshot.meta.tmpl) { + parameters.push(`tmpl=${project.snapshot.meta.tmpl}`); + } + } + + if (!config && "config" in queryParameters && queryParameters.config) { + // If config is present in project meta, use that. + // Else take it from queryparams. + config = queryParameters.config; + } + + if (config) { + parameters.push(`config=${config}`); + } + return parameters; + }; + + const getPermalinkForProject = (project) => { + let parameters = getProjectParameters(project); + let permalink = `${ + window.location.origin + }${props.anchor.getUri()}?${parameters.join("&")}`; + return permalink; + }; + + const loadProjects = () => { + setIsLoading(true); + const projectsApi = new ProjectsApi(); + projectsApi.getProjects().then((response) => { + response.text().then((text) => { + const json = JSON.parse(base64url.decode(text)); + json.map((project) => { + project.permalink = getPermalinkForProject(project); + }); + setAllProjects(json); + setIsLoading(false); + }); + }); + }; + + useEffect(() => { + loadProjects(); + }, []); + + useEffect(() => { + let term = searchTerm.toLowerCase(); + let projectsToShow = allProjects.filter((project) => { + if (searchTerm.length === 0) { + return true; + } + return project.title.toLowerCase().indexOf(searchTerm) > -1; + }); + setProjects(projectsToShow); + }, [allProjects, searchTerm]); + + return ( + <Root> + {props.authenticated ? null : ( + <Title + text="Log ind for at se projekter" + level={5} + color={DarkTheme.colors.headings} + align={Align.Center} + /> + )} + {isLoading ? ( + <CircularProgress + style={{ + color: DarkTheme.colors.interaction[4], + marginLeft: "50%", + }} + /> + ) : ( + <div> + <SearchContainer> + <Searchbox + placeholder="Søg i dashboards" + onChange={(value) => setSearchTerm(value)} + /> + </SearchContainer> + {projects.length > 0 ? ( + <div> + <Grid container> + <Grid container item xs={6}> + <Title text="Mine dashboards" level={3} /> + </Grid> + </Grid> + <Grid container> + <Grid container item xs={12}> + {projects.map((project, index) => { + return ( + <ProjectListItem + project={project} + {...props} + key={index} + /> + ); + })} + </Grid> + </Grid> + </div> + ) : ( + <Title + text="Ingen gemte dashboards" + level={6} + align={Align.Center} + color={DarkTheme.colors.headings} + /> + )} + </div> + )} + </Root> + ); +} + +const Root = styled.div` + margin-top: ${(props) => props.theme.layout.gutter / 2}px; + margin-bottom: ${(props) => props.theme.layout.gutter * 2}px; + width: 100%; +`; + +const SearchContainer = styled.div` + width: 100%; + margin: 10px 0px; +`; + +const mapStateToProps = (state) => ({ + authenticated: state.global.authenticated, +}); + +const mapDispatchToProps = (dispatch) => ({ + setDashboardContent: (value) => dispatch(setDashboardContent(value)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectList); diff --git a/browser/components/dataselector/ProjectListItem.js b/browser/components/dataselector/ProjectListItem.js new file mode 100644 index 0000000..8c16373 --- /dev/null +++ b/browser/components/dataselector/ProjectListItem.js @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { toast } from "react-toastify"; +import styled from "styled-components"; +import Icon from "../shared/icons/Icon"; +import { DarkTheme } from "../../themes/DarkTheme"; +import { Variants } from "../shared/constants/variants"; +import { Spacing } from "../shared/constants/spacing"; +import Grid from "@material-ui/core/Grid"; +import Title from "../shared/title/Title"; + +function ProjectListItem(props) { + const [isHovered, setHover] = useState(false); + const [disableStateApply, setDisableStateApply] = useState(false); + const applySnapshot = () => { + if (disableStateApply) { + return; + } + if (props.onStateSnapshotApply) props.onStateSnapshotApply(); + setDisableStateApply(true); + props.state.applyState(props.project.snapshot).then(() => { + setDisableStateApply(false); + }); + }; + + return ( + <Root + key={index} + spacing={Spacing.Lite} + onClick={() => applySnapshot()} + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + <Grid container> + <Grid container item md={7}> + <Icon + name="dashboard" + variant={Variants.Primary} + strokeColor={ + isHovered + ? DarkTheme.colors.primary[2] + : DarkTheme.colors.primary[4] + } + size={24} + marginRight={8} + /> + <Title + text={props.project.title} + level={4} + color={DarkTheme.colors.headings} + /> + </Grid> + <Grid container item md={5} justify="flex-end"> + <IconContainer onClick={(e) => e.stopPropagation()}> + <CopyToClipboard + text={props.project.permalink} + onCopy={() => + toast.success("Kopieret", { + autoClose: 1500, + position: toast.POSITION.BOTTOM_RIGHT, + toastId: "copytoast", + }) + } + > + <Icon + name="hyperlink" + variant={Variants.Primary} + size={12} + strokeColor={DarkTheme.colors.headings} + /> + </CopyToClipboard> + </IconContainer> + </Grid> + </Grid> + </Root> + ); +} + +const Root = styled.div` + background: ${(props) => props.theme.colors.primary[2]}; + border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; + width: 100%; + border: 0; + box-shadow: none; + cursor: pointer; + margin-top: ${(props) => props.theme.layout.gutter / 2}px; + padding: ${(props) => props.theme.layout.gutter / 4}px; + &:hover { + background: ${(props) => props.theme.colors.primary[5]}; + } +`; + +const IconContainer = styled.div` + display: inline-block; + width: ${(props) => (props.theme.layout.gutter * 3) / 4}px; + height: ${(props) => (props.theme.layout.gutter * 3) / 4}px; + cursor: pointer; + padding: 3px; + padding-left: 5px; + &:hover { + border: 1px solid ${(props) => props.theme.colors.primary[2]}; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + } +`; + +export default ProjectListItem; diff --git a/browser/components/decorators/InfoComponent.js b/browser/components/decorators/InfoComponent.js new file mode 100644 index 0000000..9dc829f --- /dev/null +++ b/browser/components/decorators/InfoComponent.js @@ -0,0 +1,58 @@ +import React from "react"; +import Title from "../shared/title/Title"; +import { DarkTheme } from "../../themes/DarkTheme"; +import { Grid } from "@material-ui/core"; + +const InfoComponent = (props) => { + return ( + <Grid container spacing={8} style={{ whiteSpace: "pre-line" }}> + {props.info.map((elem, index) => { + if (elem.type == "header") { + return ( + <Grid item xs={12} key={index}> + <Title + level={2} + text={elem.value} + color={DarkTheme.colors.headings} + /> + </Grid> + ); + } + + if (elem.type == "label") { + return ( + <React.Fragment key={index}> + <Grid item xs={4}> + <Title + level={4} + text={elem.label + ": "} + color={DarkTheme.colors.gray[3]} + /> + </Grid> + <Grid item xs={8}> + <Title + level={4} + text={elem.value} + color={DarkTheme.colors.gray[5]} + /> + </Grid> + </React.Fragment> + ); + } + if (elem.type == "text") { + return ( + <Grid item xs={12} key={index}> + <Title + level={4} + text={elem.value} + color={DarkTheme.colors.gray[5]} + /> + </Grid> + ); + } + })} + </Grid> + ); +}; + +export default InfoComponent; diff --git a/browser/components/decorators/MapDecorator.js b/browser/components/decorators/MapDecorator.js new file mode 100644 index 0000000..aff8fc7 --- /dev/null +++ b/browser/components/decorators/MapDecorator.js @@ -0,0 +1,329 @@ +import styled from "styled-components"; +import { DarkTheme } from "../../themes/DarkTheme"; +import { useContext, useState, useEffect } from "react"; +import Title from "../shared/title/Title"; +import Button from "../shared/button/Button"; +import ButtonGroup from "../shared/button/ButtonGroup"; +import Grid from "@material-ui/core/Grid"; +import { Variants } from "../shared/constants/variants"; +import { Size } from "../shared/constants/size"; +import { Align } from "../shared/constants/align"; +import Icon from "../shared/icons/Icon"; +import ProjectContext from "../../contexts/project/ProjectContext"; +import reduxStore from "../../redux/store"; +import { addBoreholeFeature } from "../../redux/actions"; +import { getNewPlotId } from "../../helpers/common"; +import ImageCarousel from "../shared/images/ImageCarousel"; +import { showSubscriptionIfFree } from "../../helpers/show_subscriptionDialogue"; +import InfoComponent from "./InfoComponent"; + +const session = require("./../../../../session/browser/index"); + +function MapDecorator(props) { + const [showMoreInfo, setShowMoreInfo] = useState(false); + const [moreInfo, setMoreInfo] = useState( + props.data.properties.location_info + ? props.data.properties.location_info + : [] + ); + + useEffect(() => { + $.ajax({ + url: `/api/sql/watsonc?q=SELECT ts_info FROM calypso_stationer.location_info_timeseries WHERE loc_id='${props.data.properties.loc_id}'&base64=false&lifetime=60&srs=4326`, + method: "GET", + dataType: "json", + }).then( + (response) => { + let data = response.features[0].properties; + console.log(JSON.parse(data.ts_info)); + + setMoreInfo((prev) => [ + ...prev, + { type: "header", value: "Tidsserie info" }, + ...JSON.parse(data.ts_info), + ]); + }, + (jqXHR) => { + console.error(`Error occured while getting data`); + } + ); + }, []); + + const plot = () => { + props.setLoadingData(true); + let plot = props.data.properties; + let allPlots = props.getAllPlots(); + let plotData = { + id: `plot_${getNewPlotId(allPlots)}`, + title: props.data.properties.locname, + measurements: [], + relations: {}, + measurementsCachedData: {}, + }; + allPlots.unshift(plotData); + $.ajax({ + url: `/api/sql/jupiter?q=SELECT * FROM ${props.relation} WHERE loc_id='${props.data.properties.loc_id}'&base64=false&lifetime=60&srs=4326`, + method: "GET", + dataType: "json", + }).then( + (response) => { + let data = response.features[0].properties; + var indices = []; + if (typeof plot.compound_list != "undefined") { + let idx = plot.compound_list.indexOf(plot.compound); + while (idx != -1) { + indices.push(idx); + idx = plot.compound_list.indexOf(plot.compound, idx + 1); + } + } else { + indices = [...Array(data.data.length).keys()]; + } + + for (const u of indices) { + const measurement = plot.loc_id + ":_0:" + plot.ts_id[u]; + plotData.relations[measurement] = props.relation; + plotData.measurements.push(measurement); + plotData.measurementsCachedData[measurement] = { + data: { + properties: { + _0: JSON.stringify({ + unit: data.unit[u], + title: data.ts_name[u], + locname: data.locname, + intakes: [1], + boreholeno: data.loc_id, + data: data.data, + trace: data.trace, + relation: props.relation, + parameter: data.parameter[u], + ts_id: data.ts_id, + ts_name: data.ts_name, + }), + boreholeno: data.loc_id, + numofintakes: 1, + }, + }, + }; + } + let activePlots = allPlots.map((plot) => plot.id); + props.setPlots(allPlots, activePlots); + props.setLoadingData(false); + //props.onActivePlotsChange(activePlots, allPlots, projectContext); + }, + (jqXHR) => { + console.error(`Error occured while getting data`); + } + ); + }; + const addToDashboard = () => { + reduxStore.dispatch(addBoreholeFeature(props.data)); + }; + let links = []; + + if (typeof props.data.properties.compound_list != "undefined") { + const indices = []; + let idx = props.data.properties.compound_list.indexOf( + props.data.properties.compound + ); + while (idx != -1) { + indices.push(idx); + idx = props.data.properties.compound_list.indexOf( + props.data.properties.compound, + idx + 1 + ); + } + links.push( + indices.map((elem, index) => { + return ( + <Grid container key={index}> + <Icon + name="analytics-board-graph-line" + strokeColor={DarkTheme.colors.headings} + size={16} + /> + <Title + marginTop={0} + marginLeft={4} + level={5} + color={DarkTheme.colors.headings} + text={ + props.data.properties.ts_name[elem] + ? props.data.properties.ts_name[elem] + + ", " + + props.data.properties.parameter[elem] + : props.data.properties.parameter[elem] + } + /> + </Grid> + ); + }) + ); + } else { + links.push( + props.data.properties.ts_name.map((v, index) => { + return ( + <Grid container key={index}> + <Icon + name="analytics-board-graph-line" + strokeColor={DarkTheme.colors.headings} + size={16} + /> + <Title + marginTop={0} + marginLeft={4} + level={5} + color={DarkTheme.colors.headings} + text={ + v + ? v + ", " + props.data.properties.parameter[index] + : props.data.properties.parameter[index] + } + /> + </Grid> + ); + }) + ); + } + return ( + <Root> + {showMoreInfo ? ( + <> + {/* <LabelsContainer> + <Title + level={4} + text={props.data.properties.locname} + color={DarkTheme.colors.headings} + /> + </LabelsContainer> */} + <LabelsContainer> + <InfoComponent info={moreInfo} /> + </LabelsContainer> + <ButtonGroup + align={Align.Right} + marginTop={16} + marginRight={16} + marginLeft={16} + spacing={2} + > + <Button + text={__("Tidsserier")} + variant={Variants.Transparent} + size={Size.Small} + onClick={() => setShowMoreInfo(false)} + disabled={false} + /> + </ButtonGroup> + </> + ) : ( + <> + {/* <RatingStarContainer> + <Icon + name="rating-star-solid" + strokeColor={DarkTheme.colors.headings} + size={16} + /> + </RatingStarContainer> */} + + <ImageCarousel images={props.data.properties.images} /> + {/* <Img src={props.data.properties.images[0]} /> */} + + <LabelsContainer> + <Title + level={4} + text={props.data.properties.locname} + color={DarkTheme.colors.headings} + /> + <br /> + <Title + level={6} + color={DarkTheme.colors.primary[5]} + text={__("Tidsserier")} + marginTop={16} + /> + </LabelsContainer> + <LinksContainer>{links}</LinksContainer> + <ButtonGroup + align={Align.Center} + marginTop={16} + marginRight={16} + marginLeft={16} + spacing={2} + > + <Button + text={__("Vis alle tidsserier")} + variant={Variants.Primary} + size={Size.Small} + onClick={() => { + if (showSubscriptionIfFree(props.getAllPlots().length > 0)) + return; + addToDashboard(); + plot(); + props.setDashboardMode("half"); + }} + disabled={false} + /> + <Button + text={__("Tilføj til Dashboard")} + variant={Variants.Primary} + size={Size.Small} + onClick={() => { + addToDashboard(); + props.setDashboardMode("half"); + }} + disabled={false} + /> + {moreInfo.length > 0 && ( + <Button + text={__("Mere info")} + variant={Variants.Transparent} + size={Size.Small} + onClick={() => setShowMoreInfo(true)} + disabled={false} + /> + )} + </ButtonGroup> + </> + )} + </Root> + ); +} + +const Root = styled.div` + width: 100%; + // height: 600px; + background-color: ${(props) => props.theme.colors.primary[2]}; + border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; + padding-top: 10px; + padding-bottom: 10px; + + img { + border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; + } +`; + +const Img = styled.img` + width: 100%; + height: 125px; +`; + +const LabelsContainer = styled.div` + margin-top: ${(props) => props.theme.layout.gutter / 4}px; + padding-left: ${(props) => props.theme.layout.gutter / 2}px; +`; + +const LinksContainer = styled.div` + margin-top: 8px; + padding-left: 16px; + + > div { + margin-top: 8px; + } +`; + +const RatingStarContainer = styled.div` + position: absolute; + top: ${(props) => props.theme.layout.gutter / 4}px; + right: ${(props) => props.theme.layout.gutter / 4}px; +`; + +export default MapDecorator; diff --git a/browser/components/modal/Modal.js b/browser/components/modal/Modal.js deleted file mode 100644 index 8135331..0000000 --- a/browser/components/modal/Modal.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Variants } from '../../constants'; -import Button from '../shared/button/Button'; - - -class Modal extends React.Component { - render() { - console.log(this.props); - return (<ModalContent> - <div className="modal-header"> - <Title>Testing Modal - -
- - - - -
- ); - } -} - -export default Modal; - -const Title = styled.div` - color: ${({ theme }) => { return theme.colors.headings}}; - padding: ${({ theme }) => theme.padding.titlePadding}; - text-align: center; - font-size: 34px; - text-transform: uppercase; - font-weight: 700; -`; - -const ModalContent = styled.div` - background: ${({ theme }) => theme.colors.background}; -`; - -const Right = styled.div` - text-align: right; - align-items: right; - width: 100%; -`; diff --git a/browser/components/shared/button/Button.js b/browser/components/shared/button/Button.js index 72b59c9..dc342ca 100644 --- a/browser/components/shared/button/Button.js +++ b/browser/components/shared/button/Button.js @@ -1,47 +1,88 @@ import styled, { css } from "styled-components"; -import { Variants } from "../../../constants"; -import PropTypes from 'prop-types'; +import { Variants } from "../constants/variants"; +import { Size } from "../constants/size"; +import PropTypes from "prop-types"; -function Button(props) { - return ( - - {props.text} - - ); -} Button.propTypes = { - text: PropTypes.string, - variant: PropTypes.oneOf(Object.keys(Variants)), - onClick: PropTypes.func.isRequired + text: PropTypes.string, + variant: PropTypes.oneOf(Object.keys(Variants)), + size: PropTypes.oneOf(Object.keys(Size)), + onClick: PropTypes.func.isRequired, +}; + +function Button(props) { + const onClick = (e) => { + if (props.onClick) { + props.onClick(e); + } + }; + return ( + + {props.text} + + ); } const Root = styled.button` - height: 40px; - padding: 0 24px 0 24px; - font-size: 16px; - cursor: pointer; - border: 0; - border-radius: ${props => props.theme.layout.borderRadius.small}px; - color: black; - margin: 0 10px 0 10px; - ${({ variant, theme }) => { - const styles = { - [Variants.Primary]: css ` - background-color: ${theme.colors.interaction[4]}; - &:hover { - background-color: ${props => props.theme.colors.interaction[5]}; - } - `, - [Variants.Secondary]: css ` - background-color: ${theme.colors.primary[4]}; - `, - [Variants.None]: css ` - background-color: ${theme.colors.gray[3]}; - ` - }; - return styles[variant]; + padding: 0 24px 0 24px; + font-size: 16px; + cursor: pointer; + border: 0; + font: ${(props) => props.theme.fonts.body}; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + color: black; + ${({ variant, theme }) => { + const styles = { + [Variants.Primary]: css` + background-color: ${theme.colors.interaction[4]}; + &:hover { + background-color: ${(props) => props.theme.colors.interaction[5]}; + } + `, + [Variants.PrimaryDisabled]: css` + background-color: ${theme.colors.interaction[4]}; + opacity: 0.5; + &:hover { + background-color: ${(props) => props.theme.colors.interaction[5]}; + } + `, + [Variants.Secondary]: css` + background-color: ${theme.colors.primary[3]}; + color: ${theme.colors.headings}; + `, + [Variants.None]: css` + background-color: ${theme.colors.gray[4]}; + `, + [Variants.Transparent]: css` + background-color: transparent; + border: 1px solid ${(props) => props.theme.colors.headings}; + color: ${(props) => props.theme.colors.headings}; + `, + }; + return styles[variant]; + }} + ${({ size, theme }) => { + const styles = { + [Size.Small]: css` + width: ${theme.layout.gutter * 3}px; + font: ${theme.fonts.label}; + min-height: 24px; + `, + [Size.Medium]: css` + width: ${theme.layout.gutter * 5}px; + min-height: 24px; + font: ${theme.fonts.label}; + `, + [Size.Large]: css` + width: ${theme.layout.gutter * 10}px; + `, + }; + return styles[size]; }} -` +`; export default Button; diff --git a/browser/components/shared/button/ButtonGroup.js b/browser/components/shared/button/ButtonGroup.js new file mode 100644 index 0000000..29741bc --- /dev/null +++ b/browser/components/shared/button/ButtonGroup.js @@ -0,0 +1,55 @@ +import styled, { css } from "styled-components"; +import { Align } from '../constants/align'; +import PropTypes from 'prop-types'; + +ButtonGroup.propTypes = { + text: PropTypes.string, + align: PropTypes.string, + marginTop: PropTypes.number, + marginLeft: PropTypes.number, + marginRight: PropTypes.number, + spacing: PropTypes.number, +} + +function ButtonGroup(props) { + return ( + + {props.children} + + ); +} + +const Root = styled.div` + display: flex; + margin-top: ${props => props.marginTop || props.theme.layout.gutter}px; + margin-right: ${props => props.marginRight || 0}px; + margin-left: ${props => props.marginLeft || 0}px; + ${({ align, theme }) => { + const styles = { + [Align.Left]: css ` + justify-content: start; + align-items: flex-start; + button { + margin-right: ${theme.layout.gutter / 2}px; + } + `, + [Align.Center]: css` + justify-content: center; + align-items: center; + `, + [Align.Right]: css` + justify-content: flex-end; + align-items: flex-end; + button { + margin-left: ${theme.layout.gutter / 2}px; + } + ` + } + return styles[align]; + }} + + button + button { + margin-left: ${props => props.spacing*8}px; + } +` +export default ButtonGroup; diff --git a/browser/components/shared/button/CloseButton.js b/browser/components/shared/button/CloseButton.js new file mode 100644 index 0000000..8520976 --- /dev/null +++ b/browser/components/shared/button/CloseButton.js @@ -0,0 +1,37 @@ +import styled, { css } from "styled-components"; +import { Variants } from "../../../constants"; +import PropTypes from 'prop-types'; +import Icon from "@material-ui/core/Icon"; + +CloseButton.propTypes = { + text: PropTypes.string, + onClick: PropTypes.func.isRequired +} + +function CloseButton(props) { + return ( + + + + + + + + + ); +} + +const Root = styled.button` + height: 20px; + width: 20px; + margin-top: 16px; + cursor: pointer; + border: 1px solid ${({ theme }) => theme.colors.headings}; + border-radius: ${({ theme }) => theme.layout.borderRadius.small}px; + color: ${({ theme }) => theme.colors.headings}; + background: transparent; + padding: 0; +` +export default CloseButton; diff --git a/browser/components/shared/button/IconButton.js b/browser/components/shared/button/IconButton.js new file mode 100644 index 0000000..4ee5119 --- /dev/null +++ b/browser/components/shared/button/IconButton.js @@ -0,0 +1,39 @@ +import styled, { css } from "styled-components"; +import PropTypes from 'prop-types'; +import Icon from '../icons/Icon'; + +IconButton.propTypes = { + onClick: PropTypes.func, + icon: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, +} + +function IconButton(props) { + return ( + + + {props.label} + + ) +} + +const Root = styled.button` + height: 80px; + width: 80px; + background: ${props => props.theme.colors.primary[1]}; + border 2px solid ${props => props.theme.colors.primary[3]}; + color: ${props => props.theme.colors.gray[4]}; + border-radius: ${props => props.theme.layout.borderRadius.medium}px; + margin-right: ${props => props.theme.layout.gutter/2}px; + &:hover { + border: 2px solid ${props => props.theme.colors.interaction[4]}; + } +`; + +const IconLabel = styled.div` + font: ${props => props.theme.fonts.footnote}; + color: ${props => props.theme.colors.gray[4]}; +`; + +export default IconButton; diff --git a/browser/components/shared/card/Card.js b/browser/components/shared/card/Card.js new file mode 100644 index 0000000..02c5ebf --- /dev/null +++ b/browser/components/shared/card/Card.js @@ -0,0 +1,24 @@ +import styled, { css } from "styled-components"; +import PropTypes from 'prop-types'; +import { Spacing } from '../constants/spacing'; + +function Card(props) { + + return ( + + {props.children} + + ); +} + +const Root = styled.div` + background: ${props => props.theme.colors.primary[2]}; + border-radius: ${props => props.theme.layout.borderRadius.medium}px; + width: 100%; + border: 0; + box-shadow: none; + margin-top: ${props => props.theme.layout.gutter}px; + padding: ${props => props.theme.layout.gutter/2}px; +`; + +export default Card; diff --git a/browser/components/shared/constants/align.js b/browser/components/shared/constants/align.js new file mode 100644 index 0000000..58e62de --- /dev/null +++ b/browser/components/shared/constants/align.js @@ -0,0 +1,7 @@ +const Align = { + Left: 'left', + Center: 'center', + Right: 'right' +} + +export {Align}; diff --git a/browser/components/shared/constants/size.js b/browser/components/shared/constants/size.js new file mode 100644 index 0000000..551e0d1 --- /dev/null +++ b/browser/components/shared/constants/size.js @@ -0,0 +1,9 @@ +const Size = { + Small: 'Small', + Medium: 'Medium', + Large: 'Large', +}; + +export { + Size, +} diff --git a/browser/components/shared/constants/spacing.js b/browser/components/shared/constants/spacing.js new file mode 100644 index 0000000..ad4d863 --- /dev/null +++ b/browser/components/shared/constants/spacing.js @@ -0,0 +1,8 @@ +const Spacing = { + Standard: 'Standard', + Lite: 'Lite', +}; + +export { + Spacing, +} diff --git a/browser/components/shared/constants/variants.js b/browser/components/shared/constants/variants.js new file mode 100644 index 0000000..1a887f2 --- /dev/null +++ b/browser/components/shared/constants/variants.js @@ -0,0 +1,12 @@ +const Variants = { + None: 'None', + Primary: 'Primary', + PrimaryDisabled: 'PrimaryDisabled', + Secondary: 'Secondary', + Tertiary: 'Tertiary', + Transparent: 'Transparent', +}; + +export { + Variants, +} diff --git a/browser/components/shared/hooks/useInterval.js b/browser/components/shared/hooks/useInterval.js new file mode 100644 index 0000000..927da7e --- /dev/null +++ b/browser/components/shared/hooks/useInterval.js @@ -0,0 +1,23 @@ +import React from "react"; + +const useInterval = (callback, delay) => { + const savedCallback = React.useRef(); + + React.useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + React.useEffect(() => { + // Don't schedule if no delay is specified. + // Note: 0 is a valid value for delay. + if (!delay && delay !== 0) { + return; + } + + const id = setInterval(() => savedCallback.current(), delay); + + return () => clearInterval(id); + }, [delay]); +}; + +export default useInterval; diff --git a/browser/components/shared/hooks/usePrevious.js b/browser/components/shared/hooks/usePrevious.js new file mode 100644 index 0000000..70dfec4 --- /dev/null +++ b/browser/components/shared/hooks/usePrevious.js @@ -0,0 +1,10 @@ +import { useRef, useEffect } from "react"; + +function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; //assign the value of ref to the argument + }, [value]); //this code will run when the value of 'value' changes + return ref.current; //in the end, return the current ref value. +} +export default usePrevious; diff --git a/browser/components/shared/icons/Icon.js b/browser/components/shared/icons/Icon.js new file mode 100644 index 0000000..5dde852 --- /dev/null +++ b/browser/components/shared/icons/Icon.js @@ -0,0 +1,99 @@ +import * as React from "react"; +import styled, { css } from "styled-components"; +import { Variants } from "../constants/variants"; +import icons from "../../../../shared/icons/icons.json"; +import { IconName } from "../../../../shared/icons/icons"; +import PropTypes from "prop-types"; + +Icon.propTypes = { + name: PropTypes.string, + variant: PropTypes.oneOf(Object.keys(Variants)), + onClick: PropTypes.func, + strokeColor: PropTypes.string, + fillColor: PropTypes.string, + align: PropTypes.string, + marginRight: PropTypes.number, + size: PropTypes.number, +}; + +Icon.defaultProps = { + strokeColor: "currentColor", + fillColor: "currentColor", + size: 24, + variant: "Primary", + marginRight: 0, + align: "left", +}; + +function Icon(props) { + const icon = icons.find((i) => i.name === props.name); + if (!icon) { + // tslint:disable-next-line:no-console + console.warn(`Unable to find icon with name "${props.name}"`); + return null; + } + var size = props.size; + var viewBox = icon.viewbox || "0 0 24 24"; + return ( + + + {icon.name.endsWith("-solid") ? ( + + + + ) : ( + + + + )} + + + ); +} + +const Root = styled.div` + display: inline-block; + position: relative; + margin-right: ${(props) => props.marginRight}px; + padding-left: ${(props) => props.paddingLeft}px; + align: ${(props) => props.align}; + cursor: ${(props) => (props.onClick ? "pointer" : "inherit")}; + ${({ variant, theme }) => { + const styles = { + [Variants.Primary]: css` + background-color: ${theme.colors.gray[4]}; + &:hover { + background-color: ${(props) => props.theme.colors.gray[5]}; + } + `, + [Variants.Secondary]: css` + background-color: ${theme.colors.primary[4]}; + `, + [Variants.None]: css` + background-color: ${theme.colors.gray[3]}; + `, + }; + return styles[variant]; + }} +`; + +export default Icon; diff --git a/browser/components/shared/images/ImageCarousel.js b/browser/components/shared/images/ImageCarousel.js new file mode 100644 index 0000000..5bd0cfe --- /dev/null +++ b/browser/components/shared/images/ImageCarousel.js @@ -0,0 +1,50 @@ +import styled, { css } from "styled-components"; +import PropTypes from "prop-types"; +import { useState } from "react"; +import { Spacing } from "../constants/spacing"; +import useInterval from "../../shared/hooks/useInterval"; + +function ImageCarousel(props) { + var length; + if (typeof props.images === "undefined") { + length = 0; + } else { + length = !props.images?.[0] ? 0 : props.images.length; + } + + const [index, setIndex] = useState(0); + // const [lastindex, setlastindex] = useState(0); + + useInterval(() => { + if (index === length - 1) { + setIndex(0); + // setlastindex(length); + } else { + // setlastindex(index); + setIndex((prev) => prev + 1); + } + }, 4000); + return
{length > 0 ? : null}
; +} + +// const Root = styled.div` +// background: ${(props) => props.theme.colors.primary[2]}; +// border-radius: ${(props) => props.theme.layout.borderRadius.medium}px; +// width: 100%; +// border: 0; +// box-shadow: none; +// margin-top: ${(props) => props.theme.layout.gutter}px; +// padding: ${(props) => props.theme.layout.gutter / 2}px; +// `; + +const Img = styled.img` + display: block; + margin-left: auto; + margin-right: auto; + // width: 100%; + height: 125px; + transition: opacity 1s; + // opacity: ${(props) => (props.animate ? "1" : "0")}; +`; + +export default ImageCarousel; diff --git a/browser/components/shared/inputs/CheckBox.js b/browser/components/shared/inputs/CheckBox.js new file mode 100644 index 0000000..927d18b --- /dev/null +++ b/browser/components/shared/inputs/CheckBox.js @@ -0,0 +1,53 @@ +import styled from "styled-components"; +import Icon from '../icons/Icon'; + +function CheckBox(props) { + return ( + + props.onChange(props.value)}> + + + {props.label} + + ); +} + +const HiddenCheckBox = styled.input.attrs({ type: 'checkbox' })` + border: 0; + clip: rect(0 0 0 0); + clippath: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +` +const StyledCheckBox = styled.div` + display: inline-block; + width: 16px; + height: 16px; + font-size: 12px; + background: ${props => props.checked ? props.theme.colors.interaction[4] : '#fff'}; + border-radius: 3px; + transition: all 150ms; + padding-left: ${props => props.theme.layout.gutter / 8}px; + margin-right: ${props => props.theme.layout.gutter / 4}px; + div { + visibility: ${props => props.checked ? 'visible' : 'hidden'} + } +` + +const Root = styled.div` + display: inline-block; + vertical-align: middle; +`; + +const CheckBoxLabel = styled.label` + margin-top: ${props => props.theme.layout.gutter/4}px; + color: ${props => props.theme.colors.gray[5]}; + font: ${props => props.theme.fonts.label}; +`; + +export default CheckBox; diff --git a/browser/components/shared/inputs/RadioButton.js b/browser/components/shared/inputs/RadioButton.js new file mode 100644 index 0000000..d79d194 --- /dev/null +++ b/browser/components/shared/inputs/RadioButton.js @@ -0,0 +1,61 @@ +import styled from "styled-components"; + +function RadioButton(props) { + return ( + + props.onChange(props.value)}> + + + {props.label} + + ); +} + +const HiddenRadioButton = styled.input.attrs({ type: 'radio' })` + border: 0; + clip: rect(0 0 0 0); + clippath: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +` + +const RadioButtonSelect = styled.div` + display: block; +`; + +const StyledRadioButton = styled.div` + display: inline-block; + width: 16px; + height: 16px; + border-radius: 50%; + background: ${props => props.checked ? props.theme.colors.interaction[4] : '#fff'}; + transition: all 150ms; + padding: ${props => props.theme.layout.gutter / 8}px; + margin-right: ${props => props.theme.layout.gutter / 4}px; + div { + visibility: ${props => props.checked ? 'visible' : 'hidden'}; + height: 8px; + width: 8px; + border-radius: 50%; + background: #000; + } +` + +const Root = styled.div` + display: inline-block; + vertical-align: middle; +`; + +const RadioButtonLabel = styled.label` + margin-top: ${props => props.theme.layout.gutter/4}px; + color: ${props => props.theme.colors.gray[5]}; + font: ${props => props.theme.fonts.label}; + vertical-align: text-bottom; +`; + +export default RadioButton; diff --git a/browser/components/shared/inputs/Searchbox.js b/browser/components/shared/inputs/Searchbox.js new file mode 100644 index 0000000..bd5043c --- /dev/null +++ b/browser/components/shared/inputs/Searchbox.js @@ -0,0 +1,72 @@ +import styled, { css } from "styled-components"; +import Icon from "../icons/Icon"; +import PropTypes from "prop-types"; + +Searchbox.propTypes = { + placeholder: PropTypes.string, + variant: PropTypes.string, + onChange: PropTypes.func, +}; + +Searchbox.defaultProps = { + variant: Variants.Transparent, +}; + +function Searchbox(props) { + const onChange = (event) => { + if (props.onChange) { + return props.onChange(event.target.value); + } + }; + return ( + + + + + ); +} + +const Container = styled.div` + display: flex; + height: 40px; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + border: 1px solid ${(props) => props.theme.colors.gray[2]}; + width: 100%; + color: ${(props) => props.theme.colors.gray[4]}; + > div { + margin: 10px; + } + ${({ variant, theme }) => { + const styles = { + [Variants.Primary]: css` + background-color: ${theme.colors.headings}; + `, + [Variants.Transparent]: css` + background-color: transparent; + `, + }; + return styles[variant]; + }} +`; + +const Input = styled.input` + background: transparent; + height: 40px; + box-shadow: none; + border: 0; + color: ${(props) => props.theme.colors.gray[5]}; + &:focus { + outline: none; + } + ::placeholder { + color: ${(props) => props.theme.colors.gray[4]}; + } + flex: 1; +`; + +export default Searchbox; diff --git a/browser/components/shared/inputs/Select.js b/browser/components/shared/inputs/Select.js new file mode 100644 index 0000000..e69de29 diff --git a/browser/components/shared/inputs/TextInput.js b/browser/components/shared/inputs/TextInput.js new file mode 100644 index 0000000..1067035 --- /dev/null +++ b/browser/components/shared/inputs/TextInput.js @@ -0,0 +1,73 @@ +import styled, { css } from "styled-components"; +import Icon from "../icons/Icon"; +import PropTypes from "prop-types"; + +TextInput.propTypes = { + placeholder: PropTypes.string, + variant: PropTypes.string, + onChange: PropTypes.func, +}; + +TextInput.defaultProps = { + variant: Variants.Transparent, +}; + +function TextInput(props) { + const onChange = (event) => { + if (props.onChange) { + return props.onChange(event.target.value); + } + }; + return ( + + + + ); +} + +const Container = styled.div` + display: flex; + height: 40px; + border-radius: ${(props) => props.theme.layout.borderRadius.small}px; + border: 1px solid ${(props) => props.theme.colors.gray[2]}; + width: 100%; + color: ${(props) => props.theme.colors.gray[4]}; + > div { + margin: 10px; + } + ${({ variant, theme }) => { + const styles = { + [Variants.Primary]: css` + background-color: ${theme.colors.headings}; + `, + [Variants.Transparent]: css` + background-color: transparent; + `, + }; + return styles[variant]; + }} +`; + +const Input = styled.input` + background: transparent; + height: 40px; + box-shadow: none; + border: 0; + color: ${(props) => props.theme.colors.gray[5]}; + &:focus { + outline: none; + } + ::placeholder { + color: ${(props) => props.theme.colors.gray[2]}; + } + flex: 1; +`; + +export default TextInput; diff --git a/browser/components/shared/list/CheckBoxList.js b/browser/components/shared/list/CheckBoxList.js new file mode 100644 index 0000000..244f853 --- /dev/null +++ b/browser/components/shared/list/CheckBoxList.js @@ -0,0 +1,104 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; +import PropTypes from "prop-types"; +import Checkbox from "../inputs/CheckBox"; +import { selectEndDate } from "../../../redux/actions"; + +CheckBoxList.propTypes = { + listItems: PropTypes.array.isRequired, + onChange: PropTypes.func, + selectedItems: PropTypes.array, +}; + +CheckBoxList.defaultProps = { + selectedItems: [], +}; + +function CheckBoxList(props) { + const [selectedItems, setSelectedItems] = useState(props.selectedItems); + const [listItems, setListItems] = useState([]); + var currentGroup = null; + + function onChangeCheckbox(value) { + let array = props.selectedItems.map((elem) => elem.value); + let index = array.indexOf(value); + // let array = [...props.selectedItems]; + if (index > -1) { + array.splice(index, 1); + } else { + array.push(value); + } + setSelectedItems(array); + } + + const getSelectedItems = () => { + return listItems.filter((item) => { + if (selectedItems.indexOf(item.value) > -1) { + return item; + } + return null; + }); + }; + + const renderItem = (item, index) => { + let returnData = []; + if (item.group != currentGroup) { + returnData.push( + + ); + currentGroup = item.group; + } + returnData.push( + <ListItem key={index}> + <Checkbox + value={item.value} + checked={ + props.selectedItems.map((elem) => elem.value).indexOf(item.value) > + -1 + } + onChange={onChangeCheckbox} + label={item.label} + /> + </ListItem> + ); + return returnData; + }; + + useEffect(() => { + var _listItems = [...props.listItems]; + _listItems.sort((a, b) => (a.group > b.group ? 1 : -1)); + setListItems(_listItems); + }, [props.listItems]); + + useEffect(() => { + if (props.onChange) { + props.onChange(getSelectedItems()); + } + }, [selectedItems]); + + return ( + <Root> + {listItems.map((item, index) => { + return renderItem(item, index); + })} + </Root> + ); +} + +const Root = styled.div` + height: ${(props) => props.theme.layout.gutter * 10}px; + overflow-y: scroll; + color: ${(props) => props.theme.colors.gray[4]}; +`; + +const ListItem = styled.div` + margin-left: ${(props) => props.theme.layout.gutter / 4}px; + color: ${(props) => props.theme.colors.gray[5]}; +`; + +export default CheckBoxList; diff --git a/browser/components/shared/list/RadioButtonList.js b/browser/components/shared/list/RadioButtonList.js new file mode 100644 index 0000000..a2ff0f0 --- /dev/null +++ b/browser/components/shared/list/RadioButtonList.js @@ -0,0 +1,71 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; +import PropTypes from "prop-types"; +import RadioButton from "../inputs/RadioButton"; + +RadioButtonList.propTypes = { + listItems: PropTypes.array.isRequired, + onChange: PropTypes.func, +}; + +function RadioButtonList(props) { + const [selectedItem, setSelectedItem] = useState(props.selectedParameter); + var currentGroup = null; + function onChangeRadioButton(value) { + let selectedObject = props.listItems.find((item) => item.value == value); + setSelectedItem(selectedObject); + } + + const renderItem = (item, index) => { + let returnData = []; + if (item.group != currentGroup) { + returnData.push( + <Title + key={`${index}-title`} + text={item.group} + level={5} + marginTop={16} + /> + ); + currentGroup = item.group; + } + returnData.push( + <ListItem key={index}> + <RadioButton + value={item.value} + checked={selectedItem.value == item.value} + onChange={onChangeRadioButton} + label={item.label} + /> + </ListItem> + ); + return returnData; + }; + + useEffect(() => { + if (props.onChange) { + props.onChange(selectedItem); + } + }, [selectedItem]); + + return ( + <Root> + {props.listItems.map((item, index) => { + return renderItem(item, index); + })} + </Root> + ); +} + +const Root = styled.div` + height: ${(props) => props.theme.layout.gutter * 10}px; + overflow-y: scroll; + color: ${(props) => props.theme.colors.gray[4]}; +`; + +const ListItem = styled.div` + margin-left: ${(props) => props.theme.layout.gutter / 4}px; + color: ${(props) => props.theme.colors.gray[5]}; +`; + +export default RadioButtonList; diff --git a/browser/components/shared/list/SortableList.js b/browser/components/shared/list/SortableList.js new file mode 100644 index 0000000..579ed36 --- /dev/null +++ b/browser/components/shared/list/SortableList.js @@ -0,0 +1,7 @@ +import { sortableContainer } from "react-sortable-hoc"; + +const SortableList = sortableContainer((props) => { + return <ul className="list-group">{props.children}</ul>; +}); + +export default SortableList; diff --git a/browser/components/shared/title/Title.js b/browser/components/shared/title/Title.js new file mode 100644 index 0000000..233ce16 --- /dev/null +++ b/browser/components/shared/title/Title.js @@ -0,0 +1,88 @@ +import styled, { css } from "styled-components"; +import PropTypes from "prop-types"; + +function Title(props) { + return ( + <Root + level={props.level} + color={props.color} + align={props.align} + marginTop={props.marginTop} + marginLeft={props.marginLeft} + width={props.width} + > + {props.text} + </Root> + ); +} +Title.propTypes = { + //text: PropTypes.string.isRequired, + level: PropTypes.number, + className: PropTypes.string, + align: PropTypes.string, + color: PropTypes.string, + marginTop: PropTypes.number, + marginLeft: PropTypes.number, + width: PropTypes.string, +}; + +Title.defaultProps = { + level: 1, + align: "left", + marginLeft: 0, + marginTop: 0, + color: "currentColor", + width: null, +}; + +const Root = styled.div` + display: inline-block; + font-weight: normal; + margin: 0; + line-height: 1.3; + width: ${(props) => props.width}; + text-align: ${(props) => props.align}; + margin-left: ${(props) => props.marginLeft || 0}px; + ${({ level, theme }) => { + const styles = { + 1: css` + font: ${(props) => props.theme.fonts.title}; + color: ${(props) => props.color || props.theme.colors.headings}; + `, + 2: css` + font: ${(props) => props.theme.fonts.subtitle}; + color: ${(props) => props.color || props.theme.colors.headings}; + `, + 3: css` + font: ${(props) => props.theme.fonts.heading}; + color: ${(props) => props.color || props.theme.colors.gray[3]}; + `, + 4: css` + font: ${(props) => props.theme.fonts.body}; + color: ${(props) => props.color || props.theme.colors.primary[5]}; + `, + 5: css` + margin-top: ${(props) => + props.marginTop != null + ? props.marginTop + : props.theme.layout.gutter / 2}px; + font: ${(props) => props.theme.fonts.subbody}; + color: ${(props) => props.color || props.theme.colors.gray[4]}; + `, + 6: css` + margin-top: ${(props) => + props.marginTop != null + ? props.marginTop + : props.theme.layout.gutter / 4}px; + font: ${(props) => props.theme.fonts.label}; + color: ${(props) => props.color || props.theme.colors.gray[5]}; + `, + 7: css` + font: ${(props) => props.theme.fonts.footnote}; + color: ${(props) => props.color || props.theme.colors.gray[5]}; + `, + }; + return styles[level]; + }} +`; +export default Title; diff --git a/browser/components/shared/userProfileButton/UserAvatar.js b/browser/components/shared/userProfileButton/UserAvatar.js new file mode 100644 index 0000000..8b1b81b --- /dev/null +++ b/browser/components/shared/userProfileButton/UserAvatar.js @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import PropTypes from 'prop-types'; +import Icon from '../icons/Icon'; + +function UserAvatar(props) { + + return( + <Avatar> + <Icon name='avatar' size={24} /> + </Avatar> + ); +} + +const Avatar = styled.div` + background: ${({ theme }) => theme.colors.primary[5]}; + border-radius: 50%; + padding-left: 10px; + height: 44px; + width: 44px; + color: white; + font: ${({ theme }) => theme.fonts.heading}; + display: flex; + text-align: center; + justify-content: center; + flex-direction: column; + cursor: pointer; + padding-left: 10px; + + :hover { + background: ${({ theme }) => theme.colors.primary[3]}; + } +`; + +export default UserAvatar; diff --git a/browser/components/shared/userProfileButton/UserProfileButton.js b/browser/components/shared/userProfileButton/UserProfileButton.js new file mode 100644 index 0000000..d5f48c5 --- /dev/null +++ b/browser/components/shared/userProfileButton/UserProfileButton.js @@ -0,0 +1,111 @@ +import styled from "styled-components"; +import { useState, useEffect, useRef } from 'react'; +import { usePopper } from 'react-popper'; +import {connect} from 'react-redux' +import PropTypes from 'prop-types'; +import UserAvatar from "./UserAvatar"; +import UserProfileOptionsList from "./UserProfileOptionsList"; + + +function UserProfileButton(props) { + + const [showPopper, setShowPopper] = useState(false); + const buttonRef = useRef(null); + const popperRef = useRef(null); + const [arrowRef, setArrowRef] = useState(null); + const { styles, attributes } = usePopper( + buttonRef.current, + popperRef.current, + { + modifiers: [ + { + name: "arrow", + options: { + element: arrowRef + } + }, + { + name: "offset", + options: { + offset: [0, 3] + } + }, + { + name: "preventOverflow", + options: { + altAxis: true, + padding: 10 + } + } + ] + } + ); + + useEffect(() => { + document.addEventListener("mousedown", handleDocumentClick); + return () => { + document.removeEventListener("mousedown", handleDocumentClick); + }; + }, []); + + return ( + <> + <div ref={buttonRef} onClick={() => setShowPopper(!showPopper)}> + <UserAvatar /> + </div> + + { showPopper ? ( + <PopperContainer + ref={popperRef} + style={styles.popper} + {...attributes.popper} + > + <div ref={setArrowRef} style={styles.arrow} id="arrow" /> + <UserProfileOptionsList /> + </PopperContainer> + ) : null } + + </> + ); + + function handleDocumentClick(event) { + if (buttonRef.current.contains(event.target) || + (popperRef.current && popperRef.current.contains(event.target))) { + return; + } + setShowPopper(false); + } +} + +const PopperContainer = styled.div` + box-shadow: ${({ theme }) => theme.layout.boxShadow}; + border-radius: ${({ theme }) => theme.layout.borderRadius.small}px; + background: ${({ theme }) => theme.colors.gray[5]}; + padding: ${({ theme }) => theme.layout.gutter / 2}px; + text-align: left; + + #arrow { + position: absolute; + width: 10px; + height: 10px; + &:after { + content: " "; + background-color: ${({ theme }) => theme.colors.gray[5]}; + position: absolute; + top: -20px; + left: 0; + transform: rotate(45deg); + width: 10px; + height: 10px; + } + } + + &[data-popper-placement^='top'] > #arrow { + bottom: -30px; + :after { + box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); + } + } +`; + +export default UserProfileButton; diff --git a/browser/components/shared/userProfileButton/UserProfileOptionsList.js b/browser/components/shared/userProfileButton/UserProfileOptionsList.js new file mode 100644 index 0000000..0cc3e0d --- /dev/null +++ b/browser/components/shared/userProfileButton/UserProfileOptionsList.js @@ -0,0 +1,64 @@ +import styled from "styled-components"; +import { connect } from "react-redux"; +import { showSubscription } from "../../../helpers/show_subscriptionDialogue"; +import { showLoginModal } from "../../../helpers/show_loginmodal"; +import { LOGIN_MODAL_DIALOG_PREFIX } from "../../../constants"; + +function UserProfileOptionsList(props) { + return ( + <Root> + <li + onClick={() => { + window.open("https://admin.calypso.watsonc.dk", "_blank"); + }} + > + Min profil + </li> + <li onClick={showSubscription}>Abonnement</li> + {/* <li id="btn-reset">Nulstil</li> */} + <li + onClick={() => { + window.open("http://watsonc.dk", "_blank"); + }} + > + Watsonc.dk + </li> + <li onClick={() => $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("show")}> + Log ud + {/* <a id="session" href="#" data-toggle="modal" data-target="#login-modal" onClick={() => {}}> + Log ud + </a> */} + </li> + </Root> + ); +} + +const Root = styled.ul` + background: ${({ theme }) => theme.colors.gray[5]}; + font: ${({ theme }) => theme.fonts.body}; + list-style: none; + margin: 0; + padding: 0; + + li { + padding: ${({ theme }) => theme.layout.gutter / 4}px + ${({ theme }) => theme.layout.gutter / 2}px; + border-radius: ${({ theme }) => theme.layout.borderRadius.small}px; + cursor: pointer; + color: ${({ theme }) => theme.colors.gray[1]}; + &:hover { + background: ${({ theme }) => theme.colors.gray[4]}; + } + } +`; + +const mapStateToProps = (state) => ({}); + +const mapDispatchToProps = (dispatch) => ({ + setDashboardMode: (key) => dispatch(setDashboardMode(key)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(UserProfileOptionsList); diff --git a/browser/components/withDragDropContext.js b/browser/components/withDragDropContext.js deleted file mode 100644 index 9461f90..0000000 --- a/browser/components/withDragDropContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import {DragDropContext} from 'react-dnd'; -var MultiBackend = require('react-dnd-multi-backend').default; -var HTML5toTouch = require('react-dnd-multi-backend/lib/HTML5toTouch').default; // or any other pipeline - -export default DragDropContext(MultiBackend(HTML5toTouch)); \ No newline at end of file diff --git a/browser/constants.js b/browser/constants.js index a2f291f..d92f894 100644 --- a/browser/constants.js +++ b/browser/constants.js @@ -1,15 +1,20 @@ const LAYER_NAMES = [ - `v:system.all`, // 0 - `v:sensor.sensordata_without_correction`, // 1 Calypso stations - `chemicals.boreholes_time_series_without_chemicals`, // 2 Raster layer with all boreholes - `v:chemicals.pesticidoverblik`, // 3 - `chemicals.pesticidoverblik_raster` // 4 + `v:system.all`, // 0 + `v:sensor.sensordata_without_correction`, // 1 Calypso stations + `chemicals.boreholes_time_series_without_chemicals`, // 2 Raster layer with all boreholes + `v:chemicals.pesticidoverblik`, // 3 + `chemicals.pesticidoverblik_raster`, // 4 ]; +const CUSTOM_LAYER_NAME = "Særskilte"; +const USER_LAYER_NAME = "Brugerspecifikke lag"; + const WATER_LEVEL_KEY = `99999`; const SELECT_CHEMICAL_DIALOG_PREFIX = `watsonc-select-chemical-dialog`; +const LOGIN_MODAL_DIALOG_PREFIX = `watsonc-login-modal`; + const TEXT_FIELD_DIALOG_PREFIX = `watsonc-text-field-dialog`; const LIMIT_CHAR = `<`; @@ -20,424 +25,417 @@ const VIEW_ROW = 1; const FREE_PLAN_MAX_TIME_SERIES_COUNT = 4; const FREE_PLAN_MAX_PROFILES_COUNT = 1; -const KOMMUNER =[ - { - "komkode": "580", - "komnavn": "Aabenraa" - }, - { - "komkode": "851", - "komnavn": "Aalborg" - }, - { - "komkode": "751", - "komnavn": "Aarhus" - }, - { - "komkode": "492", - "komnavn": "Ærø" - }, - { - "komkode": "165", - "komnavn": "Albertslund" - }, - { - "komkode": "201", - "komnavn": "Allerød" - }, - { - "komkode": "420", - "komnavn": "Assens" - }, - { - "komkode": "151", - "komnavn": "Ballerup" - }, - { - "komkode": "530", - "komnavn": "Billund" - }, - { - "komkode": "400", - "komnavn": "Bornholm" - }, - { - "komkode": "153", - "komnavn": "Brøndby" - }, - { - "komkode": "810", - "komnavn": "Brønderslev" - }, - { - "komkode": "411", - "komnavn": "Christiansø" - }, - { - "komkode": "155", - "komnavn": "Dragør" - }, - { - "komkode": "240", - "komnavn": "Egedal" - }, - { - "komkode": "561", - "komnavn": "Esbjerg" - }, - { - "komkode": "430", - "komnavn": "Faaborg-Midtfyn" - }, - { - "komkode": "563", - "komnavn": "Fanø" - }, - { - "komkode": "710", - "komnavn": "Favrskov" - }, - { - "komkode": "320", - "komnavn": "Faxe" - }, - { - "komkode": "210", - "komnavn": "Fredensborg" - }, - { - "komkode": "607", - "komnavn": "Fredericia" - }, - { - "komkode": "147", - "komnavn": "Frederiksberg" - }, - { - "komkode": "813", - "komnavn": "Frederikshavn" - }, - { - "komkode": "250", - "komnavn": "Frederikssund" - }, - { - "komkode": "190", - "komnavn": "Furesø" - }, - { - "komkode": "157", - "komnavn": "Gentofte" - }, - { - "komkode": "159", - "komnavn": "Gladsaxe" - }, - { - "komkode": "161", - "komnavn": "Glostrup" - }, - { - "komkode": "253", - "komnavn": "Greve" - }, - { - "komkode": "270", - "komnavn": "Gribskov" - }, - { - "komkode": "376", - "komnavn": "Guldborgsund" - }, - { - "komkode": "510", - "komnavn": "Haderslev" - }, - { - "komkode": "260", - "komnavn": "Halsnæs" - }, - { - "komkode": "766", - "komnavn": "Hedensted" - }, - { - "komkode": "217", - "komnavn": "Helsingør" - }, - { - "komkode": "163", - "komnavn": "Herlev" - }, - { - "komkode": "657", - "komnavn": "Herning" - }, - { - "komkode": "219", - "komnavn": "Hillerød" - }, - { - "komkode": "860", - "komnavn": "Hjørring" - }, - { - "komkode": "169", - "komnavn": "Høje-Taastrup" - }, - { - "komkode": "316", - "komnavn": "Holbæk" - }, - { - "komkode": "661", - "komnavn": "Holstebro" - }, - { - "komkode": "615", - "komnavn": "Horsens" - }, - { - "komkode": "223", - "komnavn": "Hørsholm" - }, - { - "komkode": "167", - "komnavn": "Hvidovre" - }, - { - "komkode": "756", - "komnavn": "Ikast-Brande" - }, - { - "komkode": "183", - "komnavn": "Ishøj" - }, - { - "komkode": "849", - "komnavn": "Jammerbugt" - }, - { - "komkode": "326", - "komnavn": "Kalundborg" - }, - { - "komkode": "440", - "komnavn": "Kerteminde" - }, - { - "komkode": "101", - "komnavn": "København" - }, - { - "komkode": "259", - "komnavn": "Køge" - }, - { - "komkode": "621", - "komnavn": "Kolding" - }, - { - "komkode": "825", - "komnavn": "Læsø" - }, - { - "komkode": "482", - "komnavn": "Langeland" - }, - { - "komkode": "350", - "komnavn": "Lejre" - }, - { - "komkode": "665", - "komnavn": "Lemvig" - }, - { - "komkode": "360", - "komnavn": "Lolland" - }, - { - "komkode": "173", - "komnavn": "Lyngby-Taarbæk" - }, - { - "komkode": "846", - "komnavn": "Mariagerfjord" - }, - { - "komkode": "410", - "komnavn": "Middelfart" - }, - { - "komkode": "773", - "komnavn": "Morsø" - }, - { - "komkode": "370", - "komnavn": "Næstved" - }, - { - "komkode": "707", - "komnavn": "Norddjurs" - }, - { - "komkode": "480", - "komnavn": "Nordfyns" - }, - { - "komkode": "450", - "komnavn": "Nyborg" - }, - { - "komkode": "727", - "komnavn": "Odder" - }, - { - "komkode": "461", - "komnavn": "Odense" - }, - { - "komkode": "306", - "komnavn": "Odsherred" - }, - { - "komkode": "730", - "komnavn": "Randers" - }, - { - "komkode": "840", - "komnavn": "Rebild" - }, - { - "komkode": "760", - "komnavn": "Ringkøbing-Skjern" - }, - { - "komkode": "329", - "komnavn": "Ringsted" - }, - { - "komkode": "175", - "komnavn": "Rødovre" - }, - { - "komkode": "265", - "komnavn": "Roskilde" - }, - { - "komkode": "230", - "komnavn": "Rudersdal" - }, - { - "komkode": "741", - "komnavn": "Samsø" - }, - { - "komkode": "740", - "komnavn": "Silkeborg" - }, - { - "komkode": "746", - "komnavn": "Skanderborg" - }, - { - "komkode": "779", - "komnavn": "Skive" - }, - { - "komkode": "330", - "komnavn": "Slagelse" - }, - { - "komkode": "269", - "komnavn": "Solrød" - }, - { - "komkode": "540", - "komnavn": "Sønderborg" - }, - { - "komkode": "340", - "komnavn": "Sorø" - }, - { - "komkode": "336", - "komnavn": "Stevns" - }, - { - "komkode": "671", - "komnavn": "Struer" - }, - { - "komkode": "479", - "komnavn": "Svendborg" - }, - { - "komkode": "706", - "komnavn": "Syddjurs" - }, - { - "komkode": "185", - "komnavn": "Tårnby" - }, - { - "komkode": "787", - "komnavn": "Thisted" - }, - { - "komkode": "550", - "komnavn": "Tønder" - }, - { - "komkode": "187", - "komnavn": "Vallensbæk" - }, - { - "komkode": "573", - "komnavn": "Varde" - }, - { - "komkode": "575", - "komnavn": "Vejen" - }, - { - "komkode": "630", - "komnavn": "Vejle" - }, - { - "komkode": "820", - "komnavn": "Vesthimmerlands" - }, - { - "komkode": "791", - "komnavn": "Viborg" - }, - { - "komkode": "390", - "komnavn": "Vordingborg" - } -] - - -const Variants = { - None: 'None', - Primary: 'Primary', - Secondary: 'Secondary', - Tertiary: 'Tertiary', -}; - +const KOMMUNER = [ + { + komkode: "580", + komnavn: "Aabenraa", + }, + { + komkode: "851", + komnavn: "Aalborg", + }, + { + komkode: "751", + komnavn: "Aarhus", + }, + { + komkode: "492", + komnavn: "Ærø", + }, + { + komkode: "165", + komnavn: "Albertslund", + }, + { + komkode: "201", + komnavn: "Allerød", + }, + { + komkode: "420", + komnavn: "Assens", + }, + { + komkode: "151", + komnavn: "Ballerup", + }, + { + komkode: "530", + komnavn: "Billund", + }, + { + komkode: "400", + komnavn: "Bornholm", + }, + { + komkode: "153", + komnavn: "Brøndby", + }, + { + komkode: "810", + komnavn: "Brønderslev", + }, + { + komkode: "411", + komnavn: "Christiansø", + }, + { + komkode: "155", + komnavn: "Dragør", + }, + { + komkode: "240", + komnavn: "Egedal", + }, + { + komkode: "561", + komnavn: "Esbjerg", + }, + { + komkode: "430", + komnavn: "Faaborg-Midtfyn", + }, + { + komkode: "563", + komnavn: "Fanø", + }, + { + komkode: "710", + komnavn: "Favrskov", + }, + { + komkode: "320", + komnavn: "Faxe", + }, + { + komkode: "210", + komnavn: "Fredensborg", + }, + { + komkode: "607", + komnavn: "Fredericia", + }, + { + komkode: "147", + komnavn: "Frederiksberg", + }, + { + komkode: "813", + komnavn: "Frederikshavn", + }, + { + komkode: "250", + komnavn: "Frederikssund", + }, + { + komkode: "190", + komnavn: "Furesø", + }, + { + komkode: "157", + komnavn: "Gentofte", + }, + { + komkode: "159", + komnavn: "Gladsaxe", + }, + { + komkode: "161", + komnavn: "Glostrup", + }, + { + komkode: "253", + komnavn: "Greve", + }, + { + komkode: "270", + komnavn: "Gribskov", + }, + { + komkode: "376", + komnavn: "Guldborgsund", + }, + { + komkode: "510", + komnavn: "Haderslev", + }, + { + komkode: "260", + komnavn: "Halsnæs", + }, + { + komkode: "766", + komnavn: "Hedensted", + }, + { + komkode: "217", + komnavn: "Helsingør", + }, + { + komkode: "163", + komnavn: "Herlev", + }, + { + komkode: "657", + komnavn: "Herning", + }, + { + komkode: "219", + komnavn: "Hillerød", + }, + { + komkode: "860", + komnavn: "Hjørring", + }, + { + komkode: "169", + komnavn: "Høje-Taastrup", + }, + { + komkode: "316", + komnavn: "Holbæk", + }, + { + komkode: "661", + komnavn: "Holstebro", + }, + { + komkode: "615", + komnavn: "Horsens", + }, + { + komkode: "223", + komnavn: "Hørsholm", + }, + { + komkode: "167", + komnavn: "Hvidovre", + }, + { + komkode: "756", + komnavn: "Ikast-Brande", + }, + { + komkode: "183", + komnavn: "Ishøj", + }, + { + komkode: "849", + komnavn: "Jammerbugt", + }, + { + komkode: "326", + komnavn: "Kalundborg", + }, + { + komkode: "440", + komnavn: "Kerteminde", + }, + { + komkode: "101", + komnavn: "København", + }, + { + komkode: "259", + komnavn: "Køge", + }, + { + komkode: "621", + komnavn: "Kolding", + }, + { + komkode: "825", + komnavn: "Læsø", + }, + { + komkode: "482", + komnavn: "Langeland", + }, + { + komkode: "350", + komnavn: "Lejre", + }, + { + komkode: "665", + komnavn: "Lemvig", + }, + { + komkode: "360", + komnavn: "Lolland", + }, + { + komkode: "173", + komnavn: "Lyngby-Taarbæk", + }, + { + komkode: "846", + komnavn: "Mariagerfjord", + }, + { + komkode: "410", + komnavn: "Middelfart", + }, + { + komkode: "773", + komnavn: "Morsø", + }, + { + komkode: "370", + komnavn: "Næstved", + }, + { + komkode: "707", + komnavn: "Norddjurs", + }, + { + komkode: "480", + komnavn: "Nordfyns", + }, + { + komkode: "450", + komnavn: "Nyborg", + }, + { + komkode: "727", + komnavn: "Odder", + }, + { + komkode: "461", + komnavn: "Odense", + }, + { + komkode: "306", + komnavn: "Odsherred", + }, + { + komkode: "730", + komnavn: "Randers", + }, + { + komkode: "840", + komnavn: "Rebild", + }, + { + komkode: "760", + komnavn: "Ringkøbing-Skjern", + }, + { + komkode: "329", + komnavn: "Ringsted", + }, + { + komkode: "175", + komnavn: "Rødovre", + }, + { + komkode: "265", + komnavn: "Roskilde", + }, + { + komkode: "230", + komnavn: "Rudersdal", + }, + { + komkode: "741", + komnavn: "Samsø", + }, + { + komkode: "740", + komnavn: "Silkeborg", + }, + { + komkode: "746", + komnavn: "Skanderborg", + }, + { + komkode: "779", + komnavn: "Skive", + }, + { + komkode: "330", + komnavn: "Slagelse", + }, + { + komkode: "269", + komnavn: "Solrød", + }, + { + komkode: "540", + komnavn: "Sønderborg", + }, + { + komkode: "340", + komnavn: "Sorø", + }, + { + komkode: "336", + komnavn: "Stevns", + }, + { + komkode: "671", + komnavn: "Struer", + }, + { + komkode: "479", + komnavn: "Svendborg", + }, + { + komkode: "706", + komnavn: "Syddjurs", + }, + { + komkode: "185", + komnavn: "Tårnby", + }, + { + komkode: "787", + komnavn: "Thisted", + }, + { + komkode: "550", + komnavn: "Tønder", + }, + { + komkode: "187", + komnavn: "Vallensbæk", + }, + { + komkode: "573", + komnavn: "Varde", + }, + { + komkode: "575", + komnavn: "Vejen", + }, + { + komkode: "630", + komnavn: "Vejle", + }, + { + komkode: "820", + komnavn: "Vesthimmerlands", + }, + { + komkode: "791", + komnavn: "Viborg", + }, + { + komkode: "390", + komnavn: "Vordingborg", + }, +]; export { - LAYER_NAMES, - WATER_LEVEL_KEY, - SELECT_CHEMICAL_DIALOG_PREFIX, - TEXT_FIELD_DIALOG_PREFIX, - LIMIT_CHAR, - VIEW_MATRIX, - VIEW_ROW, - FREE_PLAN_MAX_TIME_SERIES_COUNT, - FREE_PLAN_MAX_PROFILES_COUNT, - KOMMUNER, - Variants + LAYER_NAMES, + WATER_LEVEL_KEY, + SELECT_CHEMICAL_DIALOG_PREFIX, + CUSTOM_LAYER_NAME, + USER_LAYER_NAME, + LOGIN_MODAL_DIALOG_PREFIX, + TEXT_FIELD_DIALOG_PREFIX, + LIMIT_CHAR, + VIEW_MATRIX, + VIEW_ROW, + FREE_PLAN_MAX_TIME_SERIES_COUNT, + FREE_PLAN_MAX_PROFILES_COUNT, + KOMMUNER, }; diff --git a/browser/contexts/map/MapContext.js b/browser/contexts/map/MapContext.js new file mode 100644 index 0000000..aa58e27 --- /dev/null +++ b/browser/contexts/map/MapContext.js @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; + +const MapContext = React.createContext(); + +const MapProvider = props => { + const [selectedBoreholes, setSelectedBoreholes] = useState([]); + window.mapDataContext = { + selectedBoreholes, setSelectedBoreholes + } + + return ( + <MapContext.Provider value={{ selectedBoreholes, setSelectedBoreholes}}> + {props.children} + </MapContext.Provider> + ) +} + +function useMapContext() { + const context = React.useContext(MapContext); + if(context === undefined){ + throw new Error('useAuth must be used within a AuthProvider') + } + return context; + +} + +export { MapProvider, useMapContext } diff --git a/browser/contexts/project/ProjectContext.js b/browser/contexts/project/ProjectContext.js new file mode 100644 index 0000000..037d996 --- /dev/null +++ b/browser/contexts/project/ProjectContext.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const ProjectContext = React.createContext({ + activePlots: [], + setActivePlots: (plots) => {console.log("Setting active plots")}, + activeProfiles: [], + setActiveProfiles: (profiles) => {} +}); + +export default ProjectContext; diff --git a/browser/contexts/project/ProjectProvider.js b/browser/contexts/project/ProjectProvider.js new file mode 100644 index 0000000..893efbe --- /dev/null +++ b/browser/contexts/project/ProjectProvider.js @@ -0,0 +1,15 @@ +import { useState, useEffect } from 'react'; +import ProjectContext from './ProjectContext'; + +function ProjectProvider(props) { + const [activePlots, setActivePlots] = useState([]); + const [activeProfiles, setActiveProfiles] = useState([]); + + return ( + <ProjectContext.Provider value={{ activePlots, setActivePlots, activeProfiles, setActiveProfiles }}> + {props.children} + </ProjectContext.Provider> + ) +} + +export default ProjectProvider; diff --git a/browser/helpers/aggregate.js b/browser/helpers/aggregate.js new file mode 100644 index 0000000..11f6e0b --- /dev/null +++ b/browser/helpers/aggregate.js @@ -0,0 +1,46 @@ +import _ from "lodash"; +import moment from "moment"; + +const func_map = { + mean: _.meanBy, + sum: _.sumBy, +}; + +function aggregate(x, y, window, func) { + const grouped = _( + x.map((elem, index) => { + return { + x: moment(elem).startOf(window).format("YYYY-MM-DD HH:mm:ss.SSS"), + y: y[index], + }; + }) + ) + .groupBy("x") + .mapValues((item) => { + return func_map[func](item, "y"); + }) + .value(); + + var start = moment([2020, 1, 1]); + var end = moment([2020, 1, 1]); + var width = start.diff(end.add(1, window)); + const entries = Object.entries(grouped); + if (func === "mean") { + return { + x: entries.map((elem) => elem[0]), + y: entries.map((elem) => elem[1]), + }; + } else { + return { + x: entries.map((elem) => + moment(elem[0]) + .add(width / 2, "ms") + .format("YYYY-MM-DD HH:mm:ss.SSS") + ), + y: entries.map((elem) => elem[1]), + width: new Array(entries.length).fill(width), + }; + } +} + +export { aggregate }; diff --git a/browser/helpers/colors.js b/browser/helpers/colors.js new file mode 100644 index 0000000..9167ae0 --- /dev/null +++ b/browser/helpers/colors.js @@ -0,0 +1,16 @@ +function hexToRgbA(hex, opacity){ + var c; + if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){ + c= hex.substring(1).split(''); + if(c.length== 3){ + c= [c[0], c[0], c[1], c[1], c[2], c[2]]; + } + c= '0x'+c.join(''); + return 'rgba(' + [(c>>16)&255, (c>>8)&255, c&255, (opacity || 1)].join(',') + ')'; + } + return hex; +} + +export { + hexToRgbA, +} diff --git a/browser/helpers/common.js b/browser/helpers/common.js new file mode 100644 index 0000000..c5663f2 --- /dev/null +++ b/browser/helpers/common.js @@ -0,0 +1,15 @@ +function getNewPlotId(plots) { + let newId = plots.length + 1; + if (newId > 1) { + let existingPlot = plots.find((plot) => plot.id == `plot_${newId}`); + while (existingPlot != null) { + newId += 1; + existingPlot = plots.find((plot) => plot.id == `plot_${newId}`); + } + } + return newId; +} + +export { + getNewPlotId +} diff --git a/browser/helpers/show_loginmodal.js b/browser/helpers/show_loginmodal.js new file mode 100644 index 0000000..7a48e78 --- /dev/null +++ b/browser/helpers/show_loginmodal.js @@ -0,0 +1,25 @@ +import React from "react"; + +import { LOGIN_MODAL_DIALOG_PREFIX } from "./../constants"; +import { Provider } from "react-redux"; +import reduxStore from "./../redux/store"; +import ThemeProvider from "../themes/ThemeProvider"; +import LoginModal from "../components/LoginModal"; + +function showLoginModal() { + // const placeholderId = `${LOGIN_MODAL_DIALOG_PREFIX}-placeholder`; + + ReactDOM.render( + <div> + <Provider store={reduxStore}> + <ThemeProvider> + <LoginModal /> + </ThemeProvider> + </Provider> + </div>, + document.getElementById(LOGIN_MODAL_DIALOG_PREFIX) + ); + $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("show"); +} + +export { showLoginModal }; diff --git a/browser/helpers/show_subscriptionDialogue.js b/browser/helpers/show_subscriptionDialogue.js new file mode 100644 index 0000000..c5a8e24 --- /dev/null +++ b/browser/helpers/show_subscriptionDialogue.js @@ -0,0 +1,56 @@ +import SubscriptionDialogue from "../components/SubscriptionDialogue"; +import { setDashboardMode } from "../redux/actions"; +var ReactDOM = require("react-dom"); +const config = require("../../../../config/config.js"); + +const session = require("./../../../session/browser/index"); + +function showSubscription() { + var subscriptionDialoguePlaceholderId = "upgrade-modal"; + const onCloseButtonClick = () => { + $("#" + subscriptionDialoguePlaceholderId).modal("hide"); + }; + + const openAbonnement = () => { + setDashboardMode("minimized"); + $("#watsonc-limits-reached-text").hide(); + ReactDOM.render( + <SubscriptionDialogue + onCloseButtonClick={onCloseButtonClick} + session={session} + />, + document.getElementById(subscriptionDialoguePlaceholderId) + ); + $("#" + subscriptionDialoguePlaceholderId).modal("show"); + }; + + openAbonnement(); +} + +function showSubscriptionIfFree(other_logic = true) { + const isFree = session.getProperties()?.["license"] !== "premium"; + const email = session?.getEmail(); + + const premiumEmailExtensions = + JSON.parse( + JSON.stringify(config?.extensionConfig?.watsonc?.premiumEmailExtensions) + ) || []; + + if (premiumEmailExtensions.length > 0 && email) { + const emailDomain = email.split("@")[1]; + premiumEmailExtensions.forEach((domain) => { + const regex = new RegExp(domain.replace(/\./g, "\\."), "i"); + if (regex.test(emailDomain)) { + return false; + } + }); + } + + if (isFree && other_logic) { + showSubscription(); + return true; + } + return false; +} + +export { showSubscription, showSubscriptionIfFree }; diff --git a/browser/index.js b/browser/index.js index a665850..28ccdae 100644 --- a/browser/index.js +++ b/browser/index.js @@ -1,62 +1,77 @@ -'use strict'; - -import {Provider} from 'react-redux'; - -import PlotManager from './PlotManager'; -import ModalComponent from './components/ModalComponent'; -import DashboardComponent from './components/DashboardComponent'; -import MenuTimeSeriesComponent from './components/MenuTimeSeriesComponent'; -import MenuDataSourceAndTypeSelectorComponent from './components/MenuDataSourceAndTypeSelectorComponent'; -import MenuProfilesComponent from './components/MenuProfilesComponent'; -import IntroModal from './components/IntroModal'; -import Modal from './components/modal/Modal'; -import AnalyticsComponent from './components/AnalyticsComponent'; -import {LAYER_NAMES, WATER_LEVEL_KEY, KOMMUNER} from './constants'; -import trustedIpAddresses from './trustedIpAddresses'; - - -import reduxStore from './redux/store'; -import {setAuthenticated} from './redux/actions'; - -const symbolizer = require('./symbolizer'); - -const utils = require('./utils'); - -const evaluateMeasurement = require('./evaluateMeasurement'); +"use strict"; + +import { Provider } from "react-redux"; +import "regenerator-runtime/runtime"; + +import AnalyticsComponent from "./components/AnalyticsComponent"; +import MenuProfilesComponent from "./components/MenuProfilesComponent"; +import { + KOMMUNER, + LAYER_NAMES, + WATER_LEVEL_KEY, + LOGIN_MODAL_DIALOG_PREFIX, +} from "./constants"; +import trustedIpAddresses from "./trustedIpAddresses"; +import ThemeProvider from "./themes/ThemeProvider"; +import DataSelectorDialogue from "./components/dataselector/DataSelectorDialogue"; +import DashboardWrapper from "./components/DashboardWrapper"; +import TopBar from "./components/TopBar"; +import LoginModal from "./components/LoginModal"; +import { showSubscriptionIfFree } from "./helpers/show_subscriptionDialogue"; + +import reduxStore from "./redux/store"; +import { + addBoreholeFeature, + clearBoreholeFeatures, + setAuthenticated, + setBoreholeFeatures, + setCategories, +} from "./redux/actions"; + +const symbolizer = require("./symbolizer"); + +const utils = require("./utils"); const MODULE_NAME = `watsonc`; /** * The feature dialog constants */ -const FEATURE_CONTAINER_ID = 'watsonc-features-dialog'; -const FORM_FEATURE_CONTAINER_ID = 'watsonc-features-dialog-form'; +const FEATURE_CONTAINER_ID = "watsonc-features-dialog"; /** * The plots dialog constants */ -const DASHBOARD_CONTAINER_ID = 'watsonc-plots-dialog-form'; +const DASHBOARD_CONTAINER_ID = "watsonc-plots-dialog-form"; let PLOTS_ID = `#` + DASHBOARD_CONTAINER_ID; /** * * @type {*|exports|module.exports} */ -var cloud, switchLayer, backboneEvents, session = false; - +var cloud, + switchLayer, + backboneEvents, + session = false; /** * * @type {*|exports|module.exports} */ var layerTree, layers, anchor, state, urlparser; -var React = require('react'); +var React = require("react"); -var ReactDOM = require('react-dom'); +var ReactDOM = require("react-dom"); +var ReactDOMServer = require("react-dom/server"); -let dashboardComponentInstance = false, modalComponentInstance = false, infoModalInstance = false; +let dashboardComponentInstance = false, + modalComponentInstance = false, + infoModalInstance = false; +let dashboardShellInstance = false; -let lastSelectedChemical = false, categoriesOverall = false, enabledLoctypeIds = []; +let lastSelectedChemical = false, + categoriesOverall = false, + enabledLoctypeIds = []; let _self = false; @@ -67,9 +82,6 @@ let lastTitleAsLink = null; let dataSource = []; let boreholesDataSource = []; -let waterLevelDataSource = []; - -let previousZoom = -1; let store; @@ -79,614 +91,821 @@ let names = {}; let currentRasterLayer = null; -const DATA_SOURCES = [{ - originalLayerKey: LAYER_NAMES[0], - additionalKey: ``, - title: __(`Jupiter drilling`) -}, { - originalLayerKey: LAYER_NAMES[1], - additionalKey: `1`, - title: "Online stationer" -}, { - originalLayerKey: LAYER_NAMES[1], - additionalKey: `3`, - title: "Online stationer" -}, { - originalLayerKey: LAYER_NAMES[1], - additionalKey: `4`, - title: "Online stationer" -}, { - originalLayerKey: LAYER_NAMES[3], - additionalKey: ``, - title: "Pesticidoverblik" -}]; - /** * * @type {{set: module.exports.set, init: module.exports.init}} */ module.exports = module.exports = { - set: function (o) { - cloud = o.cloud; - switchLayer = o.switchLayer; - backboneEvents = o.backboneEvents; - layers = o.layers; - layerTree = o.layerTree; - anchor = o.anchor; - state = o.state; - urlparser = o.urlparser; - if (o.extensions && o.extensions.session) { - session = o.extensions.session.index; - } - - _self = this; - return this; - }, - - init: function () { - state.listenTo(MODULE_NAME, _self); - state.listen(MODULE_NAME, `plotsUpdate`); - state.listen(MODULE_NAME, `chemicalChange`); - state.listen(MODULE_NAME, `enabledLoctypeIdsChange`); - - this.initializeSearchBar(); - - let queryParams = new URLSearchParams(window.location.search); - let licenseToken = queryParams.get('license'); - let license = null; - - if (licenseToken) { - license = JSON.parse(base64.decode(licenseToken.split('.')[1])); - if (typeof license === 'object') { - license = license.license; - - } - } - if (trustedIpAddresses.includes(window._vidiIp)) { - license = "premium"; - } - - if (license === "premium") { - $("#watsonc-licens-btn1").html(""); - $("#watsonc-licens-btn2").html("Valgt"); - $("#watsonc-licens-btn2").attr("disabled", true); - $("#watsonc-licens-btn2").css("pointer-events", "none"); - - } else { - $("#watsonc-licens-btn1").html("Valgt"); - $("#watsonc-licens-btn2").html("Vælg"); - } - - $("#btn-plan").on("click", () => { - $('#watsonc-limits-reached-text').hide(); - $('#upgrade-modal').modal('show'); - }) - - backboneEvents.get().on(`session:authChange`, authenticated => { - reduxStore.dispatch(setAuthenticated(authenticated)); - }); + set: function (o) { + cloud = o.cloud; + switchLayer = o.switchLayer; + backboneEvents = o.backboneEvents; + layers = o.layers; + layerTree = o.layerTree; + anchor = o.anchor; + state = o.state; + urlparser = o.urlparser; + if (o.extensions && o.extensions.session) { + session = o.extensions.session.index; + } + _self = this; + return this; + }, - backboneEvents.get().on("ready:meta", function () { - setTimeout(() => { - $(".panel-title a").trigger("click"); - }, 1000); + init: function () { + state.listenTo(MODULE_NAME, _self); + state.listen(MODULE_NAME, `plotsUpdate`); + state.listen(MODULE_NAME, `chemicalChange`); + state.listen(MODULE_NAME, `enabledLoctypeIdsChange`); - }); + state.getModuleState(MODULE_NAME).then((initialState) => { + _self.applyState(initialState); + }); - $('#watsonc-plots-dialog-form').click(function () { - $('#watsonc-plots-dialog-form').css('z-index', '1000'); + this.initializeSearchBar(); - if ($('#watsonc-features-dialog').css('z-index') === '1000') { - $('#watsonc-features-dialog').css('z-index', '100'); - } else { - $('#watsonc-features-dialog').css('z-index', '10'); - } + let queryParams = new URLSearchParams(window.location.search); + let licenseToken = queryParams.get("license"); + let license = null; - if ($('#search-ribbon').css('z-index') === '1000') { - $('#search-ribbon').css('z-index', '100'); - } else { - $('#search-ribbon').css('z-index', '10'); - } - }); - - $('#watsonc-features-dialog').click(function () { - _self.bringFeaturesDialogToFront(); - }); - - $('#search-ribbon').click(function () { - if ($('#watsonc-plots-dialog-form').css('z-index') === '1000') { - $('#watsonc-plots-dialog-form').css('z-index', '100'); - } else { - $('#watsonc-plots-dialog-form').css('z-index', '10'); - } - - if ($('#watsonc-features-dialog').css('z-index') === '1000') { - $('#watsonc-features-dialog').css('z-index', '100'); - } else { - $('#watsonc-features-dialog').css('z-index', '10'); - } - - $('#search-ribbon').css('z-index', '1000'); - }); - - var lc = L.control.locate({ - drawCircle: false - }).addTo(cloud.get().map); + if (licenseToken) { + license = JSON.parse(base64.decode(licenseToken.split(".")[1])); + if (typeof license === "object") { + license = license.license; + } + } + if (trustedIpAddresses.includes(window._vidiIp)) { + license = "premium"; + } - $(`#search-border`).trigger(`click`); + if (license === "premium") { + $("#watsonc-licens-btn1").html(""); + $("#watsonc-licens-btn2").html("Valgt"); + $("#watsonc-licens-btn2").attr("disabled", true); + $("#watsonc-licens-btn2").css("pointer-events", "none"); + } else { + $("#watsonc-licens-btn1").html("Valgt"); + $("#watsonc-licens-btn2").html("Vælg"); + } - $(`#js-open-state-snapshots-panel`).click(() => { - $(`[href="#state-snapshots-content"]`).trigger(`click`); - }); + $("#btn-plan").on("click", () => { + $("#watsonc-limits-reached-text").hide(); + $("#upgrade-modal").modal("show"); + }); - $(`#js-open-watsonc-panel`).click(() => { - $(`[href="#watsonc-content"]`).trigger(`click`); - }); + backboneEvents.get().on(`session:authChange`, (authenticated) => { + reduxStore.dispatch(setAuthenticated(authenticated)); + }); + backboneEvents.get().on("ready:meta", function () { + setTimeout(() => { $(".panel-title a").trigger("click"); - - // Turn on raster layer with all boreholes. - switchLayer.init(LAYER_NAMES[2], true, true, false); - - $.ajax({ - url: '/api/sql/jupiter?q=SELECT * FROM codes.compunds_view&base64=false&lifetime=10800', - scriptCharset: "utf-8", - success: function (response) { - if (`features` in response) { - categories = {}; - limits = {}; - - response.features.map(function (v) { - categories[v.properties.kategori.trim()] = {}; - names[v.properties.compundno] = v.properties.navn; - }); - - names[WATER_LEVEL_KEY] = "Vandstand"; - - for (var key in categories) { - response.features.map(function (v) { - if (key === v.properties.kategori) { - categories[key][v.properties.compundno] = v.properties.navn; - limits["_" + v.properties.compundno] = [v.properties.attention || 0, v.properties.limit || 0]; - } - }); - } - - _self.buildBreadcrumbs(); - - categoriesOverall = {}; - categoriesOverall[LAYER_NAMES[0]] = categories; - categoriesOverall[LAYER_NAMES[0]]["Vandstand"] = {"0": WATER_LEVEL_KEY}; - categoriesOverall[LAYER_NAMES[1]] = {"Vandstand": {"0": WATER_LEVEL_KEY}}; - - if (infoModalInstance) infoModalInstance.setCategories(categoriesOverall); - - // Setup menu - let dd = $('li .dropdown-toggle'); - dd.on('click', function (event) { - $(".dropdown-top").not($(this).parent()).removeClass('open'); - $('.dropdown-submenu').removeClass('open'); - $(this).parent().toggleClass('open'); - }); - - // Open intro modal only if there is no predefined state - if (!urlparser.urlVars || !urlparser.urlVars.state) { - _self.openMenuModal(true); - } else { - _self.openMenuModal(false); - } - - backboneEvents.get().trigger(`${MODULE_NAME}:initialized`); - } else { - console.error(`Unable to request codes.compunds`); - } - }, - error: function () { - } - }); - - state.getState().then(applicationState => { - $(PLOTS_ID).attr(`style`, ` + }, 1000); + }); + + $("#watsonc-plots-dialog-form").click(function () { + $("#watsonc-plots-dialog-form").css("z-index", "1000"); + + if ($("#watsonc-features-dialog").css("z-index") === "1000") { + $("#watsonc-features-dialog").css("z-index", "100"); + } else { + $("#watsonc-features-dialog").css("z-index", "10"); + } + + if ($("#search-ribbon").css("z-index") === "1000") { + $("#search-ribbon").css("z-index", "100"); + } else { + $("#search-ribbon").css("z-index", "10"); + } + }); + + $("#watsonc-features-dialog").click(function () { + _self.bringFeaturesDialogToFront(); + }); + + $("#watsonc-data-sources").on("click", () => { + $("#watsonc-menu-dialog").modal("show"); + }); + + $("#search-ribbon").click(function () { + if ($("#watsonc-plots-dialog-form").css("z-index") === "1000") { + $("#watsonc-plots-dialog-form").css("z-index", "100"); + } else { + $("#watsonc-plots-dialog-form").css("z-index", "10"); + } + + if ($("#watsonc-features-dialog").css("z-index") === "1000") { + $("#watsonc-features-dialog").css("z-index", "100"); + } else { + $("#watsonc-features-dialog").css("z-index", "10"); + } + + $("#search-ribbon").css("z-index", "1000"); + }); + + var lc = L.control + .locate({ + drawCircle: false, + }) + .addTo(cloud.get().map); + + $(`#search-border`).trigger(`click`); + + $(`#js-open-state-snapshots-panel`).click(() => { + //$(`[href="#state-snapshots-content"]`).trigger(`click`); + }); + + $(`#state-snapshots-content`).click((e) => { + if (showSubscriptionIfFree()) { + var elem = document.getElementById("state-snapshots"); + elem.style.pointerEvents = "none"; + } + + //$(`[href="#state-snapshots-content"]`).trigger(`click`); + }); + + $("#projects-trigger").click((e) => { + e.preventDefault(); + //reduxStore.dispatch(setDashboardContent('projects')); + }); + + $("#main-tabs a").on("click", function (e) { + $("#module-container.slide-right").css("right", "0"); + }); + + $(document).on( + "click", + "#module-container .modal-header button", + function (e) { + e.preventDefault(); + $("#module-container.slide-right").css("right", "-" + 466 + "px"); + $("#side-panel ul li").removeClass("active"); + $("#search-ribbon").css("right", "-550px"); + $("#pane").css("right", "0"); + $("#map").css("width", "100%"); + } + ); + + $(`#js-open-watsonc-panel`).click(() => { + $(`[href="#watsonc-content"]`).trigger(`click`); + }); + + $(".panel-title a").trigger("click"); + + // Turn on raster layer with all boreholes. + // switchLayer.init(LAYER_NAMES[2], true, true, false); + ReactDOM.render( + <ThemeProvider> + <Provider store={reduxStore}> + <TopBar backboneEvents={backboneEvents} session={session} /> + </Provider> + </ThemeProvider>, + document.getElementById("top-bar") + ); + + ReactDOM.render( + <Provider store={reduxStore}> + <ThemeProvider> + <LoginModal + session={session} + backboneEvents={backboneEvents} + urlparser={urlparser} + /> + </ThemeProvider> + </Provider>, + document.getElementById(LOGIN_MODAL_DIALOG_PREFIX) + ); + $("#" + LOGIN_MODAL_DIALOG_PREFIX).modal("hide"); + + $.ajax({ + url: "/api/sql/jupiter?q=SELECT * FROM codes.compunds_view&base64=false&lifetime=10800", + scriptCharset: "utf-8", + success: function (response) { + if (`features` in response) { + categories = {}; + limits = {}; + + response.features.map(function (v) { + categories[v.properties.kategori.trim()] = {}; + names[v.properties.compundno] = v.properties.our_name; + }); + + names[WATER_LEVEL_KEY] = "Vandstand"; + + for (var key in categories) { + response.features.map(function (v) { + if (key === v.properties.kategori) { + categories[key][v.properties.compundno] = v.properties.our_name; + limits["_" + v.properties.compundno] = [ + v.properties.attention || 0, + v.properties.limit || 0, + ]; + } + }); + } + reduxStore.dispatch(setLimits(limits)); + + _self.buildBreadcrumbs(); + + categoriesOverall = {}; + categoriesOverall[LAYER_NAMES[0]] = categories; + categoriesOverall[LAYER_NAMES[0]]["Vandstand"] = { + 0: WATER_LEVEL_KEY, + }; + categoriesOverall[LAYER_NAMES[1]] = { + Vandstand: { 0: WATER_LEVEL_KEY }, + }; + + if (infoModalInstance) + infoModalInstance.setCategories(categoriesOverall); + reduxStore.dispatch(setCategories(categories)); + + // Setup menu + let dd = $("li .dropdown-toggle"); + dd.on("click", function (event) { + $(".dropdown-top").not($(this).parent()).removeClass("open"); + $(".dropdown-submenu").removeClass("open"); + $(this).parent().toggleClass("open"); + }); + + // Open intro modal only if there is no predefined state + if (!urlparser.urlVars || !urlparser.urlVars.state) { + _self.openMenuModal(true); + } else { + _self.openMenuModal(false); + } + + backboneEvents.get().trigger(`${MODULE_NAME}:initialized`); + } else { + console.error(`Unable to request codes.compunds`); + } + }, + error: function () {}, + }); + + state.getState().then((applicationState) => { + $(PLOTS_ID).attr( + `style`, + ` margin-bottom: 0px; - width: 80%; - max-width: 80%; - right: 10%; - left: 10%; - bottom: 0px;`); - - LAYER_NAMES.map(layerName => { - layerTree.setOnEachFeature(layerName, (clickedFeature, layer) => { - layer.on("click", function (e) { - $("#" + FEATURE_CONTAINER_ID).animate({ - bottom: "0" - }, 500, function () { - $("#" + FEATURE_CONTAINER_ID).find(".expand-less").show(); - $("#" + FEATURE_CONTAINER_ID).find(".expand-more").hide(); - }); - - let intersectingFeatures = []; - if (e.latlng) { - var clickBounds = L.latLngBounds(e.latlng, e.latlng); - let res = [156543.033928, 78271.516964, 39135.758482, 19567.879241, 9783.9396205, - 4891.96981025, 2445.98490513, 1222.99245256, 611.496226281, 305.748113141, 152.87405657, - 76.4370282852, 38.2185141426, 19.1092570713, 9.55462853565, 4.77731426782, 2.38865713391, - 1.19432856696, 0.597164283478, 0.298582141739, 0.149291, 0.074645535]; - - let distance = 10 * res[cloud.get().getZoom()]; - - let mapObj = cloud.get().map; - for (var l in mapObj._layers) { - var overlay = mapObj._layers[l]; - if (overlay._layers) { - for (var f in overlay._layers) { - var feature = overlay._layers[f]; - var bounds; - if (feature.getBounds) { - bounds = feature.getBounds(); - } else if (feature._latlng) { - let circle = new L.circle(feature._latlng, {radius: distance}); - // DIRTY HACK - circle.addTo(mapObj); - bounds = circle.getBounds(); - circle.removeFrom(mapObj); - } - - try { - if (bounds && clickBounds.intersects(bounds) && overlay.id) { - intersectingFeatures.push(feature.feature); - } - } catch (e) { - console.log(e); - } - } - } - } - } else { - // In case marker "click" event was triggered from the code - intersectingFeatures.push(e.target.feature); - } - - let titleAsLink = false; - - if (layerName.indexOf(LAYER_NAMES[0]) > -1) { - titleAsLink = true; - } - - let clickedFeatureAlreadyDetected = false; - intersectingFeatures.map(feature => { - if (feature.properties.boreholeno === clickedFeature.properties.boreholeno) { - clickedFeatureAlreadyDetected = true; - } - }); - - if (clickedFeatureAlreadyDetected === false) intersectingFeatures.unshift(clickedFeature); - - let boreholes = []; - - intersectingFeatures.map((feature) => { - boreholes.push(feature.properties.boreholeno) - }); - - let qLayer; - if (layerName.indexOf(LAYER_NAMES[0]) > -1 || layerName.indexOf(LAYER_NAMES[3]) > -1) { - qLayer = "chemicals.boreholes_time_series_with_chemicals"; - } else { - qLayer = "sensor.sensordata_with_correction"; - // Filter NaN values, so SQL doesn't return type error - boreholes = boreholes.filter((v) => { - if (!isNaN(v)) { - return v; - } - }); - } - - // Lazy load features - $.ajax({ - url: "/api/sql/jupiter?srs=25832&q=SELECT * FROM " + qLayer + " WHERE boreholeno in('" + boreholes.join("','") + "')", - scriptCharset: "utf-8", - success: function (response) { - - dataSource = []; - boreholesDataSource = response.features; - dataSource = dataSource.concat(boreholesDataSource); - if (dashboardComponentInstance) { - dashboardComponentInstance.setDataSource(dataSource); - } - - - _self.createModal(response.features, false, titleAsLink, false); - if (!dashboardComponentInstance) { - throw new Error(`Unable to find the component instance`); - } - }, - error: function () { - } - }); - }); - }, "watsonc"); - - let svgCirclePart = symbolizer.getSymbol(layerName); - if (svgCirclePart) { - layerTree.setPointToLayer(layerName, (feature, latlng) => { - let renderIcon = true; - if (layerName === LAYER_NAMES[1]) { - if (feature.properties.loctypeid && - (enabledLoctypeIds.indexOf(parseInt(feature.properties.loctypeid) + '') === -1 && enabledLoctypeIds.indexOf(parseInt(feature.properties.loctypeid)) === -1)) { - renderIcon = false; - } - } else { - return L.circleMarker(latlng); - } - - if (renderIcon) { - let participatingIds = []; - if (dashboardComponentInstance) { - let plots = dashboardComponentInstance.getPlots(); - plots.map(plot => { - participatingIds = participatingIds.concat(_self.participatingIds(plot)); - }); - } - - let highlighted = (participatingIds.indexOf(feature.properties.boreholeno) > -1); - let localSvgCirclePart = symbolizer.getSymbol(layerName, { - online: feature.properties.status, - shape: feature.properties.loctypeid, - highlighted - }); - - let icon = L.icon({ - iconUrl: 'data:image/svg+xml;base64,' + btoa(localSvgCirclePart), - iconAnchor: [8, 33], - iconSize: [30, 30], - watsoncStatus: `default` - }); - - return L.marker(latlng, {icon}); - } else { - return null; - } - }); + width: 96%; + max-width: 96%; + right: 2%; + left: 2%; + bottom: 0px;` + ); + + LAYER_NAMES.map((layerName) => { + layerTree.setOnEachFeature( + layerName, + (clickedFeature, layer) => { + layer.on("click", (e) => { + $("#" + FEATURE_CONTAINER_ID).animate( + { + bottom: "0", + }, + 500, + function () { + $("#" + FEATURE_CONTAINER_ID) + .find(".expand-less") + .show(); + $("#" + FEATURE_CONTAINER_ID) + .find(".expand-more") + .hide(); } - }); - - // Renewing the already created store by rebuilding the layer tree - setTimeout(() => { - - setTimeout(() => { - layerTree.create(false, [], true).then(() => { - //layerTree.reloadLayer(LAYER_NAMES[0]); - if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[1]) > -1) { - layerTree.reloadLayer(LAYER_NAMES[1]); - } - if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[0]) > -1) { - layerTree.reloadLayer(LAYER_NAMES[0]); - } - if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[3]) > -1) { - layerTree.reloadLayer(LAYER_NAMES[3]); - } - }); - }, 500); - }, 100); - - const proceedWithInitialization = () => { - // Setting up feature dialog - $(`#` + FEATURE_CONTAINER_ID).find(".expand-less").on("click", function () { - $("#" + FEATURE_CONTAINER_ID).animate({ - bottom: (($("#" + FEATURE_CONTAINER_ID).height() * -1) + 30) + "px" - }, 500, function () { - $(`#` + FEATURE_CONTAINER_ID).find(".expand-less").hide(); - $(`#` + FEATURE_CONTAINER_ID).find(".expand-more").show(); - }); - }); - - $(`#` + FEATURE_CONTAINER_ID).find(".expand-more").on("click", function () { - $("#" + FEATURE_CONTAINER_ID).animate({ - bottom: "0" - }, 500, function () { - $(`#` + FEATURE_CONTAINER_ID).find(".expand-less").show(); - $(`#` + FEATURE_CONTAINER_ID).find(".expand-more").hide(); - }); - }); - - $(`#` + FEATURE_CONTAINER_ID).find(".close-hide").on("click", function () { - $("#" + FEATURE_CONTAINER_ID).animate({ - bottom: "-100%" - }, 500, function () { - $(`#` + FEATURE_CONTAINER_ID).find(".expand-less").show(); - $(`#` + FEATURE_CONTAINER_ID).find(".expand-more").hide(); - }); - }); - - // Initializing data source and types selector - $(`[data-module-id="data-source-and-types-selector"]`).click(() => { - if ($(`#data-source-and-types-selector-content`).children().length === 0) { - try { - ReactDOM.render(<Provider store={reduxStore}> - <MenuDataSourceAndTypeSelectorComponent - onApply={_self.onApplyLayersAndChemical} - enabledLoctypeIds={enabledLoctypeIds} - urlparser={urlparser} - boreholes={layerTree.getActiveLayers().indexOf("_") > -1} // DIRTY HACK All raster layers has _ in name - layers={DATA_SOURCES}/> - </Provider>, document.getElementById(`data-source-and-types-selector-content`)); - } catch (e) { - console.log(e); - } - } - }); - - // Initializing TimeSeries management component - $(`[data-module-id="timeseries"]`).click(() => { - if ($(`#watsonc-timeseries`).children().length === 0) { - try { - ReactDOM.render(<Provider store={reduxStore}> - <MenuTimeSeriesComponent - backboneEvents={backboneEvents} - license={dashboardComponentInstance.getLicense()} - initialPlots={dashboardComponentInstance.getPlots()} - initialActivePlots={dashboardComponentInstance.getActivePlots()} - onPlotCreate={dashboardComponentInstance.handleCreatePlot} - onPlotDelete={dashboardComponentInstance.handleDeletePlot} - onPlotHighlight={dashboardComponentInstance.handleHighlightPlot} - onPlotShow={dashboardComponentInstance.handleShowPlot} - onPlotArchive={dashboardComponentInstance.handleArchivePlot} - onPlotHide={dashboardComponentInstance.handleHidePlot}/></Provider>, document.getElementById(`watsonc-timeseries`)); - } catch (e) { - console.error(e); - } - } - }); - - // Initializing profiles tab - if ($(`#profile-drawing-content`).length === 0) throw new Error(`Unable to get the profile drawing tab`); - - // Initializing TimeSeries management component - $(`[data-module-id="profile-drawing"]`).click(() => { - try { - ReactDOM.render(<Provider store={reduxStore}> - <MenuProfilesComponent - cloud={cloud} - backboneEvents={backboneEvents} - license={dashboardComponentInstance.getLicense()} - categories={categoriesOverall ? categoriesOverall : []} - initialProfiles={dashboardComponentInstance.getProfiles()} - initialActiveProfiles={dashboardComponentInstance.getActiveProfiles()} - onProfileCreate={dashboardComponentInstance.handleCreateProfile} - onProfileDelete={dashboardComponentInstance.handleDeleteProfile} - onProfileHighlight={dashboardComponentInstance.handleHighlightProfile} - onProfileShow={dashboardComponentInstance.handleShowProfile} - onProfileHide={dashboardComponentInstance.handleHideProfile}/> - </Provider>, document.getElementById(`profile-drawing-content`)); - - backboneEvents.get().on(`reset:all reset:profile-drawing off:all`, () => { - window.menuProfilesComponentInstance.stopDrawing(); + ); + + let intersectingFeatures = []; + if (e.latlng) { + var clickBounds = L.latLngBounds(e.latlng, e.latlng); + let res = [ + 156543.033928, 78271.516964, 39135.758482, 19567.879241, + 9783.9396205, 4891.96981025, 2445.98490513, 1222.99245256, + 611.496226281, 305.748113141, 152.87405657, 76.4370282852, + 38.2185141426, 19.1092570713, 9.55462853565, 4.77731426782, + 2.38865713391, 1.19432856696, 0.597164283478, 0.298582141739, + 0.149291, 0.074645535, + ]; + + let distance = 10 * res[cloud.get().getZoom()]; + + let mapObj = cloud.get().map; + for (var l in mapObj._layers) { + var overlay = mapObj._layers[l]; + if (overlay._layers) { + for (var f in overlay._layers) { + var feature = overlay._layers[f]; + var bounds; + if (feature.getBounds) { + bounds = feature.getBounds(); + } else if (feature._latlng) { + let circle = new L.circle(feature._latlng, { + radius: distance, }); - } catch (e) { - console.error(e); + // DIRTY HACK + circle.addTo(mapObj); + bounds = circle.getBounds(); + circle.removeFrom(mapObj); + } + + try { + if ( + bounds && + clickBounds.intersects(bounds) && + overlay.id + ) { + intersectingFeatures.push(feature.feature); + } + } catch (e) { + console.log(e); + } } - }); - - if (dashboardComponentInstance) dashboardComponentInstance.onSetMin(); - }; - - if (document.getElementById(DASHBOARD_CONTAINER_ID)) { - let initialPlots = []; - if (applicationState && `modules` in applicationState && MODULE_NAME in applicationState.modules && `plots` in applicationState.modules[MODULE_NAME]) { - initialPlots = applicationState.modules[MODULE_NAME].plots; + } } - - let initialProfiles = []; - if (applicationState && `modules` in applicationState && MODULE_NAME in applicationState.modules && `profiles` in applicationState.modules[MODULE_NAME]) { - initialProfiles = applicationState.modules[MODULE_NAME].profiles; + } else { + // In case marker "click" event was triggered from the code + intersectingFeatures.push(e.target.feature); + } + + let titleAsLink = false; + + if (layerName.indexOf(LAYER_NAMES[0]) > -1) { + titleAsLink = true; + } + + let clickedFeatureAlreadyDetected = false; + intersectingFeatures.map((feature) => { + if ( + feature.properties.boreholeno === + clickedFeature.properties.boreholeno + ) { + clickedFeatureAlreadyDetected = true; } + }); + + if (clickedFeatureAlreadyDetected === false) + intersectingFeatures.unshift(clickedFeature); + + let boreholes = []; + + intersectingFeatures.map((feature) => { + boreholes.push(feature.properties.boreholeno); + }); + + let qLayer; + if ( + layerName.indexOf(LAYER_NAMES[0]) > -1 || + layerName.indexOf(LAYER_NAMES[3]) > -1 + ) { + qLayer = "chemicals.boreholes_time_series_with_chemicals"; + } else { + qLayer = "sensor.sensordata_with_correction"; + // Filter NaN values, so SQL doesn't return type error + boreholes = boreholes.filter((v) => { + if (!isNaN(v)) { + return v; + } + }); + } + + // Lazy load features + $.ajax({ + url: + "/api/sql/jupiter?srs=25832&q=SELECT * FROM " + + qLayer + + " WHERE boreholeno in('" + + boreholes.join("','") + + "')", + scriptCharset: "utf-8", + success: function (response) { + dataSource = []; + boreholesDataSource = response.features; + dataSource = dataSource.concat(boreholesDataSource); + if (dashboardComponentInstance) { + dashboardComponentInstance.setDataSource(dataSource); + } + + /* layer.bindPopup(ReactDOMServer.renderToString(<Provider store={reduxStore}><ThemeProvider><MapDecorator /></ThemeProvider></Provider>), + { maxWidth: 500, className: 'map-decorator-popup' }); */ + reduxStore.dispatch(setBoreholeFeatures(response.features)); + _self.createModal( + response.features, + false, + titleAsLink, + false + ); + if (!dashboardComponentInstance) { + throw new Error(`Unable to find the component instance`); + } + }, + error: function () {}, + }); + }); + }, + "watsonc" + ); + + let svgCirclePart = symbolizer.getSymbol(layerName); + if (svgCirclePart) { + layerTree.setPointToLayer(layerName, (feature, latlng) => { + let renderIcon = true; + if (layerName === LAYER_NAMES[1]) { + if ( + feature.properties.loctypeid && + enabledLoctypeIds.indexOf( + parseInt(feature.properties.loctypeid) + "" + ) === -1 && + enabledLoctypeIds.indexOf( + parseInt(feature.properties.loctypeid) + ) === -1 + ) { + renderIcon = false; + } + } else { + return L.circleMarker(latlng); + } - let plotManager = new PlotManager(); - plotManager.hydratePlotsFromUser(initialPlots).then(hydratedInitialPlots => { // User plots - try { - dashboardComponentInstance = ReactDOM.render(<DashboardComponent - backboneEvents={backboneEvents} - initialPlots={hydratedInitialPlots} - initialProfiles={initialProfiles} - onOpenBorehole={this.openBorehole.bind(this)} - onPlotsChange={(plots = false) => { - backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); - if (plots) { - _self.setStyleForPlots(plots); - - if (window.menuTimeSeriesComponentInstance) window.menuTimeSeriesComponentInstance.setPlots(plots); - // Plots were updated from the DashboardComponent component - if (modalComponentInstance) _self.createModal(false, plots); - } - }} - onProfilesChange={(profiles = false) => { - backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); - if (profiles && window.menuProfilesComponentInstance) window.menuProfilesComponentInstance.setProfiles(profiles); - }} - onActivePlotsChange={(activePlots, plots) => { - backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); - if (window.menuTimeSeriesComponentInstance) window.menuTimeSeriesComponentInstance.setActivePlots(activePlots); - if (modalComponentInstance) _self.createModal(false, plots); - }} - onActiveProfilesChange={(activeProfiles) => { - backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); - if (window.menuProfilesComponentInstance) window.menuProfilesComponentInstance.setActiveProfiles(activeProfiles); - }} - onHighlightedPlotChange={(plotId, plots) => { - _self.setStyleForHighlightedPlot(plotId, plots); - if (window.menuTimeSeriesComponentInstance) window.menuTimeSeriesComponentInstance.setHighlightedPlot(plotId); - }}/>, document.getElementById(DASHBOARD_CONTAINER_ID)); - } catch (e) { - console.error(e); - } - proceedWithInitialization(); - }).catch(() => { - console.error(`Unable to hydrate initial plots`, initialPlots); + if (renderIcon) { + let participatingIds = []; + if (dashboardComponentInstance) { + let plots = dashboardComponentInstance.getPlots(); + plots.map((plot) => { + participatingIds = participatingIds.concat( + _self.participatingIds(plot) + ); }); + } + + let highlighted = + participatingIds.indexOf(feature.properties.boreholeno) > -1; + let localSvgCirclePart = symbolizer.getSymbol(layerName, { + online: feature.properties.status, + shape: feature.properties.loctypeid, + highlighted, + }); + + let icon = L.icon({ + iconUrl: + "data:image/svg+xml;base64," + btoa(localSvgCirclePart), + iconAnchor: [8, 33], + iconSize: [30, 30], + watsoncStatus: `default`, + }); + + return L.marker(latlng, { icon }); } else { - console.warn(`Unable to find the container for watsonc extension (element id: ${DASHBOARD_CONTAINER_ID})`); + return null; } - }); - $(`#search-border`).trigger(`click`); + }); + } + }); - try { + // Renewing the already created store by rebuilding the layer tree + setTimeout(() => { + setTimeout(() => { + layerTree.create(false, [], true).then(() => { + //layerTree.reloadLayer(LAYER_NAMES[0]); + if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[1]) > -1) { + layerTree.reloadLayer(LAYER_NAMES[1]); + } + if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[0]) > -1) { + layerTree.reloadLayer(LAYER_NAMES[0]); + } + if (layerTree.getActiveLayers().indexOf(LAYER_NAMES[3]) > -1) { + layerTree.reloadLayer(LAYER_NAMES[3]); + } + }); + }, 500); + }, 100); + + const proceedWithInitialization = () => { + // Setting up feature dialog + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-less") + .on("click", function () { + $("#" + FEATURE_CONTAINER_ID).animate( + { + bottom: $("#" + FEATURE_CONTAINER_ID).height() * -1 + 30 + "px", + }, + 500, + function () { + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-less") + .hide(); + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-more") + .show(); + } + ); + }); + + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-more") + .on("click", function () { + $("#" + FEATURE_CONTAINER_ID).animate( + { + bottom: "0", + }, + 500, + function () { + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-less") + .show(); + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-more") + .hide(); + } + ); + }); + + $(`#` + FEATURE_CONTAINER_ID) + .find(".close-hide") + .on("click", function () { + $("#" + FEATURE_CONTAINER_ID).animate( + { + bottom: "-100%", + }, + 500, + function () { + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-less") + .show(); + $(`#` + FEATURE_CONTAINER_ID) + .find(".expand-more") + .hide(); + } + ); + }); + + // Initializing TimeSeries management component + + // Initializing profiles tab + if ($(`#profile-drawing-content`).length === 0) + throw new Error(`Unable to get the profile drawing tab`); + + // Initializing TimeSeries management component + $(`[data-module-id="profile-drawing"]`).click(() => { + try { ReactDOM.render( - <AnalyticsComponent kommuner={KOMMUNER} - - />, document.getElementById("watsonc-analytics-content")); - } catch (e) { + <Provider store={reduxStore}> + <MenuProfilesComponent + cloud={cloud} + backboneEvents={backboneEvents} + license={dashboardComponentInstance.getLicense()} + categories={categoriesOverall ? categoriesOverall : []} + initialProfiles={dashboardComponentInstance.getProfiles()} + initialActiveProfiles={dashboardComponentInstance.getActiveProfiles()} + onProfileCreate={ + dashboardComponentInstance.handleCreateProfile + } + onProfileDelete={ + dashboardComponentInstance.handleDeleteProfile + } + onProfileHighlight={ + dashboardComponentInstance.handleHighlightProfile + } + onProfileAdd={dashboardComponentInstance.handleAddProfile} + onProfileShow={dashboardComponentInstance.handleShowProfile} + onProfileHide={dashboardComponentInstance.handleHideProfile} + /> + </Provider>, + document.getElementById(`profile-drawing-content`) + ); + + backboneEvents + .get() + .on(`reset:all reset:profile-drawing off:all`, () => { + window.menuProfilesComponentInstance.stopDrawing(); + }); + } catch (e) { console.error(e); + } + }); + + // if (dashboardComponentInstance) dashboardComponentInstance.onSetMin(); + }; + + if (document.getElementById(DASHBOARD_CONTAINER_ID)) { + let initialPlots = []; + if ( + applicationState && + `modules` in applicationState && + MODULE_NAME in applicationState.modules && + `plots` in applicationState.modules[MODULE_NAME] + ) { + initialPlots = applicationState.modules[MODULE_NAME].plots; + } + let initialProfiles = []; + if ( + applicationState && + `modules` in applicationState && + MODULE_NAME in applicationState.modules && + `profiles` in applicationState.modules[MODULE_NAME] + ) { + initialProfiles = applicationState.modules[MODULE_NAME].profiles; } - }, - - - openBorehole(boreholeIdentifier) { - let mapLayers = layers.getMapLayers(); - let boreholeIsInViewport = false; - mapLayers.map(layer => { - if ([LAYER_NAMES[0], LAYER_NAMES[1]].indexOf(layer.id) > -1 && layer._layers) { - for (let key in layer._layers) { - if (layer._layers[key].feature && layer._layers[key].feature.properties && layer._layers[key].feature.properties.boreholeno) { - if (layer._layers[key].feature.properties.boreholeno.trim() === boreholeIdentifier.trim()) { - layer._layers[key].fire(`click`); - boreholeIsInViewport = true; - setTimeout(() => { - _self.bringFeaturesDialogToFront(); - }, 500); - } - } + let reactRef = React.createRef(); + try { + ReactDOM.render( + <DashboardWrapper + cloud={cloud} + state={state} + ref={reactRef} + session={session} + backboneEvents={backboneEvents} + urlparser={urlparser} + anchor={anchor} + onApply={_self.onApplyLayersAndChemical} + initialPlots={[]} + initialProfiles={initialProfiles} + onOpenBorehole={this.openBorehole} + onDeleteMeasurement={( + plotId, + featureGid, + featureKey, + featureIntakeIndex + ) => { + dashboardComponentInstance.deleteMeasurement( + plotId, + featureGid, + featureKey, + featureIntakeIndex + ); + }} + onAddMeasurement={( + plotId, + featureGid, + featureKey, + featureIntakeIndex, + measurementsData, + relation + ) => { + dashboardComponentInstance.addMeasurement( + plotId, + featureGid, + featureKey, + featureIntakeIndex, + measurementsData, + relation + ); + }} + onPlotsChange={(plots = false, context) => { + backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); + if (plots) { + _self.setStyleForPlots(plots); + + if (window.menuTimeSeriesComponentInstance) + window.menuTimeSeriesComponentInstance.setPlots(plots); + // Plots were updated from the DashboardComponent component + if (modalComponentInstance) _self.createModal(false, plots); + context.setActivePlots(_self.getExistingActivePlots()); } + }} + onProfilesChange={(profiles = false) => { + backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); + if (profiles && window.menuProfilesComponentInstance) + window.menuProfilesComponentInstance.setProfiles(profiles); + }} + onActivePlotsChange={(activePlots, plots, context) => { + backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); + if (window.menuTimeSeriesComponentInstance) + window.menuTimeSeriesComponentInstance.setActivePlots( + activePlots + ); + if (modalComponentInstance) _self.createModal(false, plots); + + context.setActivePlots( + plots.filter((plot) => activePlots.indexOf(plot.id) > -1) + ); + }} + getAllPlots={() => { + return dashboardComponentInstance.getPlots(); + }} + getAllProfiles={() => { + return dashboardComponentInstance.getProfiles(); + }} + getDashboardItems={() => { + return dashboardComponentInstance.getDashboardItems(); + }} + getActiveProfiles={() => { + return dashboardComponentInstance.getActiveProfileObjects(); + }} + setPlots={(plots, activePlots) => { + dashboardComponentInstance.setPlots(plots); + dashboardComponentInstance.setActivePlots(activePlots); + }} + setItems={(plots) => { + dashboardComponentInstance.setItems(plots); + }} + setProfiles={(profiles, activeProfiles) => { + dashboardComponentInstance.setProfiles(profiles); + dashboardComponentInstance.setActiveProfiles(activeProfiles); + }} + onActiveProfilesChange={(activeProfiles, profiles, context) => { + backboneEvents.get().trigger(`${MODULE_NAME}:plotsUpdate`); + if (window.menuProfilesComponentInstance) + window.menuProfilesComponentInstance.setActiveProfiles( + activeProfiles + ); + context.setActiveProfiles( + profiles.filter( + (profile) => activeProfiles.indexOf(profile.key) > -1 + ) + ); + }} + onHighlightedPlotChange={(plotId, plots) => { + _self.setStyleForHighlightedPlot(plotId, plots); + if (window.menuTimeSeriesComponentInstance) + window.menuTimeSeriesComponentInstance.setHighlightedPlot( + plotId + ); + }} + />, + document.getElementById("watsonc-plots-dialog-form-hidden") + ); + dashboardComponentInstance = reactRef.current; + } catch (e) { + console.error(e); + } + proceedWithInitialization(); + } else { + console.warn( + `Unable to find the container for watsonc extension (element id: ${DASHBOARD_CONTAINER_ID})` + ); + } + }); + $(`#search-border`).trigger(`click`); + + try { + ReactDOM.render( + <AnalyticsComponent kommuner={KOMMUNER} />, + document.getElementById("watsonc-analytics-content") + ); + } catch (e) { + console.error(e); + } + }, + + let(boreholeIdentifier) { + let mapLayers = layers.getMapLayers(); + let boreholeIsInViewport = false; + mapLayers.map((layer) => { + if ( + [LAYER_NAMES[0], LAYER_NAMES[1]].indexOf(layer.id) > -1 && + layer._layers + ) { + for (let key in layer._layers) { + if ( + layer._layers[key].feature && + layer._layers[key].feature.properties && + layer._layers[key].feature.properties.boreholeno + ) { + if ( + layer._layers[key].feature.properties.boreholeno.trim() === + boreholeIdentifier.trim() + ) { + layer._layers[key].fire(`click`); + boreholeIsInViewport = true; + setTimeout(() => { + _self.bringFeaturesDialogToFront(); + }, 500); } - }); - - if (boreholeIsInViewport === false) { - alert(__(`Requested borehole is not in a viewport (data is not loaded)`)); + } } - }, + } + }); - bringFeaturesDialogToFront() { - if ($('#watsonc-plots-dialog-form').css('z-index') === '1000') { - $('#watsonc-plots-dialog-form').css('z-index', '100'); - } else { - $('#watsonc-plots-dialog-form').css('z-index', '10'); - } + if (boreholeIsInViewport === false) { + alert(__(`Requested borehole is not in a viewport (data is not loaded)`)); + } + }, - $('#watsonc-features-dialog').css('z-index', '1000'); + bringFeaturesDialogToFront() { + if ($("#watsonc-plots-dialog-form").css("z-index") === "1000") { + $("#watsonc-plots-dialog-form").css("z-index", "100"); + } else { + $("#watsonc-plots-dialog-form").css("z-index", "10"); + } - if ($('#search-ribbon').css('z-index') === '1000') { - $('#search-ribbon').css('z-index', '100'); - } else { - $('#search-ribbon').css('z-index', '10'); - } - }, - - initializeSearchBar() { - let searchBar = $(`#js-watsonc-search-field`); - $(searchBar).parent().attr(`style`, `padding-top: 8px;`); - $(searchBar).attr(`style`, `max-width: 200px; float: right;`); - $(searchBar).append(`<div class="input-group"> - <input type="text" class="form-control" placeholder="${__(`Search`) + '...'}" style="color: white;"/> + $("#watsonc-features-dialog").css("z-index", "1000"); + + if ($("#search-ribbon").css("z-index") === "1000") { + $("#search-ribbon").css("z-index", "100"); + } else { + $("#search-ribbon").css("z-index", "10"); + } + }, + + initializeSearchBar() { + let searchBar = $(`#js-watsonc-search-field`); + $(searchBar).parent().attr(`style`, `padding-top: 8px;`); + $(searchBar).attr(`style`, `max-width: 200px; float: right;`); + $(searchBar).append(`<div class="input-group"> + <input type="text" class="form-control" placeholder="${ + __(`Search`) + "..." + }" style="color: white;"/> <span class="input-group-btn"> <button class="btn btn-primary" type="button" style="color: white;"> <i class="fa fa-search"></i> @@ -694,604 +913,553 @@ module.exports = module.exports = { </span> </div>`); - $(searchBar).find('input').focus(function () { - $(this).attr(`placeholder`, __(`Enter borehole, installation, station`) + '...'); - $(searchBar).animate({"max-width": `400px`}); - }); - - $(searchBar).find('input').blur(function () { - $(this).attr(`placeholder`, __(`Search`) + '...'); - if ($(this).val() === ``) { - $(searchBar).animate({"max-width": `200px`}); - } - }); - - $(searchBar).find('button').click(() => { - alert(`Search button was clicked`); - }); - }, - - buildBreadcrumbs(secondLevel = false, thirdLevel = false, isWaterLevel = false) { - $(`.js-layer-slide-breadcrumbs`).attr('style', 'height: 60px; padding-top: 10px;'); - $(`.js-layer-slide-breadcrumbs`).empty(); - if (secondLevel !== false) { - let firstLevel = `Kemi`; - let secondLevelMarkup = `<li class="active" style="color: rgba(255, 255, 255, 0.84);">${secondLevel}</li>`; - if (isWaterLevel) { - firstLevel = `Vandstand`; - secondLevelMarkup = ``; - } - - $(`.js-layer-slide-breadcrumbs`).append(`<ol class="breadcrumb" style="background-color: transparent; margin-bottom: 0px;"> + $(searchBar) + .find("input") + .focus(function () { + $(this).attr( + `placeholder`, + __(`Enter borehole, installation, station`) + "..." + ); + $(searchBar).animate({ "max-width": `400px` }); + }); + + $(searchBar) + .find("input") + .blur(function () { + $(this).attr(`placeholder`, __(`Search`) + "..."); + if ($(this).val() === ``) { + $(searchBar).animate({ "max-width": `200px` }); + } + }); + + $(searchBar) + .find("button") + .click(() => { + alert(`Search button was clicked`); + }); + }, + + buildBreadcrumbs( + secondLevel = false, + thirdLevel = false, + isWaterLevel = false + ) { + $(`.js-layer-slide-breadcrumbs`).attr( + "style", + "height: 60px; padding-top: 10px;" + ); + $(`.js-layer-slide-breadcrumbs`).empty(); + if (secondLevel !== false) { + let firstLevel = `Kemi`; + let secondLevelMarkup = `<li class="active" style="color: rgba(255, 255, 255, 0.84);">${secondLevel}</li>`; + if (isWaterLevel) { + firstLevel = `Vandstand`; + secondLevelMarkup = ``; + } + + $(`.js-layer-slide-breadcrumbs`) + .append(`<ol class="breadcrumb" style="background-color: transparent; margin-bottom: 0px;"> <li class="active" style="color: rgba(255, 255, 255, 0.84);"><i class="fa fa-database"></i> ${firstLevel}</li> ${secondLevelMarkup} <li class="active" style="color: rgba(255, 255, 255, 0.84);"> <span style="color: rgb(160, 244, 197); font-weight: bold;">${thirdLevel}<span> </li> </ol>`); + } + }, + + onApplyLayersAndChemical: (parameters) => { + // Disabling all layers + layerTree.getActiveLayers().map((layerNameToEnable) => { + if ( + layerNameToEnable !== LAYER_NAMES[2] && + !layerNameToEnable.startsWith("gc2_io_dk") + ) + switchLayer.init(layerNameToEnable, false); + }); + + let filter = { + match: "all", + columns: [ + { + fieldname: "count", + expression: ">", + value: parameters.selectedMeasurementCount, + restriction: false, + }, + { + fieldname: "startdate", + expression: ">", + value: parameters.selectedStartDate, + restriction: false, + }, + { + fieldname: "enddate", + expression: "<", + value: parameters.selectedEndDate, + restriction: false, + }, + ], + }; + + let filters = {}; + for (let i = 0; i < parameters.layers.length; i++) { + if (parameters.layers[i] === LAYER_NAMES[0]) { + if (!parameters.chemical) return; + let rasterToEnable = `systemx._${parameters.chemical}`; + currentRasterLayer = rasterToEnable; + filters[rasterToEnable] = filter; + layerTree.applyFilters(filters); + switchLayer.init(rasterToEnable, true); + } else { + if (parameters.filters) { + layerTree.applyFilters(parameters.filters); } - }, - - onApplyLayersAndChemical: (parameters) => { - console.log("parameters", parameters) - // Disabling all layers - layerTree.getActiveLayers().map(layerNameToEnable => { - if (layerNameToEnable !== LAYER_NAMES[2] && !layerNameToEnable.startsWith("gc2_io_dk")) - switchLayer.init(layerNameToEnable, false); - }); + switchLayer.init(parameters.layers[i], true); + } + } - // Bind tool tip to stations - layerTree.setOnLoad(LAYER_NAMES[1], () => { - _self.bindToolTipOnStations() - }, "watsonc"); - - // Bind tool tip to Pesticidoverblik - layerTree.setOnLoad(LAYER_NAMES[3], () => { - _self.bindToolTipOnPesticidoverblik() - }, "watsonc"); - - let filters = {}; - let filteredLayers = []; - filters[LAYER_NAMES[1].split(":")[1]] = { - match: "all", columns: [ - {fieldname: "count", expression: ">", value: parameters.selectedMeasurementCount, restriction: false}, - {fieldname: "startdate", expression: ">", value: parameters.selectedStartDate, restriction: false}, - {fieldname: "enddate", expression: "<", value: parameters.selectedEndDate, restriction: false} - ] + enabledLoctypeIds = []; + + // Wait a bit with trigger state, so this + setTimeout(() => { + backboneEvents.get().trigger(`${MODULE_NAME}:enabledLoctypeIdsChange`); + }, 1500); + }, + + /** + * Open module menu modal dialog + * + * @returns {void} + */ + openMenuModal: (open = true) => { + const onCloseHandler = () => { + $("#watsonc-menu-dialog").modal("hide"); + }; + + const introlModalPlaceholderId = `watsonc-intro-modal-placeholder`; + if ($(`#${introlModalPlaceholderId}`).is(`:empty`)) { + try { + /* ReactDOM.render(<Provider store={reduxStore}> + <IntroModal + ref={inst => { + infoModalInstance = inst; + }} + anchor={anchor} + state={state} + urlparser={urlparser} + backboneEvents={backboneEvents} + layers={DATA_SOURCES} + categories={categoriesOverall ? categoriesOverall : []} + onApply={_self.onApplyLayersAndChemical} + onClose={onCloseHandler} + /></Provider>, document.getElementById(introlModalPlaceholderId)); */ + ReactDOM.render( + <Provider store={reduxStore}> + <ThemeProvider> + <DataSelectorDialogue + titleText={__("Velkommen til Calypso")} + urlparser={urlparser} + anchor={anchor} + backboneEvents={backboneEvents} + categories={categoriesOverall ? categoriesOverall : []} + onApply={_self.onApplyLayersAndChemical} + onCloseButtonClick={onCloseHandler} + state={state} + /> + </ThemeProvider> + </Provider>, + document.getElementById(introlModalPlaceholderId) + ); + } catch (e) { + console.error(e); + } + } - } + if (open) { + $("#watsonc-menu-dialog").modal({ + backdrop: `static`, + }); + } + }, + createModal: ( + features, + plots = false, + titleAsLink = null, + setTitle = true + ) => { + if (features === false) { + if (lastFeatures) { + features = lastFeatures; + } + } - // Enable raster layer - if (parameters.layers.indexOf(LAYER_NAMES[0]) > -1) { - if (!parameters.chemical) return; - let rasterToEnable = `system._${parameters.chemical}`; - currentRasterLayer = rasterToEnable; - filters[rasterToEnable] = { - match: "all", columns: [ - { - fieldname: "count", - expression: ">", - value: parameters.selectedMeasurementCount, - restriction: false - }, - {fieldname: "startdate", expression: ">", value: parameters.selectedStartDate, restriction: false}, - {fieldname: "enddate", expression: "<", value: parameters.selectedEndDate, restriction: false} - ] + if (titleAsLink === null) { + if (lastTitleAsLink !== null) { + titleAsLink = lastTitleAsLink; + } + } else { + lastTitleAsLink = titleAsLink; + } - } - console.log("filters", filters) - layerTree.applyFilters(filters); - switchLayer.init(rasterToEnable, true).then(() => { - if (parameters.chemical) { - _self.enableChemical(parameters.chemical, filteredLayers, false, parameters); - } else { - lastSelectedChemical = parameters.chemical; - filteredLayers.map(layerName => { - layerTree.reloadLayer(layerName); // TODO - }); - } - }); + if (features !== false) { + lastFeatures = features; + + let titles = []; + features.map((item) => { + let title = utils.getMeasurementTitle(item); + if (titleAsLink) { + let link = `http://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${encodeURIComponent( + item.properties.boreholeno + )}`; + titles.push( + `<a href="${link}" target="_blank" title="${title} @ data.geus.dk">${title}</a>` + ); } else { - layerTree.applyFilters(filters); + titles.push(`${title}`); } + }); - enabledLoctypeIds = []; - parameters.layers.map(layerName => { - if (layerName.indexOf(LAYER_NAMES[0]) === 0) { - filteredLayers.push(layerName); - } - if (layerName.indexOf(LAYER_NAMES[1]) === 0) { - if (layerName.indexOf(`#`) > -1) { - if (filteredLayers.indexOf(layerName.split(`#`)[0]) === -1) { - filteredLayers.push(layerName.split(`#`)[0]); - } - enabledLoctypeIds.push(layerName.split(`#`)[1]); - } else { - if (filteredLayers.indexOf(layerName) === -1) { - filteredLayers.push(layerName); - } - } - } - if (layerName.indexOf(LAYER_NAMES[3]) === 0 || layerName.indexOf(LAYER_NAMES[1]) === 0) { - filteredLayers.push(layerName); - if (layerName.indexOf(LAYER_NAMES[3]) === 0) switchLayer.init(LAYER_NAMES[4], true); - filteredLayers.map(layerName => { - layerTree.reloadLayer(layerName); - }); - layerTree.setStyle(LAYER_NAMES[3], { - "color": "#ffffff", - "weight": 0, - "opacity": 0.0, - "fillOpacity": 0.0 - }); - } - }); - - // Wait a bit with trigger state, so this - setTimeout(() => { - backboneEvents.get().trigger(`${MODULE_NAME}:enabledLoctypeIdsChange`); - }, 1500); - - }, - - /** - * Open module menu modal dialog - * - * @returns {void} - */ - openMenuModal: (open = true) => { - const onCloseHandler = () => { - $('#watsonc-menu-dialog').modal('hide'); - }; - - const introlModalPlaceholderId = `watsonc-intro-modal-placeholder`; - if ($(`#${introlModalPlaceholderId}`).is(`:empty`)) { - try { - ReactDOM.render(<Provider store={reduxStore}> - <IntroModal - ref={inst => { - infoModalInstance = inst; - }} - anchor={anchor} - state={state} - urlparser={urlparser} - backboneEvents={backboneEvents} - layers={DATA_SOURCES} - categories={categoriesOverall ? categoriesOverall : []} - onApply={_self.onApplyLayersAndChemical} - onClose={onCloseHandler} - /></Provider>, document.getElementById(introlModalPlaceholderId)); - } catch (e) { - console.error(e); - } - } - - if (open) { - $('#watsonc-menu-dialog').modal({ - backdrop: `static` - }); - } - - }, - createModal: (features, plots = false, titleAsLink = null, setTitle = true) => { - if (features === false) { - if (lastFeatures) { - features = lastFeatures; - } - } - - if (titleAsLink === null) { - if (lastTitleAsLink !== null) { - titleAsLink = lastTitleAsLink; - } + if (setTitle === true) { + if (titles.length === 1) { + $("#" + FEATURE_CONTAINER_ID) + .find(`.modal-title`) + .html(titles[0]); } else { - lastTitleAsLink = titleAsLink; + $("#" + FEATURE_CONTAINER_ID) + .find(`.modal-title`) + .html(`${__(`Boreholes`)} (${titles.join(`, `)})`); } + } else { + $("#" + FEATURE_CONTAINER_ID) + .find(`.modal-title`) + .html(""); + } + } - if (features !== false) { - lastFeatures = features; - - let titles = []; - features.map(item => { - let title = utils.getMeasurementTitle(item); - if (titleAsLink) { - let link = `http://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${encodeURIComponent(item.properties.boreholeno)}`; - titles.push(`<a href="${link}" target="_blank" title="${title} @ data.geus.dk">${title}</a>`); - } else { - titles.push(`${title}`); - } - }); - - if (setTitle === true) { - if (titles.length === 1) { - $("#" + FEATURE_CONTAINER_ID).find(`.modal-title`).html(titles[0]); - } else { - $("#" + FEATURE_CONTAINER_ID).find(`.modal-title`).html(`${__(`Boreholes`)} (${titles.join(`, `)})`); - } - } else { - $("#" + FEATURE_CONTAINER_ID).find(`.modal-title`).html(''); - } - - if (document.getElementById(FORM_FEATURE_CONTAINER_ID)) { - try { - let existingPlots = dashboardComponentInstance.getPlots(false); - - setTimeout(() => { - ReactDOM.unmountComponentAtNode(document.getElementById(FORM_FEATURE_CONTAINER_ID)); - modalComponentInstance = ReactDOM.render(<ModalComponent - features={features} - categories={categories} - dataSource={dataSource} - names={names} - limits={limits} - initialPlots={(existingPlots ? existingPlots : [])} - initialActivePlots={dashboardComponentInstance.getActivePlots()} - onPlotHide={dashboardComponentInstance.handleHidePlot} - onPlotShow={dashboardComponentInstance.handleShowPlot} - license={dashboardComponentInstance.getLicense()} - modalScroll={dashboardComponentInstance.getModalScroll()} - setModalScroll={dashboardComponentInstance.setModalScroll} - onAddMeasurement={(plotId, featureGid, featureKey, featureIntakeIndex) => { - dashboardComponentInstance.addMeasurement(plotId, featureGid, featureKey, featureIntakeIndex); - }} - onDeleteMeasurement={(plotId, featureGid, featureKey, featureIntakeIndex) => { - dashboardComponentInstance.deleteMeasurement(plotId, featureGid, featureKey, featureIntakeIndex); - }} - onPlotAdd={((newPlotTitle) => { - dashboardComponentInstance.addPlot(newPlotTitle, true); - })}/>, document.getElementById(FORM_FEATURE_CONTAINER_ID)); - }, 100); - } catch (e) { - console.error(e); - } - } else { - console.warn(`Unable to find the container for borehole component (element id: ${FORM_FEATURE_CONTAINER_ID})`); - } + _self.bringFeaturesDialogToFront(); + }, + + /** + * Sets style for highlighted plot + * + * @param {Number} plotId Plot identifier + * @param {Array} plots Existing plots + * + * @return {void} + */ + setStyleForHighlightedPlot: (plotId, plots) => { + // If specific chemical is activated, then do not style + if (lastSelectedChemical === false) { + let participatingIds = []; + plots.map((plot) => { + if (plot.id === plotId) { + let localParticipatingIds = _self.participatingIds(plot); + participatingIds = participatingIds.concat(localParticipatingIds); } + }); - _self.bringFeaturesDialogToFront(); - }, - - /** - * Sets style for highlighted plot - * - * @param {Number} plotId Plot identifier - * @param {Array} plots Existing plots - * - * @return {void} - */ - setStyleForHighlightedPlot: (plotId, plots) => { - // If specific chemical is activated, then do not style - if (lastSelectedChemical === false) { - let participatingIds = []; - plots.map(plot => { - if (plot.id === plotId) { - let localParticipatingIds = _self.participatingIds(plot); - participatingIds = participatingIds.concat(localParticipatingIds); - } - }); - - _self.highlightFeatures(participatingIds); - } - }, - - participatingIds(plot) { - let participatingIds = []; - if ("measurements" in plot) { - plot.measurements.map(measurement => { - let splitMeasurement = measurement.split(`:`); - if (splitMeasurement.length === 3) { - let id = parseInt(splitMeasurement[0]); - if (participatingIds.indexOf(id) === -1) participatingIds.push(id); - } - }); - } else { - participatingIds = []; + _self.highlightFeatures(participatingIds); + } + }, + + participatingIds(plot) { + let participatingIds = []; + if ("measurements" in plot) { + plot.measurements.map((measurement) => { + let splitMeasurement = measurement.split(`:`); + if (splitMeasurement.length === 3) { + let id = parseInt(splitMeasurement[0]); + if (participatingIds.indexOf(id) === -1) participatingIds.push(id); } + }); + } else { + participatingIds = []; + } - return participatingIds; - }, - - /** - * Sets style for all plots - * - * @param {Array} plots Existing plots - * - * @return {void} - */ - setStyleForPlots: (plots) => { - // If specific chemical is activated, then do not style - if (lastSelectedChemical === false) { - let participatingIds = []; - plots.map(plot => { - let localParticipatingIds = _self.participatingIds(plot); - participatingIds = participatingIds.concat(localParticipatingIds) - }); - - _self.highlightFeatures(participatingIds); - } else { - let activeLayers = layerTree.getActiveLayers(); - activeLayers.map(activeLayerKey => { - _self.displayChemicalSymbols(activeLayerKey); + return participatingIds; + }, + + /** + * Sets style for all plots + * + * @param {Array} plots Existing plots + * + * @return {void} + */ + setStyleForPlots: (plots) => { + // If specific chemical is activated, then do not style + if (lastSelectedChemical === false) { + let participatingIds = []; + plots.map((plot) => { + let localParticipatingIds = _self.participatingIds(plot); + participatingIds = participatingIds.concat(localParticipatingIds); + }); + + _self.highlightFeatures(participatingIds); + } else { + let activeLayers = layerTree.getActiveLayers(); + activeLayers.map((activeLayerKey) => { + _self.displayChemicalSymbols(activeLayerKey); + }); + } + }, + + highlightFeatures(participatingIds) { + let mapLayers = layers.getMapLayers(); + mapLayers.map((layer) => { + if ( + [LAYER_NAMES[0], LAYER_NAMES[1]].indexOf(layer.id) > -1 && + layer._layers + ) { + for (let key in layer._layers) { + let featureLayer = layer._layers[key]; + if ( + featureLayer.feature && + featureLayer.feature.properties && + featureLayer.feature.properties.boreholeno + ) { + let icon = L.icon({ + iconUrl: + "data:image/svg+xml;base64," + + btoa( + getSymbol(layer.id, { + online: featureLayer.feature.properties.status, + shape: featureLayer.feature.properties.loctypeid, + highlighted: + participatingIds.indexOf( + featureLayer.feature.properties.boreholeno + ) > -1, + }) + ), + iconAnchor: [8, 33], + watsoncStatus: + participatingIds.indexOf( + featureLayer.feature.properties.boreholeno + ) > -1 + ? `highlighted` + : `default`, }); - } - }, - - highlightFeatures(participatingIds) { - let mapLayers = layers.getMapLayers(); - mapLayers.map(layer => { - if ([LAYER_NAMES[0], LAYER_NAMES[1]].indexOf(layer.id) > -1 && layer._layers) { - for (let key in layer._layers) { - let featureLayer = layer._layers[key]; - if (featureLayer.feature && featureLayer.feature.properties && featureLayer.feature.properties.boreholeno) { - let icon = L.icon({ - iconUrl: 'data:image/svg+xml;base64,' + btoa(getSymbol(layer.id, { - online: featureLayer.feature.properties.status, - shape: featureLayer.feature.properties.loctypeid, - highlighted: (participatingIds.indexOf(featureLayer.feature.properties.boreholeno) > -1) - })), - iconAnchor: [8, 33], - watsoncStatus: participatingIds.indexOf(featureLayer.feature.properties.boreholeno) > -1 ? `highlighted` : `default` - }); - if (icon && `setIcon` in featureLayer) { - // Do not set icon if the existing one is the same as the new one - let statusOfExistingIcon = (`watsoncStatus` in featureLayer.options.icon.options ? featureLayer.options.icon.options.watsoncStatus : false); - let statusOfNewIcon = icon.options.watsoncStatus; - if (statusOfExistingIcon === false) { - featureLayer.setIcon(icon); - } else { - if (statusOfExistingIcon !== statusOfNewIcon) { - featureLayer.setIcon(icon); - } - } - } - } + if (icon && `setIcon` in featureLayer) { + // Do not set icon if the existing one is the same as the new one + let statusOfExistingIcon = + `watsoncStatus` in featureLayer.options.icon.options + ? featureLayer.options.icon.options.watsoncStatus + : false; + let statusOfNewIcon = icon.options.watsoncStatus; + if (statusOfExistingIcon === false) { + featureLayer.setIcon(icon); + } else { + if (statusOfExistingIcon !== statusOfNewIcon) { + featureLayer.setIcon(icon); } + } } - }); - }, - - bindToolTipOnStations() { - let stores = layerTree.getStores(); - stores[LAYER_NAMES[1]].layer.eachLayer(function (layer) { - let feature = layer.feature; - let html = []; - html.push(`${feature.properties.mouseover}`); - layer.bindTooltip(`${html.join('<br>')}`); - }); - }, - - bindToolTipOnPesticidoverblik() { - let stores = layerTree.getStores(); - stores[LAYER_NAMES[3]].layer.eachLayer(function (layer) { - let feature = layer.feature; - layer.bindTooltip(feature.properties.html_mouseover); - }); - }, - - displayChemicalSymbols(storeId) { - let stores = layerTree.getStores(); - let participatingIds = []; - if (dashboardComponentInstance) { - let plots = dashboardComponentInstance.getPlots(); - plots.map(plot => { - participatingIds = participatingIds.concat(_self.participatingIds(plot)); - }); + } } + } + }); + }, + + bindToolTipOnStations() { + let stores = layerTree.getStores(); + stores[LAYER_NAMES[1]].layer.eachLayer(function (layer) { + let feature = layer.feature; + let html = []; + html.push(`${feature.properties.mouseover}`); + layer.bindTooltip(`${html.join("<br>")}`); + }); + }, + + bindToolTipOnPesticidoverblik() { + let stores = layerTree.getStores(); + stores[LAYER_NAMES[3]].layer.eachLayer(function (layer) { + let feature = layer.feature; + layer.bindTooltip(feature.properties.html_mouseover); + }); + }, + + displayChemicalSymbols(storeId) { + let stores = layerTree.getStores(); + let participatingIds = []; + if (dashboardComponentInstance) { + let plots = dashboardComponentInstance.getPlots(); + plots.map((plot) => { + participatingIds = participatingIds.concat( + _self.participatingIds(plot) + ); + }); + } - if (stores[storeId]) { - stores[storeId].layer.eachLayer(function (layer) { - let feature = layer.feature; - if ("maxvalue" in feature.properties && "latestvalue" in feature.properties) { - - let html = []; - // html.push(` - // Historisk: ${!feature.properties.maxlimit ? "< " : ""} ${feature.properties.maxvalue}<br> - // Seneste: ${!feature.properties.latestlimit ? "< " : ""} ${feature.properties.latestvalue}<br>`); - html.push(` + if (stores[storeId]) { + stores[storeId].layer.eachLayer(function (layer) { + let feature = layer.feature; + if ( + "maxvalue" in feature.properties && + "latestvalue" in feature.properties + ) { + let html = []; + // html.push(` + // Historisk: ${!feature.properties.maxlimit ? "< " : ""} ${feature.properties.maxvalue}<br> + // Seneste: ${!feature.properties.latestlimit ? "< " : ""} ${feature.properties.latestvalue}<br>`); + html.push(` Historisk: ${feature.properties.maxvalue}<br> Seneste: ${feature.properties.latestvalue}<br>`); - layer.bindTooltip(`<p><a target="_blank" href="https://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${feature.properties.boreholeno}">${feature.properties.boreholeno}</a></p> - <b style="color: rgb(16, 174, 140)">${names[lastSelectedChemical]}</b><br>${html.join('<br>')}`); - - } - }); - } - }, - - enableChemical(chemicalId, layersToEnable = [], onComplete = false, parameters) { - if (!chemicalId) throw new Error(`Chemical identifier was not provided`); - setTimeout(() => { - let layersToEnableWereProvided = (layersToEnable.length > 0); - if (categoriesOverall) { - for (let layerName in categoriesOverall) { - for (let key in categoriesOverall[layerName]) { - for (let key2 in categoriesOverall[layerName][key]) { - if (key2.toString() === chemicalId.toString() || categoriesOverall[layerName][key][key2] === chemicalId.toString()) { - if (layersToEnableWereProvided === false) { - if (layersToEnable.indexOf(layerName) === -1) { - layersToEnable.push(layerName); - } - } - _self.buildBreadcrumbs(key, categoriesOverall[layerName][key][key2], layerName === LAYER_NAMES[1]); - break; - } - } - } - } - } - let filter = { - match: "all", columns: [ - {fieldname: "compound", expression: "=", value: chemicalId, restriction: false}, - { - fieldname: "count", - expression: ">", - value: parameters.selectedMeasurementCount, - restriction: false - }, - { - fieldname: "startdate", - expression: ">", - value: parameters.selectedStartDate, - restriction: false - }, - {fieldname: "enddate", expression: "<", value: parameters.selectedEndDate, restriction: false} - ] - - }; - let filters = {}; - filters[LAYER_NAMES[0].split(":")[1]] = filter; - filters[LAYER_NAMES[1].split(":")[1]] = filter; - - - layerTree.applyFilters(filters); - lastSelectedChemical = chemicalId; - backboneEvents.get().trigger(`${MODULE_NAME}:chemicalChange`); - let onLoadCallback = function (store) { - if (layersToEnable.indexOf(store.id) > -1) { - _self.displayChemicalSymbols(store.id); - } - }; - layerTree.setOnLoad(LAYER_NAMES[0], onLoadCallback, "watsonc"); - layersToEnable.map(layerName => { - layerTree.reloadLayer(layerName); - }); - layerTree.setStyle(LAYER_NAMES[0], { - "color": "#ffffff", - "weight": 0, - "opacity": 0.0, - "fillOpacity": 0.0 - }); - - if (onComplete) onComplete(); - }, 0); - }, - - - getExistingPlots: () => { - if (dashboardComponentInstance) { - return dashboardComponentInstance.getPlots(); - } else { - throw new Error(`Unable to find the component instance`); - } - }, - - getExistingActivePlots: () => { - if (dashboardComponentInstance) { - return dashboardComponentInstance.getActivePlots(); - } else { - throw new Error(`Unable to find the component instance`); + layer.bindTooltip(`<p><a target="_blank" href="https://data.geus.dk/JupiterWWW/borerapport.jsp?dgunr=${ + feature.properties.boreholeno + }">${feature.properties.boreholeno}</a></p> + <b style="color: rgb(16, 174, 140)">${ + names[lastSelectedChemical] + }</b><br>${html.join("<br>")}`); } - }, - - getExistingActiveProfiles: () => { - if (dashboardComponentInstance) { - return dashboardComponentInstance.getActiveProfileObjects(); - } else { - throw new Error('Unable to find the component instance'); - } - }, - - /** - * Returns current module state - */ - getState: () => { - let plots = dashboardComponentInstance.dehydratePlots(_self.getExistingActivePlots()); - let profiles = _self.getExistingActiveProfiles(); - return { - plots, - profiles, - selectedChemical: lastSelectedChemical, - enabledLoctypeIds - }; - }, - - /** - * Applies externally provided state - */ - applyState: (newState) => { - return new Promise((resolve, reject) => { - let plotsWereProvided = false; - if (newState && `plots` in newState && newState.plots.length > 0) { - plotsWereProvided = true; - } - - let profilesWereProvided = false; - if (newState && `profiles` in newState && newState.profiles.length > 0) { - profilesWereProvided = true; - } - - const continueWithInitialization = (populatedPlots) => { - if (populatedPlots) { - dashboardComponentInstance.setProjectPlots(populatedPlots); - populatedPlots.map((item) => { - dashboardComponentInstance.handleShowPlot(item.id); - }); - if (window.menuTimeSeriesComponentInstance) { - window.menuTimeSeriesComponentInstance.setPlots(dashboardComponentInstance.getPlots()); - } - } - - if (newState.enabledLoctypeIds && Array.isArray(newState.enabledLoctypeIds)) { - enabledLoctypeIds = newState.enabledLoctypeIds; - } - - if (newState.selectedChemical) { - lastSelectedChemical = newState.selectedChemical; - - if (plotsWereProvided) { - $(`[href="#watsonc-content"]`).trigger(`click`); - } + }); + } + }, - backboneEvents.get().once("allDoneLoading:layers", e => { - setTimeout(() => { - _self.enableChemical(newState.selectedChemical); - resolve(); - }, 1000); - }); - } else { - $(`.js-clear-breadcrubms`).trigger(`click`); - if (plotsWereProvided) { - $(`[href="#watsonc-content"]`).trigger(`click`); - } + getExistingPlots: () => { + if (dashboardComponentInstance) { + return dashboardComponentInstance.getPlots(); + } else { + throw new Error(`Unable to find the component instance`); + } + }, - resolve(); - } + getExistingActivePlots: () => { + if (dashboardComponentInstance) { + return dashboardComponentInstance.getActivePlots(); + } else { + throw new Error(`Unable to find the component instance`); + } + }, + + /** + * Returns current module state + */ + getState: () => { + let sources = []; + + const boreholeFeatureClone = JSON.parse( + JSON.stringify(reduxStore.getState().global.boreholeFeatures) + ); + boreholeFeatureClone.map((i) => { + let p = {}; + p.properties = {}; + p.properties.loc_id = i.properties.loc_id; + p.properties.gid = i.properties.gid; + p.properties.locname = i.properties.locname; + p.properties.relation = i.properties.relation; + sources.push(p); + }); + const plotsClone = JSON.parse( + JSON.stringify(dashboardComponentInstance.state.plots) + ).map((o) => { + delete o.measurementsCachedData; + return o; + }); + const profilesClone = JSON.parse( + JSON.stringify(dashboardComponentInstance.state.profiles) + ).map((o) => { + delete o.data; + return o; + }); + let dashboardItemsClone = JSON.parse( + JSON.stringify(dashboardComponentInstance.state.dashboardItems) + ); + //debugger + return (state = { + dashboardItems: dashboardItemsClone + .map((e) => e.item) + .map((o) => { + if (o?.profile?.data?.data) delete o.profile.data.data; + if (o?.measurementsCachedData) delete o.measurementsCachedData; + return o; + }), + sources: sources, + }); + }, + + /** + * Applies externally provided state + */ + applyState: (newState) => { + setTimeout(() => { + reduxStore.dispatch(clearBoreholeFeatures()); + if (newState?.sources) { + newState.sources.forEach((feature) => { + reduxStore.dispatch(addBoreholeFeature(feature)); + }); + } + + async function fetchTimeSeries(loc_id, relation) { + return await fetch( + `/api/sql/jupiter?q=SELECT * FROM ${relation} WHERE loc_id='${loc_id}'&base64=false&lifetime=60&srs=4326` + ); + } + async function fetchProfile(key) { + return await fetch(`/api/key-value/jupiter/${key}`); + } + + async function loop() { + let allPlots = []; + for (let u = 0; u < newState.dashboardItems.length; u++) { + let plot = newState.dashboardItems[u]; + if (!!plot?.id) { + let plotData = { + id: plot.id, + title: plot.title, + aggregate: plot.aggregate, + measurements: [], + measurementsCachedData: {}, + relations: {}, }; - - if (plotsWereProvided) { - (function poll() { - if (typeof dashboardComponentInstance === "object") { - dashboardComponentInstance.hydratePlotsFromIds(newState.plots).then(continueWithInitialization).catch(error => { - console.error(`Error occured while hydrating plots at state application`, error); - }); - } else { - setTimeout(() => { - poll(); - }, 100) - } - }()); - - } else { - continueWithInitialization(); - } - - if (profilesWereProvided) { - (function poll() { - if (typeof dashboardComponentInstance === "object") { - dashboardComponentInstance.setProjectProfiles(newState.profiles); - if (window.menuProfilesComponentInstance) { - window.menuProfilesComponentInstance.setProfiles(dashboardComponentInstance.getProfiles()); - } - } else { - setTimeout(() => { - poll(); - }, 100) - } - }()); + for (let i = 0; i < plot.measurements.length; i++) { + const loc_id = plot.measurements[i].split(":")[0]; + const index = plot.measurements[i].split(":")[2]; + const measurement = loc_id + ":_0:" + index; + const relation = plot.relations[measurement]; + const res = await fetchTimeSeries(loc_id, relation); + const json = await res.json(); + const props = json.features[0].properties; + plotData.measurements.push(measurement); + plotData.relations[measurement] = relation; + plotData.measurementsCachedData[measurement] = { + data: { + properties: { + _0: JSON.stringify({ + unit: props.unit[i], + title: props.ts_name[i], + locname: props.locname, + intakes: [1], + boreholeno: loc_id, + data: props.data, + trace: props.trace, + parameter: props.parameter[i], + ts_id: props.ts_id, + ts_name: props.ts_name, + }), + boreholeno: loc_id, + numofintakes: 1, + }, + }, + }; } - }); - } + allPlots.push(plotData); + } else { + const res = await fetchProfile(plot.key); + const json = await res.json(); + plot.profile.data = JSON.parse(json.data.value).profile.data; + allPlots.push(plot); + } + } + return allPlots; + } + + loop().then((plots) => { + dashboardComponentInstance.setItems(plots); + }); + // dashboardComponentInstance.setItems(newState.profiles) + }, 2000); + }, }; diff --git a/browser/redux/actions.js b/browser/redux/actions.js index 0ac5c13..f6da605 100644 --- a/browser/redux/actions.js +++ b/browser/redux/actions.js @@ -37,3 +37,32 @@ export const selectMeasurementCount = count => ({ type: 'SELECT_MEASUREMENT_COUNT', payload: count }); + +export const setBoreholeFeatures = features => ({ + type: 'SET_BOREHOLE_FEATURES', + payload: features +}); + +export const addBoreholeFeature = feature => ({ + type: 'ADD_BOREHOLE_FEATURE', + payload: feature +}); + +export const clearBoreholeFeatures = feature => ({ + type: 'CLEAR_BOREHOLE_FEATURES' +}); + +export const setLimits = limits => ({ + type: 'SET_LIMITS', + payload: limits +}); + +export const setDashboardMode = mode => ({ + type: 'SET_DASHBOARD_MODE', + payload: mode +}); + +export const setDashboardContent = contentType => ({ + type: 'SET_DASHBOARD_CONTENT', + payload: contentType +}); diff --git a/browser/redux/reducers.js b/browser/redux/reducers.js index 7a235b0..8c602a0 100644 --- a/browser/redux/reducers.js +++ b/browser/redux/reducers.js @@ -1,45 +1,92 @@ -import { combineReducers } from 'redux'; +import { combineReducers } from "redux"; const initialState = { - authenticated: false, - categories: false, - selectedLayers: [], - selectedChemical: "99999", - selectedStartDate: "", - selectedEndDate: "", - selectedMeasurementCount: 0 + authenticated: false, + categories: false, + selectedLayers: [], + selectedChemical: "99999", + selectedStartDate: "", + selectedEndDate: "", + selectedMeasurementCount: 0, + boreholeFeatures: [], + boreholeChemicals: {}, + limits: {}, + dashboardMode: "minimized", + dashboardContent: "charts", }; const reducer = (state = initialState, action) => { - let compoundLayerKey, selectedLayers; - switch (action.type) { - case 'SET_AUTHENTICATED': - return Object.assign({}, state, {authenticated: action.payload}); - case 'SET_CATEGORIES': - return Object.assign({}, state, {categories: action.payload}); - case 'SELECT_CHEMICAL': - return Object.assign({}, state, {selectedChemical: action.payload}); - case 'SELECT_START_DATE': - return Object.assign({}, state, {selectedStartDate: action.payload}); - case 'SELECT_END_DATE': - return Object.assign({}, state, {selectedEndDate: action.payload}); - case 'SELECT_MEASUREMENT_COUNT': - return Object.assign({}, state, {selectedMeasurementCount: action.payload}); - case 'SELECT_LAYER': - compoundLayerKey = action.payload.originalLayerKey + (action.payload.additionalKey ? `#${action.payload.additionalKey}` : ``); - selectedLayers = state.selectedLayers.slice(0); - if (selectedLayers.indexOf(compoundLayerKey) === -1) selectedLayers.push(compoundLayerKey); + let compoundLayerKey, selectedLayers; + switch (action.type) { + case "SET_AUTHENTICATED": + return Object.assign({}, state, { authenticated: action.payload }); + case "SET_CATEGORIES": + return Object.assign({}, state, { categories: action.payload }); + case "SET_BOREHOLE_FEATURES": + return Object.assign({}, state, { boreholeFeatures: action.payload }); + case "ADD_BOREHOLE_FEATURE": + let features = state.boreholeFeatures.slice(0); + let found = false; + for (let i = 0; i < features.length; i++) { + if ( + features[i].properties.relation === + action.payload.properties?.relation && + features[i].properties.loc_id === action.payload.properties?.loc_id + ) { + found = true; + break; + } + } + if (found) { + return state; + } + features.push(action.payload); + return Object.assign({}, state, { boreholeFeatures: features }); + case "CLEAR_BOREHOLE_FEATURES": + return Object.assign({}, state, { boreholeFeatures: [] }); + case "SET_LIMITS": + return Object.assign({}, state, { limits: action.payload }); + case "SET_DASHBOARD_MODE": + return Object.assign({}, state, { dashboardMode: action.payload }); + case "SET_DASHBOARD_CONTENT": + return Object.assign({}, state, { dashboardContent: action.payload }); + case "SET_BOREHOLE_CHEMICALS": + return Object.assign({}, state, { boreholeChemicals: action.payload }); + case "SELECT_CHEMICAL": + return Object.assign({}, state, { selectedChemical: action.payload }); + case "SELECT_START_DATE": + return Object.assign({}, state, { selectedStartDate: action.payload }); + case "SELECT_END_DATE": + return Object.assign({}, state, { selectedEndDate: action.payload }); + case "SELECT_MEASUREMENT_COUNT": + return Object.assign({}, state, { + selectedMeasurementCount: action.payload, + }); + case "SELECT_LAYER": + compoundLayerKey = + action.payload.originalLayerKey + + (action.payload.additionalKey + ? `#${action.payload.additionalKey}` + : ``); + selectedLayers = state.selectedLayers.slice(0); + if (selectedLayers.indexOf(compoundLayerKey) === -1) + selectedLayers.push(compoundLayerKey); - return Object.assign({}, state, {selectedLayers}); - case 'UNSELECT_LAYER': - compoundLayerKey = action.payload.originalLayerKey + (action.payload.additionalKey ? `#${action.payload.additionalKey}` : ``); - selectedLayers = state.selectedLayers.slice(0); - if (selectedLayers.indexOf(compoundLayerKey) !== -1) selectedLayers.splice(selectedLayers.indexOf(compoundLayerKey), 1); + return Object.assign({}, state, { selectedLayers }); + case "UNSELECT_LAYER": + compoundLayerKey = + action.payload.originalLayerKey + + (action.payload.additionalKey + ? `#${action.payload.additionalKey}` + : ``); + selectedLayers = state.selectedLayers.slice(0); + if (selectedLayers.indexOf(compoundLayerKey) !== -1) + selectedLayers.splice(selectedLayers.indexOf(compoundLayerKey), 1); - return Object.assign({}, state, {selectedLayers}); - default: - return state - } -} + return Object.assign({}, state, { selectedLayers }); + default: + return state; + } +}; -export default combineReducers({global: reducer}); +export default combineReducers({ global: reducer }); diff --git a/browser/themes/DarkTheme.js b/browser/themes/DarkTheme.js index d7ea6a5..e8f79c2 100644 --- a/browser/themes/DarkTheme.js +++ b/browser/themes/DarkTheme.js @@ -1,48 +1,42 @@ export const DarkTheme = { branding: null, // Defined in custom brandings - buttons: { - background: '#ffa137', - color: '#fff' - }, - padding: { - titlePadding: '30px' - }, colors: { background: "#F8F8F8", text: "#242323", - headings: "#292A2C", - primary: { // Low index = low brightness + headings: "#FFFFFF", + primary: { + // Low index = low brightness 1: "#001E1B", 2: "#003C36", 3: "#005A51 ", 4: "#00786D", - 5: "#009688" + 5: "#009688", }, interaction: { 1: "#33200B", 2: "#996121", 3: "#CC812C", 4: "#FFA137", - 5: "#FFB45F" + 5: "#FFB45F", }, gray: { 1: "#242323", 2: "#6C6969", 3: "#B4AFAF", 4: "#D2CFCF", - 5: "#F0EFEF" + 5: "#F0EFEF", }, denotive: { - success: "#19B037", - warning: "#FFA137", - error: "#FC3C3C" - }, + success: "#19B037", + warning: "#FFA137", + error: "#FC3C3C", + }, }, layout: { borderRadius: { - small: 4, - medium: 8, - large: 16 + small: 4, + medium: 8, + large: 16, }, gutter: 32, sidebar: [90, 300], @@ -50,15 +44,16 @@ export const DarkTheme = { breakpoints: { small: "@media (min-width: 480px)", medium: "@media (min-width: 768px)", - large: "@media (min-width: 1200px)" - } + large: "@media (min-width: 1200px)", + }, }, fonts: { - title: "'bold 40px Lato'", - subtitle: "'regular 24px Lato'", - heading: "'bold 18px Open Sans'", - body: "'regular 15px Open Sans'", - subbody: "'regular 13px Open Sans'", - footnote: "'regular 11px Open Sans'", - } + title: "bold 40px Roboto", + subtitle: "300 24px Roboto", + heading: "300 20px Roboto", + body: "300 15px Roboto", + subbody: "300 13px Roboto", + label: "300 12px Roboto", + footnote: "300 9px Roboto", + }, }; diff --git a/browser/themes/ThemeProvider.js b/browser/themes/ThemeProvider.js index 4f360f3..f1538e5 100644 --- a/browser/themes/ThemeProvider.js +++ b/browser/themes/ThemeProvider.js @@ -1,15 +1,16 @@ -import React from 'react'; +import React from "react"; import { ThemeProvider as StyledThemeProvider } from "styled-components"; +import "react-toastify/dist/ReactToastify.css"; import { DarkTheme } from "./DarkTheme"; class ThemeProvider extends React.Component { - render() { - return ( - <StyledThemeProvider theme={DarkTheme}> - {this.props.children} - </StyledThemeProvider> - ) - } + render() { + return ( + <StyledThemeProvider theme={DarkTheme}> + {this.props.children} + </StyledThemeProvider> + ); + } } -export default ThemeProvider +export default ThemeProvider; diff --git a/browser/utils.js b/browser/utils.js index ec828fc..baead8b 100644 --- a/browser/utils.js +++ b/browser/utils.js @@ -1,9 +1,9 @@ /** * Extracts the chemical name from existing categories - * + * * @param {String} chemicalId Chemical identifier * @param {Array} categories Existing categories - * + * * @returns {String} */ const getChemicalName = (chemicalId, categories) => { @@ -31,9 +31,9 @@ const getChemicalName = (chemicalId, categories) => { /** * Detects the measurement title - * + * * @param {Object} measurement Measurement object - * + * * @returns {String} */ const getMeasurementTitle = (measurement) => { @@ -48,7 +48,53 @@ const getMeasurementTitle = (measurement) => { return title; }; +const getLogo = () => { + let svg = `<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 321.2 93" style="enable-background:new 0 0 321.2 93; height: 40px;" xml:space="preserve"> + <style type="text/css"> + .st0{fill:#A0E0C5;} + .st1{fill:#FFFFFF;} + </style> + <g> + <g> + <path class="st0" d="M274.9,17.3c5.9,1.2,11,5.5,13.2,11c1.3,3.2,1.8,6.9,4.3,9.2c2,1.8,4.9,2.3,7.5,1.6c2.6-0.7,4.8-2.5,6.3-4.7 + c3.6-5.2,3.2-12.5,0-17.9c-3.2-5.4-9-9-15.2-10.6c-10.1-2.5-21.5,0.5-28.6,8.2c-1.4,1.5-2.6,3.2-3.6,5c-0.6,1,0.6,2.2,1.6,1.6 + C264.5,18.4,270.5,16.4,274.9,17.3"/> + <path class="st1" d="M313.6,54.9c-4.3-5.9-11.3-5.4-17.1-3.4c-4.4,1.5-8.5,3.8-13,4.7c-10.2,2.1-21.3-3.8-25.8-13.2 + c-2.3-4.8-2.9-10.4-1.9-15.6c0.2-1.2-1.3-1.9-2.1-1c-6.7,8.3-9.9,19.4-8.2,30c2.2,13.3,12.1,25.1,24.8,29.5 + c12.7,4.4,27.8,1.3,37.7-7.8c4.7-4.3,8.4-10.2,7.9-16.9C315.7,59,314.9,56.8,313.6,54.9"/> + </g> + <g> + <path class="st1" d="M12.4,55.7c0,6.6,2.7,11.6,9.5,11.6c3.2,0,6.5-1.5,8.8-4.5c0.2-0.4,0.7-0.6,1.2-0.6c0.2,0,0.5,0.1,0.7,0.2 + l3.1,1.9c0.4,0.3,0.6,0.7,0.6,1.2c0,0.3-0.1,0.6-0.2,0.8c-2.7,4.7-7.7,7.5-14.2,7.5c-10.5,0-16.6-7.5-16.6-18 + c0-10.6,6.1-18.5,16.6-18.5c6.5,0,11.6,2.9,14.3,7.5c0.2,0.2,0.2,0.5,0.2,0.8c0,0.5-0.2,0.9-0.6,1.2l-3,1.9 + c-0.2,0.1-0.5,0.2-0.7,0.2c-0.6,0-1-0.2-1.2-0.6c-2.2-3-5.5-4.5-8.8-4.5C15.1,43.6,12.4,49.3,12.4,55.7z"/> + <path class="st1" d="M57.5,37.8c0.7,0,1.4,0.6,1.7,1.4l9.5,32.5c0,0.8-0.7,1.4-1.4,1.4h-4.2c-0.7,0-1.4-0.6-1.6-1.4l-1-4.1H46.9 + l-1.1,4.1c-0.2,0.7-0.9,1.4-1.6,1.4H40c-0.7,0-1.4-0.6-1.4-1.4l9.5-32.5c0.2-0.8,0.9-1.4,1.7-1.4H57.5z M58.4,61.3l-4.7-16.5 + l-4.8,16.5H58.4z"/> + <path class="st1" d="M74.6,39.2c0-0.4,0.2-0.8,0.5-1.1c0.2-0.2,0.6-0.4,0.9-0.4h4.3c0.4,0,0.8,0.1,1.1,0.4c0.2,0.2,0.4,0.6,0.4,1 + v27.2h14.4c0.4,0,0.8,0.2,1.1,0.5c0.2,0.2,0.3,0.6,0.3,0.9v4c0,0.4-0.1,0.7-0.4,0.9c-0.2,0.2-0.6,0.4-1,0.4H76 + c-0.4,0-0.8-0.2-1.1-0.4c-0.2-0.2-0.4-0.6-0.4-1V39.2z"/> + <path class="st1" d="M110.6,53l6.8-13.8c0.4-0.7,1.1-1.4,1.8-1.4h4.7c0.7,0,1.4,0.6,1.4,1.4L114.1,62v9.8c0,0.8-0.7,1.4-1.4,1.4 + h-4.3c-0.7,0-1.4-0.6-1.4-1.4V62L95.7,39.2c0-0.8,0.7-1.4,1.4-1.4h4.7c0.7,0,1.4,0.7,1.8,1.4L110.6,53z"/> + <path class="st1" d="M141.1,37.8c10.5,0,15.2,6.2,15.2,12.9v0.5c0,6.7-4.7,13.1-15.2,13.1h-4.9v7.3c0,0.8-0.7,1.4-1.4,1.4h-4.3 + c-0.7,0-1.4-0.6-1.4-1.4V39.2c0-0.8,0.7-1.4,1.4-1.4H141.1z M136.3,44.1v13.7h4.9c7,0,8-3.3,8-6.6v-0.5c0-3.4-1.1-6.7-8-6.7H136.3 + z"/> + <path class="st1" d="M184.5,47.6h-4c-0.7,0-1.6-0.7-1.9-1.5c-1.2-2.4-3.8-2.6-7-2.6c-3.7,0-5.7,1.4-5.7,3.7 + c0,5.5,21.2,4.5,21.2,15.6c0,8.5-6,11.1-13.9,11.1c-7.5,0-13.5-2.9-14.2-8.9c0-0.8,0.6-1.7,1.6-1.7h4c0.7,0,1.6,0.6,1.9,1.4 + c1,2.3,4.2,2.9,6.8,2.9c3.2,0,6.8,0,6.8-4.3c0-6.1-21.2-4-21.2-15.9c0-6.2,4.9-10.1,12.8-10.1c7.7,0,13.7,2.8,14.5,8.7 + C186.1,46.7,185.5,47.6,184.5,47.6z"/> + <path class="st1" d="M190.2,55.7c0-10.6,6.1-18.5,16.5-18.5c10.5,0,16.5,7.9,16.5,18.5c0,10.6-6.1,18-16.5,18 + C196.3,73.8,190.2,66.3,190.2,55.7z M216.1,55.7c0-6.5-2.5-12.3-9.4-12.3c-6.8,0-9.4,5.7-9.4,12.3c0,6.7,2.6,11.8,9.4,11.8 + C213.6,67.5,216.1,62.4,216.1,55.7z"/> + </g> + </g> + </svg>`; + return 'data:image/svg+xml;base64,' + btoa(svg); +} + module.exports = { getChemicalName, - getMeasurementTitle -} \ No newline at end of file + getMeasurementTitle, + getLogo +} diff --git a/config/_variables.less b/config/_variables.less index 689632a..32af3e8 100644 --- a/config/_variables.less +++ b/config/_variables.less @@ -2,7 +2,7 @@ @import "_colors.less"; // Typography elements -@mdb-font-family: 'Open Sans', sans-serif; +@mdb-font-family: 'Roboto', sans-serif; @mdb-text-color-light: ~"rgba(@{rgb-white}, 0.84)"; @mdb-text-color-light-hex: @white; // for contrast function in inverse @mdb-text-color-primary: ~"rgba(@{rgb-black}, 0.87)"; @@ -33,7 +33,7 @@ @border-radius-small: 1px; // Typography -@font-family-sans-serif: 'Open Sans', sans-serif; +@font-family-sans-serif: 'Roboto', sans-serif; @headings-font-weight: 300; @body-bg: #EEEEEE; @@ -172,4 +172,4 @@ /* SHADOWS */ @mdb-shadow-key-umbra-opacity: 0.2; @mdb-shadow-key-penumbra-opacity: 0.14; -@mdb-shadow-ambient-shadow-opacity: 0.12; \ No newline at end of file +@mdb-shadow-ambient-shadow-opacity: 0.12; diff --git a/package-lock.json b/package-lock.json index eff86a3..25fb8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -936,6 +936,80 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "@popperjs/core": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.3.tgz", + "integrity": "sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==" + }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + }, + "@types/react": { + "version": "17.0.40", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.40.tgz", + "integrity": "sha512-UrXhD/JyLH+W70nNSufXqMZNuUD2cXHu6UjCllC6pmOQgBX4SGXOH8fjRka0O0Ee0HrFxapDD8Bwn81Kmiz6jQ==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-redux": { + "version": "7.1.23", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.23.tgz", + "integrity": "sha512-D02o3FPfqQlfu2WeEYwh3x2otYd2Dk1o8wAfsA0B1C2AJEFxE663Ozu7JzuWbznGgW248NaOF6wsqCGNq9d3qw==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "CSSselect": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/CSSselect/-/CSSselect-0.4.1.tgz", + "integrity": "sha1-+Kt+H4QYzmPNput713ioXX7EkrI=", + "requires": { + "CSSwhat": "0.4", + "domutils": "1.4" + } + }, + "CSSwhat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/CSSwhat/-/CSSwhat-0.4.7.tgz", + "integrity": "sha1-hn2g/zn3eGEyQsRM/qg/CqTr35s=" + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -973,6 +1047,12 @@ "object-assign": "4.x" } }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "optional": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1083,9 +1163,9 @@ } }, "array-move": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz", - "integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-move/-/array-move-3.0.1.tgz", + "integrity": "sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==" }, "array-unique": { "version": "0.3.2", @@ -1093,11 +1173,6 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "optional": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, "asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -1623,10 +1698,24 @@ "supports-color": "^5.3.0" } }, - "change-emitter": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", - "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + "cheerio": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.17.0.tgz", + "integrity": "sha1-+lrkLMYBIRM9KW0LRtmDIV9yaOo=", + "requires": { + "CSSselect": "~0.4.0", + "dom-serializer": "~0.0.0", + "entities": "~1.1.1", + "htmlparser2": "~3.7.2", + "lodash": "~2.4.1" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + } + } }, "chokidar": { "version": "2.1.8", @@ -1685,6 +1774,11 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -1801,6 +1895,14 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "optional": true }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", @@ -1932,6 +2034,11 @@ "postcss-value-parser": "^4.0.2" } }, + "csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" + }, "dash-ast": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", @@ -2067,14 +2174,13 @@ } }, "dnd-core": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-6.0.0.tgz", - "integrity": "sha512-WnnFSbnC3grP/XJ+xfxgM8DyIsts3Q/rfgE6WGRWs6tCQcwILputNNm/Kw+WPS2N1e46hRy5iPl2pYwkP9kK9Q==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.0.tgz", + "integrity": "sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA==", "requires": { - "asap": "^2.0.6", - "invariant": "^2.2.4", - "lodash": "^4.17.11", - "redux": "^4.0.1" + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.5" } }, "dnd-multi-backend": { @@ -2087,11 +2193,48 @@ "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.10.4.tgz", "integrity": "sha512-wytDzaru67AmqFOY4B9GUb/hrwWagezoYYK97D/vpK+ezg+cnuZO0Q2gltUPa7KfNmIqfRIYVCF8UhRDEHAmgQ==" }, + "dom-serializer": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.0.1.tgz", + "integrity": "sha1-lYmCfx4y0iw3yCmtq9WbMkevjq8=", + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.2.1.tgz", + "integrity": "sha1-Wd+dzSJ+gIs2Wuc+H2aErD2Ub8I=", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", + "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "requires": { + "domelementtype": "1" + } + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2119,14 +2262,6 @@ "minimalistic-crypto-utils": "^1.0.0" } }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2135,6 +2270,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -2145,11 +2285,48 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", + "integrity": "sha1-8CQBb1qI4Eb9EgBQVek5gC5sXyM=", + "requires": { + "esprima": "~1.1.1", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.33" + }, + "dependencies": { + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "eslint-visitor-keys": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" }, + "esprima": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz", + "integrity": "sha1-W28VR/TRAuZw4UDFCb5ncdautUk=" + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2339,6 +2516,54 @@ } } }, + "extract-svg-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/extract-svg-path/-/extract-svg-path-2.1.0.tgz", + "integrity": "sha1-YGKkpLuA/qit8JfY9MKZuIxTrSg=", + "requires": { + "cheerio": "^0.17.0", + "jsesc": "^0.5.0", + "object-assign": "^4.0.1", + "static-module": "^1.1.3", + "through2": "^2.0.0", + "xml-parse-from-string": "^1.0.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "extract-svg-viewbox": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/extract-svg-viewbox/-/extract-svg-viewbox-1.0.1.tgz", + "integrity": "sha1-b/6jAAdV8FUAMDZyU3szKfJdJ4o=" + }, + "falafel": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz", + "integrity": "sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==", + "requires": { + "acorn": "^7.1.1", + "foreach": "^2.0.5", + "isarray": "^2.0.1", + "object-keys": "^1.0.6" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } + } + }, "fast-csv": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-2.5.0.tgz", @@ -2351,32 +2576,16 @@ "string-extended": "0.0.8" } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2412,6 +2621,16 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "optional": true }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2437,493 +2656,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", - "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.3", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.4.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.7", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "optional": true - } + "nan": "^2.12.1" } }, "fstream": { @@ -3093,19 +2832,60 @@ "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" }, + "htmlparser2": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.7.3.tgz", + "integrity": "sha1-amTHdjfAjG8w7CqBV6UzM758sF4=", + "requires": { + "domelementtype": "1", + "domhandler": "2.2", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + }, + "dependencies": { + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -3299,11 +3079,6 @@ "isobject": "^3.0.1" } }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -3321,15 +3096,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "optional": true }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3656,9 +3422,9 @@ "optional": true }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, "nanomatch": { @@ -3680,15 +3446,6 @@ "to-regex": "^3.0.1" } }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, "node-releases": { "version": "1.1.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.50.tgz", @@ -3755,6 +3512,11 @@ "is-extended": "~0.0.3" } }, + "object-inspect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-0.4.0.tgz", + "integrity": "sha1-9RV8EWwUVbJDsG7pdwM5LFrYn+w=" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -3893,6 +3655,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==" + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -3908,14 +3675,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - }, "promish": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/promish/-/promish-5.1.1.tgz", @@ -3962,6 +3721,65 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, + "quote-stream": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-0.0.0.tgz", + "integrity": "sha1-zeKelMQJsW4Z3HCYuJtmWPlyHTs=", + "requires": { + "minimist": "0.0.8", + "through2": "~0.4.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, "raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -4064,26 +3882,33 @@ "shallowequal": "^1.1.0" } }, + "react-copy-to-clipboard": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz", + "integrity": "sha512-9S3j+m+UxDZOM0Qb8mhnT/rMR0NGSrj9A/073yz2DSxPMYhmYFBMYIdI2X4o8AjOjyFsSNxDRnCX6s/gRxpriw==", + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, "react-dnd": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-6.0.0.tgz", - "integrity": "sha512-XI14rxF5eeGk8045xh/6KbjfLSzgkfNdQCqwkR5qAvBf0QYvkGAUz1AQfLrQudFs/DVw7WiCoCohRzJR1Kyn9Q==", - "requires": { - "dnd-core": "^6.0.0", - "hoist-non-react-statics": "^3.1.0", - "invariant": "^2.1.0", - "lodash": "^4.17.11", - "recompose": "^0.30.0", - "shallowequal": "^1.1.0" + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.2.tgz", + "integrity": "sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.0", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" } }, "react-dnd-html5-backend": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-6.0.0.tgz", - "integrity": "sha512-NRaApaf3IBrE/LgOCoTqbsI1MT5HKIBDkyMF1zoJUy+slW/RLjLuxMC2gYU1Y89Ra/e7CQ5Hw/gJY7oIXLK32g==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz", + "integrity": "sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g==", "requires": { - "dnd-core": "^6.0.0", - "lodash": "^4.17.11" + "dnd-core": "14.0.0" } }, "react-dnd-multi-backend": { @@ -4112,6 +3937,11 @@ "invariant": "^2.2.4" } }, + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, "react-is": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", @@ -4122,17 +3952,46 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-popper": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", + "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, "react-redux": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz", - "integrity": "sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", + "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", "requires": { - "@babel/runtime": "^7.3.1", - "hoist-non-react-statics": "^3.3.0", - "invariant": "^2.2.4", + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", "loose-envify": "^1.4.0", "prop-types": "^15.7.2", - "react-is": "^16.8.2" + "react-is": "^17.0.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.7.tgz", + "integrity": "sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + } } }, "react-sortable-hoc": { @@ -4145,6 +4004,14 @@ "prop-types": "^15.5.7" } }, + "react-toastify": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.4.tgz", + "integrity": "sha512-Rol7+Cn39hZp5hQ/k6CbMNE2CKYV9E5OQdC/hBLtIQU2xz7DdAm7xil4NITQTHR6zEbE5RVFbpgSwTD7xRGLeQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-tooltip": { "version": "3.11.6", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.11.6.tgz", @@ -4186,35 +4053,29 @@ "readable-stream": "^2.0.2" } }, - "recompose": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", - "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", "requires": { - "@babel/runtime": "^7.0.0", - "change-emitter": "^0.1.2", - "fbjs": "^0.8.1", - "hoist-non-react-statics": "^2.3.1", - "react-lifecycles-compat": "^3.0.2", - "symbol-observable": "^1.0.4" + "@babel/runtime": "^7.9.2" }, "dependencies": { - "hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + "@babel/runtime": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz", + "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" } } }, - "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -4410,6 +4271,11 @@ "safe-buffer": "^5.0.1" } }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, "shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -4587,6 +4453,36 @@ "extend-shallow": "^3.0.0" } }, + "static-eval": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-0.2.4.tgz", + "integrity": "sha1-t9NNg4k3uWn5ZBygfUj47eJj6ns=", + "requires": { + "escodegen": "~0.0.24" + }, + "dependencies": { + "escodegen": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz", + "integrity": "sha1-Dk/xcV8yh3XWyrUaxEpAbNer/9M=", + "requires": { + "esprima": "~1.0.2", + "estraverse": "~1.3.0", + "source-map": ">= 0.1.2" + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz", + "integrity": "sha1-N8K4k+8T1yPydth41g2FNRUqbEI=" + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -4608,6 +4504,90 @@ } } }, + "static-module": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/static-module/-/static-module-1.5.0.tgz", + "integrity": "sha1-J9qYg8QajNCSNvhC8MHrxu32PYY=", + "requires": { + "concat-stream": "~1.6.0", + "duplexer2": "~0.0.2", + "escodegen": "~1.3.2", + "falafel": "^2.1.0", + "has": "^1.0.0", + "object-inspect": "~0.4.0", + "quote-stream": "~0.0.0", + "readable-stream": "~1.0.27-1", + "shallow-copy": "~0.0.1", + "static-eval": "~0.2.0", + "through2": "~0.4.1" + }, + "dependencies": { + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "requires": { + "readable-stream": "~1.1.9" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -4710,11 +4690,6 @@ "has-flag": "^3.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, "syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -4824,6 +4799,11 @@ "repeat-string": "^1.6.1" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -4839,11 +4819,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "ua-parser-js": { - "version": "0.7.21", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", - "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==" - }, "umd": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", @@ -5017,6 +4992,14 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "walk": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", + "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", + "requires": { + "foreachasync": "^3.0.0" + } + }, "warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -5025,16 +5008,16 @@ "loose-envify": "^1.0.0" } }, - "whatwg-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha1-qQKekp09vN7RafPG4oI42VpdWig=" + }, "xmlbuilder": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", diff --git a/package.json b/package.json index ef25c97..f208e19 100644 --- a/package.json +++ b/package.json @@ -16,21 +16,29 @@ "@babel/plugin-proposal-object-rest-spread": "^7.2.0", "@babel/preset-env": "^7.2.0", "@babel/preset-react": "^7.0.0", - "array-move": "^2.1.0", + "@popperjs/core": "^2.9.3", + "array-move": "^3.0.1", "babel-eslint": "^10.0.1", "babelify": "10", "browserify": "^16.2.3", "excel4node": "^1.7.2", "exceljs": "^1.13.0", + "extract-svg-path": "^2.1.0", + "extract-svg-viewbox": "^1.0.1", + "prettier": "^2.2.1", "rc-slider": "^8.6.9", - "react-dnd": "^6.0.0", - "react-dnd-html5-backend": "^6.0.0", + "react-copy-to-clipboard": "^5.0.3", + "react-dnd": "^14.0.2", + "react-dnd-html5-backend": "^14.0.0", "react-dnd-multi-backend": "^3.1.11", "react-dnd-touch-backend": "^0.7.1", - "react-redux": "^6.0.1", + "react-popper": "^2.2.5", + "react-redux": "^7.2.6", "react-sortable-hoc": "^1.8.3", + "react-toastify": "^7.0.4", "react-tooltip": "^3.10.0", - "styled-components": "^5.2.3" + "styled-components": "^5.2.3", + "walk": "^2.3.14" }, "keywords": [ "Vidi" diff --git a/public/index.html b/public/index.html index 62408e1..b2a1ffa 100644 --- a/public/index.html +++ b/public/index.html @@ -21,7 +21,7 @@ ~ @author Martin Høgh <mh@mapcentia.com> ~ @copyright 2013-2020 MapCentia ApS ~ @license http://www.gnu.org/licenses/#AGPL GNU AFFERO GENERAL PUBLIC LICENSE 3 - ~ @version 2020.12.0 + ~ @version 05b816a1 --> <html lang="en"> @@ -31,7 +31,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="google" value="notranslate"> <meta name="Description" content="A platform for building spatial data infrastructure and deploying browser based GIS."> - <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"> + <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"> <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png"> diff --git a/scripts/icon-generator/generate.js b/scripts/icon-generator/generate.js new file mode 100644 index 0000000..771a190 --- /dev/null +++ b/scripts/icon-generator/generate.js @@ -0,0 +1,69 @@ +(function() { + "use strict"; + const walk = require("walk"); + const path = require("path"); + const fs = require("fs"); + const extractSvgPath = require("extract-svg-path"); + const extract = require('extract-svg-viewbox'); + const prettier = require("prettier"); + const SOURCE_FOLER = __dirname + "/icons/"; + const DESTINATION_FOLDER = process.argv[2]; + const icons = []; + const walker = walk.walk(SOURCE_FOLER); + walker.on("file", async function(_root, fileStats, next) { + var isSVG = path.parse(fileStats.name).ext === ".svg"; + if (isSVG) { + var svgFilePath = SOURCE_FOLER + fileStats.name; + var svg = fs.readFileSync(svgFilePath, 'utf8'); + var svgPath = extractSvgPath(svgFilePath); + var svgViewBox = extract(svg); + icons.push({ + name: path.parse(fileStats.name).name, + path: svgPath, + viewbox: svgViewBox + }); + } + next(); + }); + + walker.on("errors", function(_root, _nodeStatsArray, next) { + next(); + }); + + walker.on("end", function() { + // JSON + fs.writeFile( + DESTINATION_FOLDER + "/icons.json", + JSON.stringify(icons), + "utf8", + function() {} + ); + + // Types + const fileContents = `export const IconName = { + ${icons.map( + icon => + `${icon.name.toUpperCase().replace(/-/g, "_")}: "${icon.name}" as "${ + icon.name + }"` + )} + } + export type IconName = typeof IconName[keyof typeof IconName];`; + fs.writeFile( + DESTINATION_FOLDER + "/icons.ts", + prettier.format(fileContents, { + parser: "typescript" + }), + "utf8", + function() { + console.log( + "🎉 Finished writing " + + icons.length + + " icons and their type definitions to " + + DESTINATION_FOLDER + + "/icons.ts" + ); + } + ); + }); +})(); diff --git a/scripts/icon-generator/icons/analytics-board-graph-line.svg b/scripts/icon-generator/icons/analytics-board-graph-line.svg new file mode 100644 index 0000000..1de13b8 --- /dev/null +++ b/scripts/icon-generator/icons/analytics-board-graph-line.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + +<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 4.75C2.08579 4.75 1.75 5.08579 1.75 5.5V19C1.75 19.4142 2.08579 19.75 2.5 19.75H22C22.4142 19.75 22.75 19.4142 22.75 19V5.5C22.75 5.08579 22.4142 4.75 22 4.75H2.5ZM0.25 5.5C0.25 4.25736 1.25736 3.25 2.5 3.25H22C23.2426 3.25 24.25 4.25736 24.25 5.5V19C24.25 20.2426 23.2426 21.25 22 21.25H2.5C1.25736 21.25 0.25 20.2426 0.25 19V5.5Z" fill="black"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20.916 7.87597C21.2607 8.10573 21.3538 8.57138 21.124 8.91603L16.624 15.666C16.4994 15.853 16.2975 15.9742 16.0739 15.9964C15.8503 16.0185 15.6285 15.9392 15.4697 15.7803L13 13.3107L10.5303 15.7803C10.363 15.9477 10.1265 16.0262 9.89234 15.9922C9.65816 15.9583 9.45375 15.8158 9.34085 15.6078L7.11742 11.512L4.64311 15.6359C4.43 15.9911 3.9693 16.1062 3.61412 15.8931C3.25893 15.68 3.14376 15.2193 3.35687 14.8641L6.50687 9.61413C6.64538 9.38328 6.89699 9.24438 7.16614 9.25018C7.4353 9.25598 7.68069 9.40559 7.80913 9.64218L10.1799 14.0094L12.4697 11.7197C12.7626 11.4268 13.2374 11.4268 13.5303 11.7197L15.8834 14.0728L19.876 8.08398C20.1057 7.73933 20.5714 7.6462 20.916 7.87597Z" fill="black"/> + +</svg> diff --git a/scripts/icon-generator/icons/arrow-down.svg b/scripts/icon-generator/icons/arrow-down.svg new file mode 100644 index 0000000..4c9a211 --- /dev/null +++ b/scripts/icon-generator/icons/arrow-down.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 14.08L11.75 17.83L15.5 14.08M11.75 7V16.25" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/scripts/icon-generator/icons/avatar.svg b/scripts/icon-generator/icons/avatar.svg new file mode 100644 index 0000000..702129d --- /dev/null +++ b/scripts/icon-generator/icons/avatar.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12 15C16 15 17.25 9.8995 17.25 7C17.25 4.10051 14.8995 1.75 12 1.75C9.10051 1.75 6.75 4.10051 6.75 7C6.75 9.8995 8 15 12 15Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2 22C2 20.1435 3.12464 18.3128 5 17C6.87536 15.6873 9.34784 16 12 16C14.6522 16 17.1246 15.6873 19 17C20.8754 18.3128 22 20.1435 22 22" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> + diff --git a/scripts/icon-generator/icons/check-mark-solid.svg b/scripts/icon-generator/icons/check-mark-solid.svg new file mode 100644 index 0000000..4aee5d9 --- /dev/null +++ b/scripts/icon-generator/icons/check-mark-solid.svg @@ -0,0 +1,13 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 352.62 352.62" + width="352.62px" height="352.62px" style="enable-background:new 0 0 352.62 352.62;" + xml:space="preserve"> +<g> + <path d="M337.222,22.952c-15.912-8.568-33.66,7.956-44.064,17.748c-23.867,23.256-44.063,50.184-66.708,74.664 + c-25.092,26.928-48.348,53.856-74.052,80.173c-14.688,14.688-30.6,30.6-40.392,48.96c-22.032-21.421-41.004-44.677-65.484-63.648 + c-17.748-13.464-47.124-23.256-46.512,9.18c1.224,42.229,38.556,87.517,66.096,116.28c11.628,12.24,26.928,25.092,44.676,25.704 + c21.42,1.224,43.452-24.48,56.304-38.556c22.645-24.48,41.005-52.021,61.812-77.112c26.928-33.048,54.468-65.485,80.784-99.145 + C326.206,96.392,378.226,44.983,337.222,22.952z M26.937,187.581c-0.612,0-1.224,0-2.448,0.611 + c-2.448-0.611-4.284-1.224-6.732-2.448l0,0C19.593,184.52,22.653,185.132,26.937,187.581z"/> +</g> +</svg> diff --git a/scripts/icon-generator/icons/cleaning-spray.svg b/scripts/icon-generator/icons/cleaning-spray.svg new file mode 100644 index 0000000..d766ab1 --- /dev/null +++ b/scripts/icon-generator/icons/cleaning-spray.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>cleaning-spray \ No newline at end of file diff --git a/scripts/icon-generator/icons/cross.svg b/scripts/icon-generator/icons/cross.svg new file mode 100644 index 0000000..ba278c2 --- /dev/null +++ b/scripts/icon-generator/icons/cross.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/dashboard-full-solid.svg b/scripts/icon-generator/icons/dashboard-full-solid.svg new file mode 100644 index 0000000..3063811 --- /dev/null +++ b/scripts/icon-generator/icons/dashboard-full-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/dashboard-half-solid.svg b/scripts/icon-generator/icons/dashboard-half-solid.svg new file mode 100644 index 0000000..7abc16d --- /dev/null +++ b/scripts/icon-generator/icons/dashboard-half-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/dashboard-minimized-solid.svg b/scripts/icon-generator/icons/dashboard-minimized-solid.svg new file mode 100644 index 0000000..cd98784 --- /dev/null +++ b/scripts/icon-generator/icons/dashboard-minimized-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/dashboard.svg b/scripts/icon-generator/icons/dashboard.svg new file mode 100644 index 0000000..9027d08 --- /dev/null +++ b/scripts/icon-generator/icons/dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/scripts/icon-generator/icons/drag-handle-solid.svg b/scripts/icon-generator/icons/drag-handle-solid.svg new file mode 100644 index 0000000..9d68920 --- /dev/null +++ b/scripts/icon-generator/icons/drag-handle-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/drag-handle.svg b/scripts/icon-generator/icons/drag-handle.svg new file mode 100644 index 0000000..9d68920 --- /dev/null +++ b/scripts/icon-generator/icons/drag-handle.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/drill-space-solid.svg b/scripts/icon-generator/icons/drill-space-solid.svg new file mode 100644 index 0000000..bfe5f5a --- /dev/null +++ b/scripts/icon-generator/icons/drill-space-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/drill.svg b/scripts/icon-generator/icons/drill.svg new file mode 100644 index 0000000..3d0a270 --- /dev/null +++ b/scripts/icon-generator/icons/drill.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/earth-layers.svg b/scripts/icon-generator/icons/earth-layers.svg new file mode 100644 index 0000000..2c7fabb --- /dev/null +++ b/scripts/icon-generator/icons/earth-layers.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/icon-generator/icons/folder-solid.svg b/scripts/icon-generator/icons/folder-solid.svg new file mode 100644 index 0000000..65069b2 --- /dev/null +++ b/scripts/icon-generator/icons/folder-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/hyperlink.svg b/scripts/icon-generator/icons/hyperlink.svg new file mode 100644 index 0000000..0a0e880 --- /dev/null +++ b/scripts/icon-generator/icons/hyperlink.svg @@ -0,0 +1 @@ +hyperlink-2 \ No newline at end of file diff --git a/scripts/icon-generator/icons/lab-flask-experiment.svg b/scripts/icon-generator/icons/lab-flask-experiment.svg new file mode 100644 index 0000000..015d477 --- /dev/null +++ b/scripts/icon-generator/icons/lab-flask-experiment.svg @@ -0,0 +1 @@ +lab-flask-experiment \ No newline at end of file diff --git a/scripts/icon-generator/icons/logo.svg b/scripts/icon-generator/icons/logo.svg new file mode 100644 index 0000000..be360a6 --- /dev/null +++ b/scripts/icon-generator/icons/logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + diff --git a/scripts/icon-generator/icons/minus.svg b/scripts/icon-generator/icons/minus.svg new file mode 100644 index 0000000..0e9a3fc --- /dev/null +++ b/scripts/icon-generator/icons/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/icon-generator/icons/no3-solid.svg b/scripts/icon-generator/icons/no3-solid.svg new file mode 100644 index 0000000..52938fd --- /dev/null +++ b/scripts/icon-generator/icons/no3-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/pin-location-solid.svg b/scripts/icon-generator/icons/pin-location-solid.svg new file mode 100644 index 0000000..eed2db9 --- /dev/null +++ b/scripts/icon-generator/icons/pin-location-solid.svg @@ -0,0 +1,21 @@ + + + +pin-location-1 + + + + + + diff --git a/scripts/icon-generator/icons/plus-solid.svg b/scripts/icon-generator/icons/plus-solid.svg new file mode 100644 index 0000000..b2c33d4 --- /dev/null +++ b/scripts/icon-generator/icons/plus-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/icon-generator/icons/rating-star-solid.svg b/scripts/icon-generator/icons/rating-star-solid.svg new file mode 100644 index 0000000..d87803c --- /dev/null +++ b/scripts/icon-generator/icons/rating-star-solid.svg @@ -0,0 +1,22 @@ + + + +rating-star + + + + diff --git a/scripts/icon-generator/icons/search.svg b/scripts/icon-generator/icons/search.svg new file mode 100644 index 0000000..db7501d --- /dev/null +++ b/scripts/icon-generator/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/scripts/icon-generator/icons/star-solid.svg b/scripts/icon-generator/icons/star-solid.svg new file mode 100644 index 0000000..c625fcd --- /dev/null +++ b/scripts/icon-generator/icons/star-solid.svg @@ -0,0 +1 @@ + diff --git a/scripts/icon-generator/icons/water-wifi-solid.svg b/scripts/icon-generator/icons/water-wifi-solid.svg new file mode 100644 index 0000000..57272ed --- /dev/null +++ b/scripts/icon-generator/icons/water-wifi-solid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/server/profilesController.js b/server/profilesController.js index 43a88c7..a53f2d6 100644 --- a/server/profilesController.js +++ b/server/profilesController.js @@ -1,131 +1,132 @@ - -var request = require('request'); -const shared = require('./../../../controllers/gc2/shared'); -var config = require('./../../../config/config'); -const uuid = require('uuid/v1'); - -if (!config.gc2.host) throw new Error(`Unable to get the GC2 host from config`); -const API_LOCATION = config.gc2.host + `/api/v2/keyvalue`; - -const createProfile = (req, res) => { - if (`profile` in req.body && `title` in req.body && `buffer` in req.body && `data` in req.body) { - let { userId } = shared.getCurrentUserIdentifiers(req); - if (userId) { - const key = `watsonc_profile_` + uuid(); - let currentDate = new Date(); - - let storedData = { - key, - userId, - created_at: currentDate.toISOString(), - profile: JSON.parse(JSON.stringify(req.body)) - }; - - request({ - method: 'POST', - encoding: 'utf8', - uri: API_LOCATION + `/` + req.params.dataBase + `/` + key, - form: JSON.stringify(storedData) - }, (error, response) => { - let parsedBody = false; - try { - let localParsedBody = JSON.parse(response.body); - parsedBody = localParsedBody; - } catch (e) {} - - if (parsedBody) { - if (parsedBody.success) { - res.send(parsedBody); - } else { - shared.throwError(res, parsedBody.message); - } - } else { - shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { body: response.body }); - } - }); - } else { - shared.throwError(res, 'UNAUTHORIZED'); - } - } else { - shared.throwError(res, 'MISSING_DATA'); - } -}; - -const getAllProfiles = (req, res) => { - let { userId } = shared.getCurrentUserIdentifiers(req); - - request({ - method: 'GET', - encoding: 'utf8', - uri: API_LOCATION + `/` + req.params.dataBase + `?like=watsonc_profile_%&filter='{userId}'='${userId}'` - }, (error, response) => { - if (error) { - shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { error }); - return; - } - - let parsedBody = false; - try { - let localParsedBody = JSON.parse(response.body); - parsedBody = localParsedBody; - } catch (e) {} - - if (parsedBody && parsedBody.data) { - // Filter by user ownership - let results = []; - parsedBody.data.map(item => { - let parsedSnapshot = JSON.parse(item.value); - if (userId && parsedSnapshot.userId === userId) { - results.push(item); - } - }); - - res.send(results); - } else { - shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { - body: response.body, - url: API_LOCATION + `/` + req.params.dataBase - }); - } - }); -}; - -const deleteProfile = (req, res) => { - let { userId } = shared.getCurrentUserIdentifiers(req); - - // Get the specified profile - request({ - method: 'GET', - encoding: 'utf8', - uri: API_LOCATION + `/` + req.params.dataBase + `/` + req.params.id, - }, (error, response) => { - if (response.body.data === false) { - shared.throwError(res, 'INVALID_PROFILE_ID'); - } else { - let parsedBody = false; - try { - let localParsedBody = JSON.parse(response.body); - parsedBody = localParsedBody; - } catch (e) {} - - if (parsedBody && parsedBody.data.value) { - let parsedSnapshotData = JSON.parse(parsedBody.data.value); - if (`userId` in parsedSnapshotData && parsedSnapshotData.userId === userId) { - request({ - method: 'DELETE', - encoding: 'utf8', - uri: API_LOCATION + `/` + req.params.dataBase + `/` + req.params.id, - }, (error, response) => { - res.send({ status: 'success' }); - }); - } else { - shared.throwError(res, 'ACCESS_DENIED'); - } - } else { - shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { body: response.body }); - } - } - }); -}; - -module.exports = {createProfile, getAllProfiles, deleteProfile}; \ No newline at end of file + +var request = require('request'); +const shared = require('./../../../controllers/gc2/shared'); +var config = require('./../../../config/config'); +const uuid = require('uuid/v1'); + +if (!config.gc2.host) throw new Error(`Unable to get the GC2 host from config`); +const API_LOCATION = config.gc2.host + `/api/v2/keyvalue`; + +const createProfile = (req, res) => { + if (`profile` in req.body && `title` in req.body && `buffer` in req.body && `data` in req.body) { + let { userId } = shared.getCurrentUserIdentifiers(req); + if (userId) { + // TODO flyt til klient før python script og send med + const key = `watsonc_profile_` + uuid(); + let currentDate = new Date(); + + let storedData = { + key, + userId, + created_at: currentDate.toISOString(), + profile: JSON.parse(JSON.stringify(req.body)) + }; + + request({ + method: 'POST', + encoding: 'utf8', + uri: API_LOCATION + `/` + req.params.dataBase + `/` + key, + form: JSON.stringify(storedData) + }, (error, response) => { + let parsedBody = false; + try { + let localParsedBody = JSON.parse(response.body); + parsedBody = localParsedBody; + } catch (e) {} + + if (parsedBody) { + if (parsedBody.success) { + res.send(parsedBody); + } else { + shared.throwError(res, parsedBody.message); + } + } else { + shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { body: response.body }); + } + }); + } else { + shared.throwError(res, 'UNAUTHORIZED'); + } + } else { + shared.throwError(res, 'MISSING_DATA'); + } +}; + +const getAllProfiles = (req, res) => { + let { userId } = shared.getCurrentUserIdentifiers(req); + + request({ + method: 'GET', + encoding: 'utf8', + uri: API_LOCATION + `/` + req.params.dataBase + `?like=watsonc_profile_%&filter='{userId}'='${userId}'` + }, (error, response) => { + if (error) { + shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { error }); + return; + } + + let parsedBody = false; + try { + let localParsedBody = JSON.parse(response.body); + parsedBody = localParsedBody; + } catch (e) {} + + if (parsedBody && parsedBody.data) { + // Filter by user ownership + let results = []; + parsedBody.data.map(item => { + let parsedSnapshot = JSON.parse(item.value); + if (userId && parsedSnapshot.userId === userId) { + results.push(item); + } + }); + + res.send(results); + } else { + shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { + body: response.body, + url: API_LOCATION + `/` + req.params.dataBase + }); + } + }); +}; + +const deleteProfile = (req, res) => { + let { userId } = shared.getCurrentUserIdentifiers(req); + + // Get the specified profile + request({ + method: 'GET', + encoding: 'utf8', + uri: API_LOCATION + `/` + req.params.dataBase + `/` + req.params.id, + }, (error, response) => { + if (response.body.data === false) { + shared.throwError(res, 'INVALID_PROFILE_ID'); + } else { + let parsedBody = false; + try { + let localParsedBody = JSON.parse(response.body); + parsedBody = localParsedBody; + } catch (e) {} + + if (parsedBody && parsedBody.data.value) { + let parsedSnapshotData = JSON.parse(parsedBody.data.value); + if (`userId` in parsedSnapshotData && parsedSnapshotData.userId === userId) { + request({ + method: 'DELETE', + encoding: 'utf8', + uri: API_LOCATION + `/` + req.params.dataBase + `/` + req.params.id, + }, (error, response) => { + res.send({ status: 'success' }); + }); + } else { + shared.throwError(res, 'ACCESS_DENIED'); + } + } else { + shared.throwError(res, 'INVALID_OR_EMPTY_EXTERNAL_API_REPLY', { body: response.body }); + } + } + }); +}; + +module.exports = {createProfile, getAllProfiles, deleteProfile}; diff --git a/shared/icons/icons.json b/shared/icons/icons.json new file mode 100644 index 0000000..d9a8611 --- /dev/null +++ b/shared/icons/icons.json @@ -0,0 +1 @@ +[{"name":"analytics-board-graph-line","path":"M2.5 4.75C2.08579 4.75 1.75 5.08579 1.75 5.5V19C1.75 19.4142 2.08579 19.75 2.5 19.75H22C22.4142 19.75 22.75 19.4142 22.75 19V5.5C22.75 5.08579 22.4142 4.75 22 4.75H2.5ZM0.25 5.5C0.25 4.25736 1.25736 3.25 2.5 3.25H22C23.2426 3.25 24.25 4.25736 24.25 5.5V19C24.25 20.2426 23.2426 21.25 22 21.25H2.5C1.25736 21.25 0.25 20.2426 0.25 19V5.5Z M20.916 7.87597C21.2607 8.10573 21.3538 8.57138 21.124 8.91603L16.624 15.666C16.4994 15.853 16.2975 15.9742 16.0739 15.9964C15.8503 16.0185 15.6285 15.9392 15.4697 15.7803L13 13.3107L10.5303 15.7803C10.363 15.9477 10.1265 16.0262 9.89234 15.9922C9.65816 15.9583 9.45375 15.8158 9.34085 15.6078L7.11742 11.512L4.64311 15.6359C4.43 15.9911 3.9693 16.1062 3.61412 15.8931C3.25893 15.68 3.14376 15.2193 3.35687 14.8641L6.50687 9.61413C6.64538 9.38328 6.89699 9.24438 7.16614 9.25018C7.4353 9.25598 7.68069 9.40559 7.80913 9.64218L10.1799 14.0094L12.4697 11.7197C12.7626 11.4268 13.2374 11.4268 13.5303 11.7197L15.8834 14.0728L19.876 8.08398C20.1057 7.73933 20.5714 7.6462 20.916 7.87597Z","viewbox":"0 0 24 24"},{"name":"arrow-down","path":"M8 14.08L11.75 17.83L15.5 14.08M11.75 7V16.25","viewbox":"0 0 24 24"},{"name":"avatar","path":"M12 15C16 15 17.25 9.8995 17.25 7C17.25 4.10051 14.8995 1.75 12 1.75C9.10051 1.75 6.75 4.10051 6.75 7C6.75 9.8995 8 15 12 15Z M2 22C2 20.1435 3.12464 18.3128 5 17C6.87536 15.6873 9.34784 16 12 16C14.6522 16 17.1246 15.6873 19 17C20.8754 18.3128 22 20.1435 22 22","viewbox":"0 0 24 24"},{"name":"check-mark-solid","path":"M337.222,22.952c-15.912-8.568-33.66,7.956-44.064,17.748c-23.867,23.256-44.063,50.184-66.708,74.664 c-25.092,26.928-48.348,53.856-74.052,80.173c-14.688,14.688-30.6,30.6-40.392,48.96c-22.032-21.421-41.004-44.677-65.484-63.648 c-17.748-13.464-47.124-23.256-46.512,9.18c1.224,42.229,38.556,87.517,66.096,116.28c11.628,12.24,26.928,25.092,44.676,25.704 c21.42,1.224,43.452-24.48,56.304-38.556c22.645-24.48,41.005-52.021,61.812-77.112c26.928-33.048,54.468-65.485,80.784-99.145 C326.206,96.392,378.226,44.983,337.222,22.952z M26.937,187.581c-0.612,0-1.224,0-2.448,0.611 c-2.448-0.611-4.284-1.224-6.732-2.448l0,0C19.593,184.52,22.653,185.132,26.937,187.581z","viewbox":"0 0 352.62 352.62"},{"name":"cleaning-spray","path":"M20.625 2.25L22.5 1.125 M20.625 7.5L22.5 8.625 M20.625 4.875h2.25 M14.625 1.875h-6.75a4.5 4.5 0 0 0-4.5 4.5h3.75v4.5h3v-4.5h4.5z M10.125 6.375s2.25 3.75 5.25 3.75 M11.625 13.875v-1.5a1.5 1.5 0 0 0-1.5-1.5h-3a1.5 1.5 0 0 0-1.5 1.5v1.5c-2.25 0-4.5 1.5-4.5 6a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3c0-4.5-2.25-6-4.5-6z M17.625 3.375v2.25","viewbox":"0 0 24 24"},{"name":"cross","path":"M10.505 11.889L8 14.394l1.414 1.414 2.505-2.505 2.475 2.475 1.414-1.414-2.475-2.475 2.445-2.445-1.414-1.414-2.445 2.445L9.444 8 8.03 9.414l2.475 2.475z","viewbox":"0 0 24 24"},{"name":"dashboard-full-solid","path":"M2.25 20.5a2.252 2.252 0 0 1-2.247-2.25v-12A2.252 2.252 0 0 1 2.25 4h19.5a2.252 2.252 0 0 1 2.253 2.25v12a2.252 2.252 0 0 1-2.253 2.25H2.25zm0-15a.75.75 0 0 0-.75.75v4c.019.027 20.97 0 21 0v-4a.75.75 0 0 0-.75-.75H2.25z","viewbox":"0 0 24 24"},{"name":"dashboard-half-solid","path":"M2.25 20.5a2.252 2.252 0 0 1-2.247-2.25v-12A2.252 2.252 0 0 1 2.25 4h19.5a2.252 2.252 0 0 1 2.253 2.25v12a2.252 2.252 0 0 1-2.253 2.25H2.25zm0-15a.75.75 0 0 0-.75.75v8c.019.027 20.97 0 21 0v-8a.75.75 0 0 0-.75-.75H2.25z","viewbox":"0 0 24 24"},{"name":"dashboard-minimized-solid","path":"M2.25 20.5a2.252 2.252 0 0 1-2.247-2.25v-12A2.252 2.252 0 0 1 2.25 4h19.5a2.252 2.252 0 0 1 2.253 2.25v12a2.252 2.252 0 0 1-2.253 2.25H2.25zm0-15a.75.75 0 0 0-.75.75v11c.019.027 20.97 0 21 0v-11a.75.75 0 0 0-.75-.75H2.25z","viewbox":"0 0 24 24"},{"name":"dashboard","path":"M7 1.9H3C2.44772 1.9 2 2.30294 2 2.8V8.2C2 8.69706 2.44772 9.1 3 9.1H7C7.55228 9.1 8 8.69706 8 8.2V2.8C8 2.30294 7.55228 1.9 7 1.9ZM3 1C1.89543 1 1 1.80589 1 2.8V8.2C1 9.19411 1.89543 10 3 10H7C8.10457 10 9 9.19411 9 8.2V2.8C9 1.80589 8.10457 1 7 1H3Z M21.1667 1.9H13.8333C13.3271 1.9 12.9167 2.30294 12.9167 2.8V8.2C12.9167 8.69706 13.3271 9.1 13.8333 9.1H21.1667C21.6729 9.1 22.0833 8.69706 22.0833 8.2V2.8C22.0833 2.30294 21.6729 1.9 21.1667 1.9ZM13.8333 1C12.8208 1 12 1.80589 12 2.8V8.2C12 9.19411 12.8208 10 13.8333 10H21.1667C22.1792 10 23 9.19411 23 8.2V2.8C23 1.80589 22.1792 1 21.1667 1H13.8333Z M21 14H17C16.4477 14 16 14.4477 16 15V21C16 21.5523 16.4477 22 17 22H21C21.5523 22 22 21.5523 22 21V15C22 14.4477 21.5523 14 21 14ZM17 13C15.8954 13 15 13.8954 15 15V21C15 22.1046 15.8954 23 17 23H21C22.1046 23 23 22.1046 23 21V15C23 13.8954 22.1046 13 21 13H17Z M10.1667 14H2.83333C2.32707 14 1.91667 14.4477 1.91667 15V21C1.91667 21.5523 2.32707 22 2.83333 22H10.1667C10.6729 22 11.0833 21.5523 11.0833 21V15C11.0833 14.4477 10.6729 14 10.1667 14ZM2.83333 13C1.82081 13 1 13.8954 1 15V21C1 22.1046 1.82081 23 2.83333 23H10.1667C11.1792 23 12 22.1046 12 21V15C12 13.8954 11.1792 13 10.1667 13H2.83333Z","viewbox":"0 0 24 24"},{"name":"drag-handle-solid","path":"M9.5 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.5 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z","viewbox":"0 0 24 24"},{"name":"drag-handle","path":"M9.5 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.5 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM14.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z","viewbox":"0 0 24 24"},{"name":"drill-space-solid","path":"M12 21a9 9 0 1 1 9-9 8.942 8.942 0 0 1-9 9zm1.285-6.858a1.715 1.715 0 1 0-.002 3.428 1.715 1.715 0 0 0 .003-3.427l-.001-.001zM7.714 9a2.143 2.143 0 1 0 0 4.286 2.143 2.143 0 0 0 0-4.286zm7.715-2.571a3 3 0 1 0 0 6 3 3 0 0 0 0-6z","viewbox":"0 0 24 24"},{"name":"drill","path":"M12.5 21a.975.975 0 0 1-.563-.313 5.416 5.416 0 0 1-.687-.787 8.247 8.247 0 0 1-1.12-2.045L15 15.833V17.3c-.055.374-.17.737-.339 1.075-.197.423-.428.83-.69 1.217a6.629 6.629 0 0 1-.8 1A1.16 1.16 0 0 1 12.5 21zM10 16.776v-1.5l5-2.076v1.5l-5 2.076zm0-2.635v-1.32l5-2.077v1.322l-5 2.075zm0-2.453V4h5v5.613l-5 2.075z","viewbox":"4 4 18 18"},{"name":"earth-layers","path":"M1 8.90799V3.15399C1 3.15399 3.17034 3.18145 5 3.15399C6.99214 3.1241 8.524 3.45399 9.5 3.40499C10.476 3.35599 13 3.40501 13 3.40501C13 3.40501 14.8137 2.88847 16.072 2.99999C18.0792 3.17789 20.5 3.40499 20.5 3.40499L22.753 2.99999L22.755 6.55099C22.755 6.55099 18.8671 5.83197 16.406 6.16799C14.1484 6.47622 10.859 6.97699 10.859 6.97699C10.859 6.97699 8.51808 7.13828 7.559 6.86099C4.62103 6.01157 1 8.90799 1 8.90799Z M1 14.558V10.011C1 10.011 5.61844 7.19305 8.555 8.02098C9.51087 8.29048 10.8857 7.88969 11.855 8.10598C14.074 8.60114 14.1513 7.64784 16.405 7.34798C18.867 7.02041 22.754 7.71998 22.754 7.71998V11.548C22.754 11.548 20.0597 11.5932 18.373 11.404C15.1607 11.0436 13.331 14.326 10.907 13.259C8.483 12.192 8.30334 12.4012 6.826 12.404C4.40029 12.4085 1 14.558 1 14.558Z M22.754 21H1V16.3C1 16.3 4.42796 14.2458 6.82 14.366C8.49193 14.45 9.23987 15.507 10.907 15.659C13.9569 15.937 15.3123 13.1508 18.373 13.259C20.1292 13.3211 22.753 14.291 22.753 14.291V20.999L22.754 21Z","viewbox":"0 0 24 24"},{"name":"folder-solid","path":"M21.003 8.273a1.637 1.637 0 0 0-1.637-1.637H9.55L8.39 5.479A1.637 1.637 0 0 0 7.232 5h-2.6A1.637 1.637 0 0 0 3 6.637v11.456a1.637 1.637 0 0 0 1.637 1.637h14.73a1.637 1.637 0 0 0 1.637-1.637l-.001-9.82z","viewbox":"0 0 24 24"},{"name":"hyperlink","path":"M6.75 17.249l10.5-10.5M7.735 12.021a4.472 4.472 0 0 0-3.417 1.3l-2.25 2.25a4.5 4.5 0 0 0 6.364 6.364l2.25-2.25a4.472 4.472 0 0 0 1.3-3.417M16.265 11.976a4.473 4.473 0 0 0 3.417-1.3l2.25-2.25a4.5 4.5 0 0 0-6.364-6.364l-2.25 2.25a4.475 4.475 0 0 0-1.295 3.417","viewbox":"0 0 24 24"},{"name":"lab-flask-experiment","path":"M6.726.75h10.5 M15.726 8.25V.75h-7.5v7.5L1.489 18.615A3 3 0 0 0 4 23.25h15.948a3 3 0 0 0 2.515-4.635z M5.301 12.75h13.35 M14.226 17.25h3 M15.726 15.75v3 M6.726 19.125a.375.375 0 0 1 .374.375 M6.351 19.5a.375.375 0 0 1 .375-.375 M6.726 19.875a.375.375 0 0 1-.375-.375 M7.1 19.5a.375.375 0 0 1-.375.375 M9.726 16.125a.375.375 0 0 1 .375.375 M9.351 16.5a.375.375 0 0 1 .375-.375 M9.726 16.875a.375.375 0 0 1-.375-.375 M10.1 16.5a.375.375 0 0 1-.375.375 M15.726 3.75h-3 M15.726 6.75h-3","viewbox":"0 0 24 24"},{"name":"logo","path":"M274.9,17.3c5.9,1.2,11,5.5,13.2,11c1.3,3.2,1.8,6.9,4.3,9.2c2,1.8,4.9,2.3,7.5,1.6c2.6-0.7,4.8-2.5,6.3-4.7 c3.6-5.2,3.2-12.5,0-17.9c-3.2-5.4-9-9-15.2-10.6c-10.1-2.5-21.5,0.5-28.6,8.2c-1.4,1.5-2.6,3.2-3.6,5c-0.6,1,0.6,2.2,1.6,1.6 C264.5,18.4,270.5,16.4,274.9,17.3 M313.6,54.9c-4.3-5.9-11.3-5.4-17.1-3.4c-4.4,1.5-8.5,3.8-13,4.7c-10.2,2.1-21.3-3.8-25.8-13.2 c-2.3-4.8-2.9-10.4-1.9-15.6c0.2-1.2-1.3-1.9-2.1-1c-6.7,8.3-9.9,19.4-8.2,30c2.2,13.3,12.1,25.1,24.8,29.5 c12.7,4.4,27.8,1.3,37.7-7.8c4.7-4.3,8.4-10.2,7.9-16.9C315.7,59,314.9,56.8,313.6,54.9 M12.4,55.7c0,6.6,2.7,11.6,9.5,11.6c3.2,0,6.5-1.5,8.8-4.5c0.2-0.4,0.7-0.6,1.2-0.6c0.2,0,0.5,0.1,0.7,0.2 l3.1,1.9c0.4,0.3,0.6,0.7,0.6,1.2c0,0.3-0.1,0.6-0.2,0.8c-2.7,4.7-7.7,7.5-14.2,7.5c-10.5,0-16.6-7.5-16.6-18 c0-10.6,6.1-18.5,16.6-18.5c6.5,0,11.6,2.9,14.3,7.5c0.2,0.2,0.2,0.5,0.2,0.8c0,0.5-0.2,0.9-0.6,1.2l-3,1.9 c-0.2,0.1-0.5,0.2-0.7,0.2c-0.6,0-1-0.2-1.2-0.6c-2.2-3-5.5-4.5-8.8-4.5C15.1,43.6,12.4,49.3,12.4,55.7z M57.5,37.8c0.7,0,1.4,0.6,1.7,1.4l9.5,32.5c0,0.8-0.7,1.4-1.4,1.4h-4.2c-0.7,0-1.4-0.6-1.6-1.4l-1-4.1H46.9 l-1.1,4.1c-0.2,0.7-0.9,1.4-1.6,1.4H40c-0.7,0-1.4-0.6-1.4-1.4l9.5-32.5c0.2-0.8,0.9-1.4,1.7-1.4H57.5z M58.4,61.3l-4.7-16.5 l-4.8,16.5H58.4z M74.6,39.2c0-0.4,0.2-0.8,0.5-1.1c0.2-0.2,0.6-0.4,0.9-0.4h4.3c0.4,0,0.8,0.1,1.1,0.4c0.2,0.2,0.4,0.6,0.4,1 v27.2h14.4c0.4,0,0.8,0.2,1.1,0.5c0.2,0.2,0.3,0.6,0.3,0.9v4c0,0.4-0.1,0.7-0.4,0.9c-0.2,0.2-0.6,0.4-1,0.4H76 c-0.4,0-0.8-0.2-1.1-0.4c-0.2-0.2-0.4-0.6-0.4-1V39.2z M110.6,53l6.8-13.8c0.4-0.7,1.1-1.4,1.8-1.4h4.7c0.7,0,1.4,0.6,1.4,1.4L114.1,62v9.8c0,0.8-0.7,1.4-1.4,1.4 h-4.3c-0.7,0-1.4-0.6-1.4-1.4V62L95.7,39.2c0-0.8,0.7-1.4,1.4-1.4h4.7c0.7,0,1.4,0.7,1.8,1.4L110.6,53z M141.1,37.8c10.5,0,15.2,6.2,15.2,12.9v0.5c0,6.7-4.7,13.1-15.2,13.1h-4.9v7.3c0,0.8-0.7,1.4-1.4,1.4h-4.3 c-0.7,0-1.4-0.6-1.4-1.4V39.2c0-0.8,0.7-1.4,1.4-1.4H141.1z M136.3,44.1v13.7h4.9c7,0,8-3.3,8-6.6v-0.5c0-3.4-1.1-6.7-8-6.7H136.3 z M184.5,47.6h-4c-0.7,0-1.6-0.7-1.9-1.5c-1.2-2.4-3.8-2.6-7-2.6c-3.7,0-5.7,1.4-5.7,3.7 c0,5.5,21.2,4.5,21.2,15.6c0,8.5-6,11.1-13.9,11.1c-7.5,0-13.5-2.9-14.2-8.9c0-0.8,0.6-1.7,1.6-1.7h4c0.7,0,1.6,0.6,1.9,1.4 c1,2.3,4.2,2.9,6.8,2.9c3.2,0,6.8,0,6.8-4.3c0-6.1-21.2-4-21.2-15.9c0-6.2,4.9-10.1,12.8-10.1c7.7,0,13.7,2.8,14.5,8.7 C186.1,46.7,185.5,47.6,184.5,47.6z M190.2,55.7c0-10.6,6.1-18.5,16.5-18.5c10.5,0,16.5,7.9,16.5,18.5c0,10.6-6.1,18-16.5,18 C196.3,73.8,190.2,66.3,190.2,55.7z M216.1,55.7c0-6.5-2.5-12.3-9.4-12.3c-6.8,0-9.4,5.7-9.4,12.3c0,6.7,2.6,11.8,9.4,11.8 C213.6,67.5,216.1,62.4,216.1,55.7z","viewbox":null},{"name":"minus","path":"M0 0h24v24H0z M10.925 10.904H7.383v2h9v-2h-5.458z","viewbox":"0 0 24 24"},{"name":"no3-solid","path":"M13.289 14.494H9.705L3.584 4.83v9.664H.174V.416H4.62L9.88 8.48V.416h3.409zM31.015 7.46q0 3.365-1.976 5.35-1.976 1.977-5.463 1.977-3.477 0-5.453-1.976-1.977-1.986-1.977-5.352 0-3.394 1.977-5.36Q20.099.121 23.576.121q3.467 0 5.454 1.976 1.985 1.967 1.985 5.361zm-4.93 3.564q.542-.643.804-1.512.26-.88.26-2.062 0-1.267-.299-2.156-.3-.889-.785-1.437-.494-.567-1.143-.823-.64-.255-1.336-.255-.708 0-1.337.246-.62.246-1.143.813-.485.53-.794 1.466-.301.926-.301 2.155 0 1.258.291 2.147.3.879.784 1.436.485.558 1.134.823.649.265 1.366.265.717 0 1.366-.265.649-.274 1.133-.841z M38.828 17.099q.277.23.44.54.165.309.165.82 0 .58-.238 1.082-.231.502-.718.86-.474.348-1.117.535-.64.183-1.554.183-1.046 0-1.797-.16-.746-.16-1.215-.36v-1.802h.22q.487.286 1.158.496.679.21 1.238.21.328 0 .712-.05.384-.056.65-.23.208-.14.332-.333.125-.198.125-.568 0-.358-.17-.551-.169-.198-.446-.281-.277-.088-.667-.094-.389-.01-.722-.01h-.463v-1.468h.48q.44 0 .779-.027.34-.028.576-.128.249-.104.373-.275.124-.176.124-.513 0-.249-.13-.396-.13-.155-.327-.244-.22-.1-.52-.132-.3-.033-.514-.033-.531 0-1.153.182-.622.176-1.203.512h-.208v-1.781q.463-.182 1.259-.347.796-.171 1.615-.171.798 0 1.397.138.598.132.988.359.462.27.69.655.226.387.226.905 0 .684-.436 1.224-.434.535-1.146.685v.076q.288.039.61.155.321.115.587.337z","viewbox":"0 0 41 24"},{"name":"pin-location-solid","path":"M12,11.25c-2.068,0-3.75-1.682-3.75-3.75S9.932,3.75,12,3.75c2.068,0,3.75,1.682,3.75,3.75S14.068,11.25,12,11.25z M12,5.25c-1.241,0-2.25,1.009-2.25,2.25S10.759,9.75,12,9.75c1.241,0,2.25-1.009,2.25-2.25S13.241,5.25,12,5.25z M11.998,20.741c-0.24,0-0.47-0.076-0.665-0.219c-0.094-0.069-0.175-0.151-0.243-0.243C5.142,12.087,4.5,8.613,4.5,7.5 C4.5,3.365,7.865,0,12,0s7.5,3.365,7.5,7.5c0,3.435-5.043,10.648-6.589,12.777c-0.179,0.245-0.442,0.405-0.74,0.45 C12.114,20.737,12.056,20.741,11.998,20.741z M12,1.5c-3.308,0-6,2.692-6,6c0,2.291,3.164,7.514,6,11.475 c2.835-3.961,6-9.184,6-11.475C18,4.192,15.308,1.5,12,1.5z M12,24c-2.756,0-5.495-0.32-7.514-0.879C2.042,22.445,0.75,21.452,0.75,20.25c0-1.803,2.775-2.734,5.103-3.198 c0.046-0.01,0.098-0.015,0.149-0.015c0.355,0,0.663,0.254,0.733,0.604c0.039,0.197,0,0.397-0.112,0.563 c-0.111,0.167-0.281,0.28-0.477,0.319C3.231,19.104,2.25,19.952,2.25,20.25c0,0.176,0.465,0.81,2.531,1.4 C6.699,22.198,9.262,22.5,12,22.5s5.301-0.302,7.219-0.85c2.066-0.59,2.531-1.224,2.531-1.4c0-0.299-0.986-1.15-3.917-1.73 c-0.197-0.039-0.366-0.152-0.478-0.319s-0.151-0.366-0.113-0.563c0.069-0.35,0.379-0.604,0.736-0.604 c0.048,0,0.097,0.005,0.146,0.015c2.338,0.463,5.125,1.394,5.125,3.202c0,1.202-1.292,2.195-3.736,2.871 C17.495,23.68,14.756,24,12,24z","viewbox":null},{"name":"plus-solid","path":"M0 0h24v24H0z M10.925 10.904H7.383v2h3.542v3.5h2v-3.5h3.458v-2h-3.458v-3.5h-2v3.5z","viewbox":"0 0 24 24"},{"name":"rating-star-solid","path":"M18.894,24.004c-0.242,0-0.485-0.058-0.702-0.167L12,20.771l-6.187,3.064C5.596,23.944,5.361,24,5.117,24 c-0.595,0-1.131-0.33-1.398-0.861c-0.172-0.342-0.212-0.734-0.114-1.103l1.85-6.721L0.462,10.37 c-0.295-0.293-0.459-0.682-0.461-1.098C0,8.857,0.16,8.466,0.452,8.17c0.26-0.263,0.607-0.424,0.976-0.455l6.018-0.596l3.156-6.257 c0.193-0.377,0.518-0.654,0.917-0.782c0.157-0.051,0.32-0.076,0.483-0.076c0.246,0,0.493,0.06,0.714,0.173 c0.295,0.151,0.531,0.387,0.681,0.682c0.001,0.003,3.162,6.265,3.162,6.265l6.03,0.598c0.845,0.07,1.48,0.823,1.409,1.677 c-0.031,0.369-0.192,0.715-0.455,0.976l-4.989,4.944l1.849,6.715c0.222,0.838-0.275,1.698-1.108,1.919 C19.165,23.986,19.03,24.004,18.894,24.004z M12,19.184c0.115,0,0.231,0.027,0.334,0.079l6.528,3.232 c0.012,0.006,0.022,0.009,0.032,0.009c0.05-0.011,0.07-0.046,0.061-0.079l-1.963-7.131c-0.072-0.261,0.003-0.541,0.195-0.732 l5.3-5.253c0.009-0.009,0.015-0.021,0.016-0.034c0.002-0.03-0.021-0.057-0.05-0.06l-6.452-0.639 c-0.257-0.026-0.479-0.178-0.596-0.408L12.06,1.539c-0.006-0.011-0.016-0.021-0.029-0.028c-0.009-0.005-0.02-0.007-0.031-0.007 c-0.04,0.009-0.053,0.02-0.062,0.037L8.6,8.163c-0.116,0.23-0.339,0.383-0.596,0.409l-6.44,0.638 C1.532,9.212,1.521,9.223,1.518,9.226C1.507,9.237,1.502,9.25,1.502,9.265c0,0.015,0.005,0.028,0.016,0.038l5.304,5.253 c0.192,0.19,0.267,0.47,0.195,0.732l-1.965,7.138c-0.002,0.006-0.002,0.022,0.006,0.039c0.013,0.025,0.039,0.034,0.057,0.034 c0.01,0,0.019-0.002,0.027-0.006l6.524-3.231C11.769,19.211,11.885,19.184,12,19.184z","viewbox":null},{"name":"search","path":"M19.25 18.5C19.25 18.9142 18.9142 19.25 18.5 19.25C18.0858 19.25 17.75 18.9142 17.75 18.5C17.75 18.0858 18.0858 17.75 18.5 17.75C18.9142 17.75 19.25 18.0858 19.25 18.5ZM18.25 11C18.25 14.4518 15.4518 17.25 12 17.25C8.54822 17.25 5.75 14.4518 5.75 11C5.75 7.54822 8.54822 4.75 12 4.75C15.4518 4.75 18.25 7.54822 18.25 11Z","viewbox":"0 0 24 24"},{"name":"star-solid","path":"M12.784 4.326l2.454 4.861 4.724.468a.59.59 0 0 1 .366 1.008l-3.886 3.852 1.44 5.235a.6.6 0 0 1-.844.686l-4.789-2.37-4.78 2.367a.6.6 0 0 1-.844-.686l1.44-5.235-3.89-3.852a.59.59 0 0 1 .368-1.008l4.723-.467 2.45-4.858a.6.6 0 0 1 1.069 0z","viewbox":"0 0 24 24"},{"name":"water-wifi-solid","path":"M12.59 21.3269C13.7533 20.1614 14.4069 18.5807 14.4069 16.9325C14.4069 13.5006 8.20345 7.61043 8.20345 7.61043C8.20345 7.61043 2 13.5006 2 16.9325C2 18.5807 2.65358 20.1614 3.81695 21.3269C4.98032 22.4924 6.55819 23.1471 8.20345 23.1471C9.84871 23.1471 11.4266 22.4924 12.59 21.3269ZM6.94724 16.9891C6.94724 16.5545 6.59497 16.2023 6.16043 16.2023C5.72589 16.2023 5.37363 16.5545 5.37363 16.9891C5.37363 17.7467 5.67404 18.4736 6.20923 19.0097C6.74446 19.5459 7.4707 19.8474 8.22825 19.8474C8.66279 19.8474 9.01506 19.4952 9.01506 19.0606C9.01506 18.6261 8.66279 18.2738 8.22825 18.2738C7.88897 18.2738 7.56329 18.1388 7.32294 17.898C7.08255 17.6572 6.94724 17.3303 6.94724 16.9891Z M14.045 7.97482C13.1641 7.09238 11.9695 6.59656 10.7238 6.59639C10.2897 6.59633 9.93775 6.24371 9.93781 5.80877C9.93787 5.37384 10.2899 5.0213 10.724 5.02136C12.3866 5.02158 13.9811 5.68334 15.1567 6.86111C16.3323 8.03888 16.9929 9.63621 16.9931 11.3018C16.9932 11.7368 16.6413 12.0894 16.2071 12.0894C15.773 12.0895 15.421 11.737 15.4209 11.302C15.4208 10.0541 14.9258 8.85727 14.045 7.97482Z M17.3238 4.68905C15.826 3.18851 13.7945 2.3455 11.6763 2.34549C11.2421 2.34549 10.8902 1.9929 10.8902 1.55797C10.8902 1.12304 11.2421 0.770458 11.6763 0.770461C14.2115 0.770476 16.6429 1.77942 18.4355 3.57534C20.2282 5.37126 21.2353 7.80704 21.2354 10.3469C21.2354 10.7818 20.8834 11.1344 20.4493 11.1344C20.0151 11.1344 19.6632 10.7818 19.6632 10.3469C19.6632 8.22477 18.8217 6.1896 17.3238 4.68905Z","viewbox":"0 0 24 24"},{"name":"list-station","path":"M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zm-6 400H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h404a6 6 0 0 1 6 6v340a6 6 0 0 1-6 6zm-42-92v24c0 6.627-5.373 12-12 12H204c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h200c6.627 0 12 5.373 12 12zm0-96v24c0 6.627-5.373 12-12 12H204c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h200c6.627 0 12 5.373 12 12zm0-96v24c0 6.627-5.373 12-12 12H204c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h200c6.627 0 12 5.373 12 12zm-252 12c0 19.882-16.118 36-36 36s-36-16.118-36-36 16.118-36 36-36 36 16.118 36 36zm0 96c0 19.882-16.118 36-36 36s-36-16.118-36-36 16.118-36 36-36 36 16.118 36 36zm0 96c0 19.882-16.118 36-36 36s-36-16.118-36-36 16.118-36 36-36 36 16.118 36 36z","viewbox":"0 0 24 24"}, {"name":"full-screen","path":"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z","viewbox":"-4 -4 24 24"}] \ No newline at end of file diff --git a/shared/icons/icons.ts b/shared/icons/icons.ts new file mode 100644 index 0000000..cb93a81 --- /dev/null +++ b/shared/icons/icons.ts @@ -0,0 +1,32 @@ +export const IconName = { + ANALYTICS_BOARD_GRAPH_LINE: + "analytics-board-graph-line" as "analytics-board-graph-line", + ARROW_DOWN: "arrow-down" as "arrow-down", + AVATAR: "avatar" as "avatar", + CHECK_MARK_SOLID: "check-mark-solid" as "check-mark-solid", + CLEANING_SPRAY: "cleaning-spray" as "cleaning-spray", + CROSS: "cross" as "cross", + DASHBOARD_FULL_SOLID: "dashboard-full-solid" as "dashboard-full-solid", + DASHBOARD_HALF_SOLID: "dashboard-half-solid" as "dashboard-half-solid", + DASHBOARD_MINIMIZED_SOLID: + "dashboard-minimized-solid" as "dashboard-minimized-solid", + DASHBOARD: "dashboard" as "dashboard", + DRAG_HANDLE_SOLID: "drag-handle-solid" as "drag-handle-solid", + DRAG_HANDLE: "drag-handle" as "drag-handle", + DRILL_SPACE_SOLID: "drill-space-solid" as "drill-space-solid", + DRILL: "drill" as "drill", + EARTH_LAYERS: "earth-layers" as "earth-layers", + FOLDER_SOLID: "folder-solid" as "folder-solid", + HYPERLINK: "hyperlink" as "hyperlink", + LAB_FLASK_EXPERIMENT: "lab-flask-experiment" as "lab-flask-experiment", + LOGO: "logo" as "logo", + MINUS: "minus" as "minus", + NO3_SOLID: "no3-solid" as "no3-solid", + PIN_LOCATION_SOLID: "pin-location-solid" as "pin-location-solid", + PLUS_SOLID: "plus-solid" as "plus-solid", + RATING_STAR_SOLID: "rating-star-solid" as "rating-star-solid", + SEARCH: "search" as "search", + STAR_SOLID: "star-solid" as "star-solid", + WATER_WIFI_SOLID: "water-wifi-solid" as "water-wifi-solid", +}; +export type IconName = typeof IconName[keyof typeof IconName]; diff --git a/templates/watsonc.tmpl b/templates/watsonc.tmpl index 6b235cc..dc29ff4 100644 --- a/templates/watsonc.tmpl +++ b/templates/watsonc.tmpl @@ -2,10 +2,42 @@ #side-panel { height: calc(100vh - 100px); + background-color: #001e1b; + } + + #side-panel li a { + color: #fff; + } + #side-panel ul li svg { + margin-right: 17px; + padding: 2px; } .js-browser-owned { display: none; } + + .js-user-owned .panel { + background-color: #003C36 !important; + } + + .js-user-owned > div > h4 > div > input, + .js-user-owned > div > h4, + .panel .material-icons, + .form-control + { + color: #ffffff !important; + } + .leaflet-popup-content .panel-heading + { + background-color: #003C36; + } + + .panel > .panel-heading, .panel.panel-default > .panel-heading { + background-color: #003C36; + } + .panel { + background-color: transparent; + } .rc-slider { position: relative; height: 14px; @@ -286,7 +318,17 @@ } #myNavmenu .navbar-default { - background-color: white; + background-color: #001e1b; + } + + #myNavmenu { + right: -500px; + transition: right 0.5s linear; + } + + #myNavmenu:hover { + right: -300px; + transition: right 0.5s linear; } .navbar-header .navbar-toggle { @@ -355,12 +397,95 @@ .snapshot-copy-token, .snapshot-copy-png-link { display: none; } + + .leaflet-popup-content { + margin: 0; + width: 362px !important; + overflow-x: hidden; + max-height: 500px; + } + + .leaflet-popup-content-wrapper { + background: transparent; + box-shadow: none; + } + + .leaflet-popup-close-button { + display: none; + } + + .custom-popup .leaflet-popup-content-wrapper { + background: transparent !important; + } + + .custom-popup .leaflet-popup-content-wrapper a { + color: white !important; + } + + .custom-popup .leaflet-popup-content { + overflow-x: hidden !important; + max-height: 500px; + } + + #upgrade-modal { + width: 70%; + top: 10%; + left: 15%; + } + + ::-ms-reveal { + filter: invert(100%); + } + + #watsonc-login-modal { + width: 900px; + position: absolute; + left: 50%; + top: 10%; + margin-left: -450px; + } + + #module-container { + background: #001e1b; + color: #fff; + } + + #module-container .modal-header .close { + color: #fff; + } + + #module-container .list-group-item { + /*color: #000;*/ + } + + #module-container .tab-pane a { + color: #fff; + } + + .module-title { + vertical-align: super; + } + #main-tabs li a { + text-decoration: none !important; + } + .checkbox .checkbox-material .check, label.checkbox-inline .checkbox-material .check { + border: 1px solid white; + } + .checkbox input[type=checkbox]:checked + .checkbox-material .check, label.checkbox-inline input[type=checkbox]:checked + .checkbox-material .check { + border: 1px solid white; + } + .checkbox input[type=checkbox]:checked + .checkbox-material .check:before, label.checkbox-inline input[type=checkbox]:checked + .checkbox-material .check:before { + color: white; + } + .leaflet-top { + display: none; + }
-
-
+
+ +