diff --git a/.gitignore b/.gitignore index 0910e89c44..a74f392f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ nohup.out # Visual Studio Code settings file .vscode/settings.json - +.vscode/launch.json # tmp file src/server/tmp/* diff --git a/.vscode/launch.json b/.vscode/launch.json index 5723943bc0..004a0a438f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,9 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Attach to Node in Docker", "type": "node", "request": "attach", @@ -44,6 +46,7 @@ "Attach to Node in Docker", "Launch Firefox against localhost" ] + } ] } diff --git a/as b/as new file mode 100644 index 0000000000..e13c0ed59e --- /dev/null +++ b/as @@ -0,0 +1,7 @@ + origin/HEAD -> origin/development + origin/ann_branch + origin/ann_branch_2 + origin/ann_will_branch + origin/codebase-testing + origin/development + origin/w/dev diff --git a/d b/d new file mode 100644 index 0000000000..e13c0ed59e --- /dev/null +++ b/d @@ -0,0 +1,7 @@ + origin/HEAD -> origin/development + origin/ann_branch + origin/ann_branch_2 + origin/ann_will_branch + origin/codebase-testing + origin/development + origin/w/dev diff --git a/src/client/app/components/ChartLinkComponent.tsx b/src/client/app/components/ChartLinkComponent.tsx index 0cfacbfcbf..295a1ae707 100644 --- a/src/client/app/components/ChartLinkComponent.tsx +++ b/src/client/app/components/ChartLinkComponent.tsx @@ -5,15 +5,28 @@ import * as React from 'react'; import { toast } from 'react-toastify'; import ReactTooltip from 'react-tooltip'; -import { Button, ButtonGroup, Input } from 'reactstrap'; +import { Button, ButtonGroup,Input} from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectChartLink } from '../redux/selectors/uiSelectors'; -import { selectChartLinkHideOptions, setChartLinkOptionsVisibility } from '../redux/slices/appStateSlice'; -import { selectSelectedGroups, selectSelectedMeters } from '../redux/slices/graphSlice'; -import { showErrorNotification, showInfoNotification } from '../utils/notifications'; +import { + selectChartLinkHideOptions, selectIsKeepCurrent, setChartLinkOptionsVisibility, + setIsKeepCurrent +} from '../redux/slices/appStateSlice'; +import { + selectSelectedGroups, + selectSelectedMeters, + selectQueryTimeInterval +} from '../redux/slices/graphSlice'; +import { + showErrorNotification, + showInfoNotification +} from '../utils/notifications'; import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { wellStyle, rowFlexStart } from '../styles/modalStyle'; +import { wellStyle, rowFlexStart, labelStyle } from '../styles/modalStyle'; +import { checkboxStyle } from '../styles/modalStyle'; + + /** * @returns chartLinkComponent @@ -26,59 +39,134 @@ export default function ChartLinkComponent() { const linkHideOptions = useAppSelector(selectChartLinkHideOptions); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); + const queryTimeInterval = useAppSelector(selectQueryTimeInterval); + const isKeepCurrent = useAppSelector(selectIsKeepCurrent); const ref = React.useRef(null); + + // THIS react.UseMemo ONLY RETURNS TRUE WHEN THE CONDITIONS ARE MET FOR USING KEEP CURRENT (left is is bounded and right is unbounded) + const shouldShowKeepCurrentCheckbox = React.useMemo(() => { + if (!queryTimeInterval) return false; + if (queryTimeInterval.getIsBounded()) return false; + if ( + queryTimeInterval.getStartTimestamp() == null && + !queryTimeInterval.getIsBounded() + ) + return false; + return true; + }, [queryTimeInterval]); + const handleButtonClick = () => { // First attempt to write directly to user's clipboard. - navigator.clipboard.writeText(linkText) + navigator.clipboard + .writeText(linkText) .then(() => { - showInfoNotification(translate('clipboard.copied'), toast.POSITION.TOP_RIGHT, 1000); + showInfoNotification( + translate('clipboard.copied'), + toast.POSITION.TOP_RIGHT, + 1000 + ); }) .catch(() => { // if operation fails, open copyable text for manual copy. - showErrorNotification(translate('clipboard.not.copied'), toast.POSITION.TOP_RIGHT, 1000); + showErrorNotification( + translate('clipboard.not.copied'), + toast.POSITION.TOP_RIGHT, + 1000 + ); setLinkTextVisible(true); }); }; if (selectedMeters.length > 0 || selectedGroups.length > 0) { return (
+ {/* inputting new "keep chart current" feature */} +
{translate('chart.link.options.title')}
+ {/* hide options checkbox */} +
+ { + e.stopPropagation(); + dispatch(setChartLinkOptionsVisibility(!linkHideOptions)); + }} + onMouseOver={() => { + ref.current && ReactTooltip.show(ref.current); + }} + onMouseLeave={() => { + ref.current && ReactTooltip.hide(ref.current); + }} + /> + + +
+ {/* keep current checkbox ----> */} + {/* USE shouldShowKeepCurrentCheckbox AS THE CONDITIONAL BOOLEAN */} +
+ { + ref.current && ReactTooltip.show(ref.current); + }} + onMouseLeave={() => { + ref.current && ReactTooltip.hide(ref.current); + }} + checked={isKeepCurrent} + onChange={e => dispatch(setIsKeepCurrent(e.target.checked))} + disabled={!shouldShowKeepCurrentCheckbox} + // shouldShow is the value we use to tell if it should be disabled or not + //when disabled = true, you CANNOT click the checkbox + /> + + + {/* we need to create a tool tip for "keep chart current" checkbox */} + +
+
- - - + -
- { - linkTextVisible && -
- {linkText} -
- } -
+ {linkTextVisible &&
{linkText}
} + ); - } - else { + } else { return null; } -} \ No newline at end of file +} diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx index 62dcd445a8..347ecab15a 100644 --- a/src/client/app/components/PlotNavComponent.tsx +++ b/src/client/app/components/PlotNavComponent.tsx @@ -12,7 +12,7 @@ import { changeSliderRange, selectChartToRender, selectHistoryIsDirty, selectSelectedGroups, selectSelectedMeters, selectSliderRangeInterval, selectInitialXAxisRange, - selectQueryTimeInterval, updateTimeIntervalAndSliderRange + selectQueryTimeInterval, updateTimeIntervalAndSliderRange,updateTimeCreated } from '../redux/slices/graphSlice'; import HistoryComponent from './HistoryComponent'; import { ChartTypes } from '../types/redux/graph'; @@ -110,6 +110,7 @@ export const RefreshGraphComponent = () => { const maxX = initialXAxisRange?.getEndTimestamp?.(); const nextInterval = getNextQueryTimeInterval(queryTimeInterval, sliderInterval, minX, maxX); dispatch(updateTimeIntervalAndSliderRange(nextInterval)); + dispatch(updateTimeCreated()); } }} /> diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index e64f5cd09f..1a5625f8ca 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -68,6 +68,7 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { 'help.home.select.units': { link: `${helpUrl}/graphingUnits/` }, 'help.home.readings.per.day': { link: `${helpUrl}/readingsPerDay/` }, 'help.home.toggle.chart.link': { link: `${helpUrl}/chartLink/` }, + 'help.home.toggle.chart.link.keep.current': { link: `${helpUrl}/chartLink/` }, 'help.groups.groupdetails': { link: `${helpUrl}/groupViewing/#groupDetails` }, 'help.groups.groupview': { link: `${helpUrl}/groupViewing/` }, 'help.meters.meterview': { link: `${helpUrl}/meterViewing/` }, diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index b43445045d..fc77bb7e36 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -5,7 +5,7 @@ import { LanguageTypes } from 'types/redux/i18n'; import { selectGroupDataById } from '../../redux/api/groupsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { selectChartLinkHideOptions, selectSelectedLanguage } from '../../redux/slices/appStateSlice'; +import { selectChartLinkHideOptions, selectIsKeepCurrent, selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, ShiftAmount } from '../../types/redux/graph'; @@ -453,9 +453,10 @@ export const selectChartLink = createAppSelector( selectGraphState, selectChartLinkHideOptions, selectSliderRangeInterval, + selectIsKeepCurrent, state => state.maps.selectedMap ], - (current, chartLinkHideOptions, rangeSliderInterval, selectedMap) => { + (current, chartLinkHideOptions, rangeSliderInterval, isKeepCurrent,selectedMap) => { // Determine the beginning of the URL to add arguments to. // This is the current URL. const winLocHref = window.location.href; @@ -514,6 +515,12 @@ export const selectChartLink = createAppSelector( if (chartLinkHideOptions) { linkText += '&optionsVisibility=false'; } + if (isKeepCurrent) { + const timeCreatedEnd = current.timeCreated.getEndTimestamp(); + const sliderStart = current.rangeSliderInterval.getStartTimestamp(); + const diffDays = timeCreatedEnd.diff(sliderStart, 'days', true); + linkText += `&timeSpan=${diffDays.toFixed(2)}`; + } return linkText; } ); diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index a356bd4619..7413752f03 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -25,6 +25,7 @@ export interface AppState { selectedLanguage: LanguageTypes; languageManuallySet: boolean; refreshingReadings: boolean; + isKeepCurrent: boolean; } const defaultState: AppState = { @@ -33,7 +34,8 @@ const defaultState: AppState = { selectedLanguage: LanguageTypes.en, chartLinkHideOptions: false, languageManuallySet: false, - refreshingReadings: false + refreshingReadings: false, + isKeepCurrent: false }; export const appStateSlice = createThunkSlice({ @@ -63,6 +65,9 @@ export const appStateSlice = createThunkSlice({ setRefresingReadings: create.reducer((state, action) => { state.refreshingReadings = action.payload; }), + setIsKeepCurrent: create.reducer((state, action) => { + state.isKeepCurrent = action.payload; + }), initApp: create.asyncThunk( // Thunk initiates many data fetching calls on startup before react begins to render async (_: void, { dispatch }) => { @@ -132,7 +137,9 @@ export const appStateSlice = createThunkSlice({ selectOptionsVisibility: state => state.optionsVisibility, selectSelectedLanguage: state => state.selectedLanguage, selectChartLinkHideOptions: state => state.chartLinkHideOptions, - selectRefreshingReadings: state => state.refreshingReadings + selectRefreshingReadings: state => state.refreshingReadings, + selectIsKeepCurrent: state => state.isKeepCurrent + } }); @@ -143,7 +150,8 @@ export const { setOptionsVisibility, updateSelectedLanguage, setChartLinkOptionsVisibility, - setRefresingReadings + setRefresingReadings, + setIsKeepCurrent } = appStateSlice.actions; export const { @@ -151,5 +159,6 @@ export const { selectOptionsVisibility, selectSelectedLanguage, selectChartLinkHideOptions, - selectRefreshingReadings + selectRefreshingReadings, + selectIsKeepCurrent } = appStateSlice.selectors; diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index c326d840af..fa2cc31ee4 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -25,6 +25,7 @@ const defaultState: GraphState = { initialXAxisRange: TimeInterval.unbounded(), queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), + timeCreated: new TimeInterval(moment(),moment()), duration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), @@ -140,7 +141,10 @@ export const graphSlice = createSlice({ if (state.current.threeD.meterOrGroupID !== action.payload) { state.current.threeD.meterOrGroupID = action.payload; } + },updateTimeCreated: state => { + state.current.timeCreated = new TimeInterval(moment(), moment()); }, + // Added here because it is used to easily track if the last added was meter or group, which then used to select which is active in threeD setLastAddedMeterOrGroup: (state, action: PayloadAction) => { state.current.lastAddedMeterOrGroup = action.payload; @@ -289,6 +293,12 @@ export const graphSlice = createSlice({ case 'shiftTimeInterval': current.shiftTimeInterval = TimeInterval.fromString(value); break; + case 'timeSpan':{ + const days = parseFloat(value); + const now = moment(); + const leftBound = now.clone().subtract(days, 'days'); + current.rangeSliderInterval = new TimeInterval(leftBound,undefined); + break;} } }); } @@ -333,7 +343,8 @@ export const graphSlice = createSlice({ selectPlotlySliderMin: state => state.current.rangeSliderInterval.getStartTimestamp()?.utc().toDate().toISOString(), selectPlotlySliderMax: state => state.current.rangeSliderInterval.getEndTimestamp()?.utc().toDate().toISOString(), selectShiftAmount: state => state.current.shiftAmount, - selectShiftTimeInterval: state => state.current.shiftTimeInterval + selectShiftTimeInterval: state => state.current.shiftTimeInterval, + selectTimeCreated : state => state.current.timeCreated } }); @@ -349,11 +360,12 @@ export const { selectSelectedGroups, selectQueryTimeInterval, selectThreeDMeterOrGroup, selectCompareTimeInterval, selectThreeDMeterOrGroupID, selectThreeDReadingInterval, - selectLastMeterOrGroup, selectGraphAreaNormalization, + selectLastMeterOrGroup, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, selectPlotlySliderMax, selectPlotlySliderMin, selectShiftAmount, - selectShiftTimeInterval, selectInitialXAxisRange + selectShiftTimeInterval, selectInitialXAxisRange,selectGraphAreaNormalization,selectTimeCreated + } = graphSlice.selectors; // actionCreators exports @@ -372,6 +384,6 @@ export const { updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, updateShiftAmount, setInitialXAxisRange, updateTimeIntervalAndSliderRange, - updateShiftTimeInterval + updateShiftTimeInterval,updateTimeCreated } = graphSlice.actions; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 51c6fa3e67..6b290f7ea4 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -52,6 +52,7 @@ const LocaleTranslationData = { "calibration.submit.button": "Submit", "cancel": "Cancel", "chart.link": "Chart Link", + "chart.link.options.title": "Chart Link Options:", "child.groups": "Child Groups", "child.meters": "Child Meters", "clear.date.range": "Clear Date Range", @@ -302,11 +303,13 @@ const LocaleTranslationData = { "help.home.select.rates": "Rates determine the time normalization for a line graph. Please visit {link} for further details and information", "help.home.select.units": "Units determine the values displayed in a graphic. Please visit {link} for further details and information", "help.home.toggle.chart.link": "With the \"Toggle chart link\" button a box appears that gives a URL that will recreate the current graphic. The link will recreate the graphic in the future, esp. for others to see. The person using this URL will be in a fully functional OED so they can make changes after the original graphic is displayed. You can help discourage changes by choosing the option to hide the option choice so they are not visible when using the URL. Please visit {link} for further details and information.", + "help.home.toggle.chart.link.keep.current": "With the \"Keep current\" option, the chart link will always show the same span of time as the original graphic, but shifted so it ends at the current time when the link is used. This is only available if the server data range goes up to the present (unbounded on the right). The link will work for any supported graphic type (line or bar) and ensures viewers always see the most recent data for that time span. Please visit {link} for further details and information.", "help.meters.meterview": "This page shows information on meters. Please visit {link} for further details and information.", "here": "here", "hide": "Hide", "hide.options": "Hide options", "hide.options.in.link": "Hide options in link", + "hide.options.when.using.this.label": "Hide options when using this link", "home": "Home", "hour": "Hour", "identifier": "Identifier:", @@ -325,6 +328,7 @@ const LocaleTranslationData = { "invalid.number": "Please submit a valid number (between 0 and 2.0)", "invalid.token.login": "Token has expired. Please log in again.", "invalid.token.login.admin": "Token has expired. Please log in again to view this page.", + "keep.chart.current.label": "Keep chart current", "language": "Language", "last.four.weeks": "Last four weeks", "last.week": "Last week", @@ -640,6 +644,7 @@ const LocaleTranslationData = { "calibration.submit.button": "Soumettre", "cancel": "Annuler", "chart.link": "Chart Link\u{26A1}", + "chart.link.options.title": "Chart Link Options:\u{26A1}", "child.groups": "Groupes Enfants", "child.meters": "Mètres Enfants", "clear.date.range": "Clear Date Range\u{26A1}", @@ -890,11 +895,13 @@ const LocaleTranslationData = { "help.home.select.rates": "Rates determine the time normalization for a line graph. Please visit {link} for further details and information\u{26A1}", "help.home.select.units": "Units determine the values displayed in a graphic. Please visit {link} for further details and information\u{26A1}", "help.home.toggle.chart.link": "With the \"Toggle chart link\" button a box appears that gives a URL that will recreate the current graphic. The link will recreate the graphic in the future, esp. for others to see. The person using this URL will be in a fully functional OED so they can make changes after the original graphic is displayed. You can help discourage changes by choosing the option to hide the option choice so they are not visible when using the URL. Please visit {link} for further details and information.\u{26A1}", + "help.home.toggle.chart.link.keep.current": "With the \"Keep current\" option, the chart link will always show the same span of time as the original graphic, but shifted so it ends at the current time when the link is used. This is only available if the server data range goes up to the present (unbounded on the right). The link will work for any supported graphic type (line or bar) and ensures viewers always see the most recent data for that time span. Please visit {link} for further details and information.\u{26A1}", "help.meters.meterview": "This page shows information on meters. Please visit {link} for further details and information.\u{26A1}", "here": "ici", "hide": "Cacher", "hide.options": "Options de quai", "hide.options.in.link": "Hide options in link\u{26A1}", + "hide.options.when.using.this.label": "Hide options when using this link\u{26A1}", "home": "Accueil", "hour": "Hour\u{26A1}", "identifier": "Identifier:\u{26A1}", @@ -913,6 +920,7 @@ const LocaleTranslationData = { "invalid.number": "Please submit a valid number (between 0 and 2.0)\u{26A1}", "invalid.token.login": "Le jeton a expiré. Connectez-vous à nouveau.", "invalid.token.login.admin": "Le jeton a expiré. Please log in again to view this page.\u{26A1}", + "keep.chart.current.label": "Keep chart current\u{26A1}", "language": "Langue", "last.four.weeks": "Quatre dernières semaines", "last.week": "La semaine dernière", @@ -1228,6 +1236,7 @@ const LocaleTranslationData = { "calibration.submit.button": "Enviar", "cancel": "Cancelar", "chart.link": "Enlace de la tabla", + "chart.link.options.title": "Opciones de enlace de gráfico:", "child.groups": "Grupos secundarios", "child.meters": "Medidores secundarios", "clear.date.range": "Clear Date Range\u{26A1}", @@ -1478,11 +1487,13 @@ const LocaleTranslationData = { "help.home.select.rates": "Las tasas determinan la normalización de tiempo para un gráfico de línea. Por favor visite {link} para obtener más detalles e información.", "help.home.select.units": "Las unidades determinan los valores mostrados en un gráfico. Por favor visite {link} para obtener más detalles e información.", "help.home.toggle.chart.link": "Con el botón \"Alternar enlace de gráfico\" aparece un cuadro que da un URL para recrear el gráfico actual. El enlace recreará el gráfico en el futuro, especialmente para que otros lo vean. La persona que usa este URL estará en un OED completamente funcional para que pueda realizar cambios después de mostrar el gráfico original. Puede desalentar los cambios eligiendo la opción de esconder las opciones del menú para que no sean visible al usar el URL. Por favor visite {link} para obtener más detalles e información.", + "help.home.toggle.chart.link.keep.current": "Con la opción \"Mantener actualizado\", el enlace del gráfico siempre mostrará el mismo lapso de tiempo que el gráfico original, pero desplazado para que finalice en el momento actual cuando se utiliza. Esto solo está disponible si el rango de datos del servidor llega hasta el presente (sin límite a la derecha). El enlace funciona con cualquier tipo de gráfico compatible (línea o barra) y garantiza que los usuarios siempre vean los datos más recientes para ese lapso. Visite {link} para obtener más detalles e información.", "help.meters.meterview": "Esta página muestra información sobre los medidores. Por favor visite {link} para obtener más detalles e información.", "here": "aquí", "hide": "Esconder", "hide.options": "Esconder las opciones", "hide.options.in.link": "Esconder las opciones en el enlace", + "hide.options.when.using.this.label": "Ocultar opciones al usar este enlace", "home": "Inicio", "hour": "Hora", "identifier": "Identificador:", @@ -1501,6 +1512,7 @@ const LocaleTranslationData = { "invalid.number": "Por favor indique un número válido (entre 0 a 2.0)", "invalid.token.login": "El token se ha vencido. Inicie la sesión nuevamente", "invalid.token.login.admin": "El token se ha vencido. Inicie la sesión nuevamente para ver esta página.", + "keep.chart.current.label": "Mantener el gráfico actualizado", "language": "Idioma", "last.four.weeks": "Últimas cuatro semanas", "last.week": "La semana pasada", diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 9101ba8c91..f24112ae39 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -87,4 +87,5 @@ export interface GraphState { hotlinked: boolean; shiftAmount: ShiftAmount; shiftTimeInterval: TimeInterval; + timeCreated: TimeInterval; }