diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d4dce094f..4b4d06884 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,8 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at leadership@OpenEnergyDashboard.org. All +reported by contacting the project team via the [Contact page](https://openenergydashboard.org/contact/), +using the contact form and selecting the leadership topic/area. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. @@ -63,10 +64,10 @@ Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. If your concern revolves around a project -maintainer or you have concerns contacting us via the leadership email then you -can directly contact the main project coordinator at -StevenHussLederman@OpenEnergyDashboard.org. If the issue involves the main project +members of the project's leadership. If your concern revolves around a project +maintainer or you have concerns contacting us via the contact form then you +can directly contact the main project coordinator via Discord (@SteveOED). +If the issue involves the main project coordinator then please contact other project leadership or other project members. ## Attribution diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51b81b51d..b452c585a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,15 +4,15 @@ We're thrilled that you're interested in contributing to the OED project. We welcome all open source contributions, whether bug reports, patches, or anything else. -Check out the [developer getting started](https://openenergydashboard.github.io/developer/gettingStarted.html) page to learn how you can set up OED on your +Check out the [developer first steps](https://openenergydashboard.org/developer/gettingStarted/) page to learn how you can set up OED on your own system. ## Legal Stuff -Please sign our [Contributor License Agreement](https://openenergydashboard.github.io/developer/cla.html) before +Please sign our [Contributor License Agreement](https://openenergydashboard.org/developer/cla/) before contributing code to OED. It is not necessary to sign the CLA in order to contribute by filing or discussing issues. ## Working as a Developer on OED -Web pages with [information for developers](https://openenergydashboard.github.io/developer/) is available to explain how to develop and contribute code to OED. +Web pages with [information for developers](https://openenergydashboard.org/developer/developer/) is available to explain how to develop and contribute code to OED. diff --git a/README.md b/README.md index 19d421e2e..31e7e57ce 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Github Build](https://github.com/OpenEnergyDashboard/OED/workflows/Build/badge.svg) -Open Energy Dashboard is a user-friendly way to display energy information from smart energy meters or uploaded via CSV files. It is available to anyone and is optimized for non-technical users with a simple interface that provides access to your organization's energy data. To learn more, see [our website](https://openenergydashboard.github.io/). +Open Energy Dashboard is a user-friendly way to display energy information from smart energy meters or uploaded via CSV files. It is available to anyone and is optimized for non-technical users with a simple interface that provides access to your organization's energy data. To learn more, see [our website](https://openenergydashboard.org/). Open Energy Dashboard is available under the Mozilla Public License v2, and contributions, in the form of bug reports, feature requests, and code contributions, are welcome. @@ -30,11 +30,11 @@ For a list of contributors, [click here](https://github.com/OpenEnergyDashboard/ This project is licensed under the MPL version 2.0. -See the full licensing agreement [here](LICENSE.txt) +See the full licensing agreement [here](License.txt) ## Contributions ## -We welcome others to contribute to this project by writing code for submission or collaborating with us. Before contributing, please sign our [Contributor License Agreement](https://openenergydashboard.github.io/developer/cla.html). Web pages with [information for developers](https://openenergydashboard.github.io/developer/) is available. If you have any questions or concerns feel free to at engage@OpenEnergyDashboard.org. +We welcome others to contribute to this project by writing code for submission or collaborating with us. Before contributing, please sign our [Contributor License Agreement](https://openenergydashboard.org/developer/cla/). Web pages with [information for developers](https://openenergydashboard.org/developer/developer/) is available. If you have any questions or concerns feel free to [contact us](https://OpenEnergyDashboard.org/contact/). ## Code of Conduct ## @@ -53,4 +53,4 @@ If you think there is a security concern in the OED software, the please visit o ## Contact ## -To contact us, see our [contact web page](https://openenergydashboard.github.io/contact.html), send an email to info@OpenEnergyDashboard.org or open an issue on GitHub. +To contact us, see our [contact web page](https://openenergydashboard.org/contact/) or open an issue on GitHub. diff --git a/SECURITY.md b/SECURITY.md index 1de3e1d8f..00ac40717 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ OED welcomes input at any time. Having said that, security issues can be special as they could represent a risk to those using the OED software. Given this, OED suggests these avenues for reporting a security concern: -1. If the concern may represent a real risk to sites, you should report it to [security@OpenEnergyDashboard.org](mailto:security@OpenEnergyDashboard.org). This email is regularly monitored and you should receive a response indicating your concern has been received by the project. Please provide the type of information that would be in an issue including what you are concerned about, where it is located in the code base (if possible), any links to security notices about this issue and any thoughts on how to address the issue. Additionally, you are welcome to indicate you would like us to publicly acknowledge you when an issue and/or pull request is created. If you would like this then please also provide your GitHub ID if you have one. We may delay doing this until a security patch is available for sites to install. +1. If the concern may represent a real risk to sites, you should report it via our [contact page](https://OpenEnergyDashboard.org/contact/), click to get to the "Contact Form" and select "Security" as the topic/area. Submissions are regularly monitored and you should receive a response indicating your concern has been received by the project (if you provide contact information). Please provide the type of information that would be in an issue including what you are concerned about, where it is located in the code base (if possible), any links to security notices about this issue and any thoughts on how to address the issue. Additionally, you are welcome to indicate you would like us to publicly acknowledge you when an issue and/or pull request is created. If you would like this then please also provide your GitHub ID if you have one. We may delay doing this until a security patch is available for sites to install. 2. You can open an [issue](https://github.com/OpenEnergyDashboard/OED/issues) on our GitHub repository. The same information as requested in the previous item is appreciated. The major difference is that issues are usually public so anyone (including bad actors) can see the issue before the OED project can act. 3. You can create a draft security advisory by using the "Report a vulnerability" as the issue type on the OED [issues](https://github.com/OpenEnergyDashboard/OED/issues) on our GitHub repository. Since OED is not a package included in other software, this may be less applicable but still available. These are not public unless the project chooses to make them so. diff --git a/SUPPORT.md b/SUPPORT.md index eaa632759..72ad8c484 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,3 +1,3 @@ # OED Support -OED maintains a [website](https://openenergydashboard.github.io/), [help ages](https://openenergydashboard.github.io/help/index.html) and [contact info](https://openenergydashboard.github.io/contact.html) to support you in interacting with the project. We welcome your input, thoughts, ideas, questions and requests for help. +OED maintains a [website](https://openenergydashboard.org/), [help pages](https://openenergydashboard.org/helpV1_0_0/user/) and [contact info](https://openenergydashboard.org/contact/) to support you in interacting with the project. We welcome your input, thoughts, ideas, questions and requests for help. diff --git a/USAGE.md b/USAGE.md index cace7e26d..d7f1aa238 100644 --- a/USAGE.md +++ b/USAGE.md @@ -2,8 +2,8 @@ ## Developers -The [developer website](https://openenergydashboard.github.io/developer/) is available to learn about working with the OED project. +The [developer website](https://openenergydashboard.org/developer/developer/) is available to learn about working with the OED project. ## Production/Sites -The [admin installation page](https://OpenEnergyDashboard.github.io/help/v0.7.0/adminInstallation.html) is available to learn about installing OED for a production environment (normal OED site). These are part of the overall admin help pages for OED. +The [admin documentation page](https://openenergydashboard.org/helpV1_0_0/admin/) has a section about "Site Installation" to learn about installing OED for a production environment (normal OED site). These are part of the overall admin help pages for OED. diff --git a/pull_request_template.md b/pull_request_template.md index a9e491d1a..982321649 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -17,9 +17,9 @@ Fixes #[issue] (Note what you have done by placing an "x" instead of the space in the [ ] so it becomes [x]. It is hoped you do all of them.) -- [ ] I have followed the [OED pull request](https://openenergydashboard.github.io/developer/pr.html) ideas +- [ ] I have followed the [OED pull request](https://openenergydashboard.org/developer/pr/) ideas - [ ] I have removed text in ( ) from the issue request -- [ ] You acknowledge that every person contributing to this work has signed the [OED Contributing License Agreement](https://openenergydashboard.github.io/developer/cla.html) and each author is listed in the Description section. +- [ ] You acknowledge that every person contributing to this work has signed the [OED Contributing License Agreement](https://openenergydashboard.org/developer/cla/) and each author is listed in the Description section. ## Limitations diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 92fbcf3a2..5e16f9fc8 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -31,7 +31,7 @@ export default function DashboardComponent() {
-
+
{chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } diff --git a/src/client/app/components/FooterComponent.tsx b/src/client/app/components/FooterComponent.tsx index 18e8b4f53..93e27bdb3 100644 --- a/src/client/app/components/FooterComponent.tsx +++ b/src/client/app/components/FooterComponent.tsx @@ -16,16 +16,17 @@ export default function FooterComponent() { return ( ); } @@ -36,4 +37,4 @@ const footerStyle: React.CSSProperties = { padding: '10px 0px', textAlign: 'center', width: '100%' -}; \ No newline at end of file +}; diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 3464c8a6c..21bd5288b 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -36,7 +36,7 @@ export default function HeaderButtonsComponent() { const version = useAppSelector(selectOEDVersion); const helpUrl = useAppSelector(selectHelpUrl); // options help - const optionsHelp = helpUrl + '/optionsMenu.html'; + const optionsHelp = helpUrl + '/optionsMenu/'; const loggedInAsAdmin = useAppSelector(selectIsAdmin); const loggedIn = useAppSelector(selectIsLoggedIn); @@ -127,7 +127,7 @@ export default function HeaderButtonsComponent() { display: pathname === '/' ? 'block' : 'none' }; // Admin help or regular user page - const neededPage = loggedInAsAdmin ? '/adminPageChoices.html' : '/pageChoices.html'; + const neededPage = loggedInAsAdmin ? '/adminPageChoices/' : '/pageChoices/'; const currentPageChoicesHelp = helpUrl + neededPage; setState(prevState => ({ diff --git a/src/client/app/components/LanguageSelectorComponent.tsx b/src/client/app/components/LanguageSelectorComponent.tsx index 5a5c3f035..cb081f4a4 100644 --- a/src/client/app/components/LanguageSelectorComponent.tsx +++ b/src/client/app/components/LanguageSelectorComponent.tsx @@ -6,10 +6,9 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { selectSelectedLanguage, updateSelectedLanguage } from '../redux/slices/appStateSlice'; -import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { LanguageTypes } from '../types/redux/i18n'; -import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; +import { selectHelpUrl } from '../redux/slices/adminSlice'; /** * A component that allows users to select which language the page should be displayed in. @@ -19,10 +18,7 @@ export default function LanguageSelectorComponent() { const dispatch = useAppDispatch(); const selectedLanguage = useAppSelector(selectSelectedLanguage); - const version = useAppSelector(selectOEDVersion); - const baseHelpUrl = useAppSelector(selectBaseHelpUrl); - - const helpUrl = baseHelpUrl + version; + const helpUrl = useAppSelector(selectHelpUrl); return ( <> @@ -48,7 +44,7 @@ export default function LanguageSelectorComponent() { + href={helpUrl + '/language/'}> diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index f49f99009..28b0d082c 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -18,6 +18,7 @@ import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectAreaScalingFromEntity } from '../redux/selectors/entitySelectors'; import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; @@ -30,7 +31,7 @@ import { itemMapInfoOk, normalizeImageDimensions } from '../utils/calibration'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { useTranslate } from '../redux/componentHooks'; import SpinnerComponent from './SpinnerComponent'; @@ -144,25 +145,20 @@ export default function MapChartComponent() { for (const meterID of selectedMeters) { // Get meter id number. - // Get meter GPS value. - const gps = meterDataById[meterID].gps; + const entity = meterDataById[meterID]; // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && meterReadings !== undefined) { - let meterArea = meterDataById[meterID].area; + if (entity.gps !== undefined && entity.gps !== null && meterReadings !== undefined) { // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); - } + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const meterArea = selectAreaScalingFromEntity(entity, selectedAreaUnit, areaNormalization); // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, entity.gps, origin, scaleOfMap, map.northAngle); // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + if (itemMapInfoOk(meterID, DataType.Meter, map, entity.gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); @@ -174,7 +170,7 @@ export default function MapChartComponent() { // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !meterIsFetching) { // Meter name to include in hover on graph. - const label = meterDataById[meterID].identifier; + const label = entity.identifier; // The usual color for this meter. colors.push(getGraphColor(meterID, DataType.Meter)); if (!readingsData) { @@ -220,24 +216,20 @@ export default function MapChartComponent() { for (const groupID of selectedGroups) { // Get group id number. - // Get group GPS value. - const gps = groupDataById[groupID].gps; + const entity = groupDataById[groupID]; // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && groupData !== undefined) { - let groupArea = groupDataById[groupID].area; - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); - } + if (entity.gps !== undefined && entity.gps !== null && groupData !== undefined) { + // we either don't care about area, or we do in which case there needs to be a nonzero area + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const groupArea = selectAreaScalingFromEntity(entity, selectedAreaUnit, areaNormalization); // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, entity.gps, origin, scaleOfMap, map.northAngle); // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + if (itemMapInfoOk(groupID, DataType.Group, map, entity.gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); @@ -248,7 +240,7 @@ export default function MapChartComponent() { // This protects against there being no readings or that the data is being updated. if (readingsData && !groupIsFetching) { // Group name to include in hover on graph. - const label = groupDataById[groupID].name; + const label = entity.name; // The usual color for this group. colors.push(getGraphColor(groupID, DataType.Group)); if (!readingsData) { diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index 43f60deb4..3e0427ca4 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -14,6 +14,8 @@ import { useAppSelector } from '../redux/reduxHooks'; import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import { LanguageTypes } from 'types/redux/i18n'; /** * Component that defines compare chart @@ -28,6 +30,7 @@ export default function MultiCompareChartComponent() { const sortingOrder = useAppSelector(selectSortingOrder); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); + const locale = useAppSelector(selectSelectedLanguage); const meterDataByID = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); @@ -74,7 +77,7 @@ export default function MultiCompareChartComponent() { } }); - selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder); + selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder, locale); // Compute how much space should be used in the bootstrap grid system @@ -139,22 +142,13 @@ function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeri /** * @param ids An array of items being compared that contain but are more than the id * @param sortingOrder The desired order or the comparison items based on change + * @param locale Current language selected from Redux state * @returns An array of items being compared in desired sortingOrder */ -function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { +function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder, locale: LanguageTypes): CompareEntity[] { switch (sortingOrder) { case SortingOrder.Alphabetical: - ids.sort((a, b) => { - const identifierA = a.identifier.toLowerCase(); - const identifierB = b.identifier.toLowerCase(); - if (identifierA < identifierB) { - return -1; - } - if (identifierA > identifierB) { - return 1; - } - return 0; - }); + ids.sort((idA, idB) => idA.identifier.toLowerCase().localeCompare(idB.identifier.toLowerCase(), locale, { sensitivity: 'accent' })); break; case SortingOrder.Ascending: ids.sort((a, b) => { diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx index 3ff51879b..dcee2f321 100644 --- a/src/client/app/components/RadarChartComponent.tsx +++ b/src/client/app/components/RadarChartComponent.tsx @@ -13,13 +13,14 @@ import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectRadarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectScalingFromEntity } from '../redux/selectors/entitySelectors'; import { selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; import { DataType } from '../types/Datasources'; import Locales from '../types/locales'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import { useTranslate } from '../redux/componentHooks'; @@ -71,17 +72,13 @@ export default function RadarChartComponent() { // Add all valid data from existing meters to the radar plot for (const meterID of selectedMeters) { if (meterReadings) { - const meterArea = meterDataById[meterID].area; + const entity = meterDataById[meterID]; // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - meterArea * getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const scaling = selectScalingFromEntity(entity, selectedAreaUnit, areaNormalization, rateScaling); const readingsData = meterReadings[meterID]; if (readingsData) { - const label = meterDataById[meterID].identifier; + const label = entity.identifier; const colorID = meterID; // TODO If we are sure the data is always defined then remove this commented out code. // Be consistent for all graphing and groups below. @@ -132,17 +129,13 @@ export default function RadarChartComponent() { for (const groupID of selectedGroups) { // const byGroupID = state.readings.line.byGroupID[groupID]; if (groupData) { - const groupArea = groupDataById[groupID].area; + const entity = groupDataById[groupID]; // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const scaling = selectScalingFromEntity(entity, selectedAreaUnit, areaNormalization, rateScaling); const readingsData = groupData[groupID]; if (readingsData) { - const label = groupDataById[groupID].name; + const label = entity.name; const colorID = groupID; // if (readingsData.readings === undefined) { // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index b9eac43cf..b3a2a6a73 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -11,6 +11,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; +import { selectScalingFromEntity } from '../redux/selectors/entitySelectors'; import { selectGraphState } from '../redux/slices/graphSlice'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; @@ -18,7 +19,7 @@ import { GroupDataByID } from '../types/redux/groups'; import { MeterDataByID } from '../types/redux/meters'; import { UnitDataById } from '../types/redux/units'; import { isValidThreeDInterval, roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { lineUnitLabel } from '../utils/graphics'; // Both translates are used since some are in the function component where the React Hook is okay // and some are in other functions where the older method is needed. @@ -138,23 +139,13 @@ function formatThreeDData( // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. const rateScaling = needsRateScaling ? currentSelectedRate.rate : 1; - const meterArea = meterOrGroup === MeterOrGroup.meters ? - meterDataById[selectedMeterOrGroupID].area + const entity = meterOrGroup === MeterOrGroup.meters ? + meterDataById[selectedMeterOrGroupID] : - groupDataById[selectedMeterOrGroupID].area; + groupDataById[selectedMeterOrGroupID]; + const scaling = selectScalingFromEntity(entity, graphState.selectedAreaUnit, graphState.areaNormalization, rateScaling); - const areaUnit = meterOrGroup === MeterOrGroup.meters ? - meterDataById[selectedMeterOrGroupID].areaUnit - : - groupDataById[selectedMeterOrGroupID].areaUnit; - - // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!graphState.areaNormalization || (meterArea > 0 && areaUnit != AreaUnitType.none)) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = graphState.areaNormalization ? - meterArea * getAreaUnitConversion(areaUnit, graphState.selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!graphState.areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { zDataToRender = data.zData.map(day => day.map(reading => reading === null ? null : reading * scaling)); } } diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index 313a288d9..eb5bcd3d0 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -32,14 +32,12 @@ export function UnsavedWarningComponent(props: UnsavedWarningProps) { submitChanges(changes) .unwrap() .then(() => { - //TODO translate me showSuccessNotification(translate('unsaved.success')); if (blocker.state === 'blocked') { blocker.proceed(); } }) .catch(() => { - //TODO translate me showErrorNotification(translate('unsaved.failure')); if (blocker.state === 'blocked') { blocker.proceed(); diff --git a/src/client/app/components/admin/users/UsersDetailComponent.tsx b/src/client/app/components/admin/users/UsersDetailComponent.tsx index c01c382db..3d64cb60c 100644 --- a/src/client/app/components/admin/users/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/users/UsersDetailComponent.tsx @@ -10,6 +10,8 @@ import TooltipHelpComponent from '../../TooltipHelpComponent'; import TooltipMarkerComponent from '../../TooltipMarkerComponent'; import CreateUserModalComponent from './CreateUserModalComponent'; import UserViewComponent from './UserViewComponent'; +import { selectSelectedLanguage } from '../../../redux/slices/appStateSlice'; +import { useAppSelector } from '../../../redux/reduxHooks'; const tooltipStyle = { display: 'inline-block', @@ -22,6 +24,7 @@ const tooltipStyle = { * @returns User Detail element */ export default function UserDetailComponent() { + const locale = useAppSelector(selectSelectedLanguage); const translate = useTranslate(); const { data: users = stableEmptyUsers } = userApi.useGetUsersQuery(); @@ -42,7 +45,7 @@ export default function UserDetailComponent() { {// display users and sort by username alphabetically [...users] - .sort((a, b) => a.username.localeCompare(b.username)) + .sort((a, b) => a.username.localeCompare(b.username, locale, { sensitivity : 'accent' })) .map(user => ( - ((unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase() > - (unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase()) ? 1 : - (((unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase() > - (unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase()) ? -1 : 0)) + ((unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase().localeCompare(( + unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase(), locale, + { sensitivity: 'accent'}))) .map(conversionData => ( (MetersCSVUploadDefaults); const [selectedFile, setSelectedFile] = React.useState(null); const [isValidFileType, setIsValidFileType] = React.useState(false); + // tracks if should show spinner (true while loading data, false otherwise) + const [showSpinner, setShowSpinner] = React.useState(false); const dispatch = useAppDispatch(); // Check for admin status const isAdmin = useAppSelector(selectIsAdmin); @@ -72,7 +75,10 @@ export default function MetersCSVUploadComponent() { const handleSubmit = async (e: React.MouseEvent) => { e.preventDefault(); if (selectedFile) { + // show spinner before calling api, then stop it immediately after + setShowSpinner(true); const { success, message } = await submitMeters(meterData, selectedFile, dispatch); + setShowSpinner(false); if (success) { showSuccessNotification(message); } else { @@ -81,6 +87,11 @@ export default function MetersCSVUploadComponent() { } }; + const spinContainerStyle = { + display: 'flex', + justifyContent: 'center' + }; + const tooltipStyle = { display: 'inline-block', fontSize: '50%', @@ -93,118 +104,124 @@ export default function MetersCSVUploadComponent() { return ( - -
- - -
-

- {translate('csv.upload.meters')} -
- -
-

-
- - - - -