diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss index f860f35ec9383cbd6d4f9ebb0c4c3b885ac43898..234ceaa63beaaf5778919958491b1a79c1ea93c6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss @@ -55,6 +55,10 @@ flex-direction: row; overflow: scroll; + &.centered { + justify-content: center; + } + label { display: inline-block; width: 6rem; @@ -114,8 +118,8 @@ .element { display: ruby; - overflow: hidden; - text-overflow: ellipsis; + //overflow: hidden; + //text-overflow: ellipsis; } label { @@ -144,14 +148,18 @@ } } } +} +.timeline-zoom-and-move { .zoom-selector-container { + label { margin-right: 0.25rem; } .p-dropdown { - float: none + float: none; + width: 7rem; } } @@ -165,8 +173,17 @@ font-weight: normal; color: var(--gray-500); } + + .p-link:disabled { + cursor: not-allowed; + opacity: 50%; + } } + .reset-container { + margin-right: 1.5rem; + margin-left: 2.5rem; + } } .legend-header { @@ -268,6 +285,7 @@ color: #ffffff; text-align: right; padding-right: 10px; + padding-top: 3px; background-color: #8ba7d9; } @@ -275,6 +293,10 @@ height: 30px; } +.spinner { + color: var(--primary-300); +} + .legend-row { padding-top: 10px; padding-left: 10px; @@ -587,15 +609,20 @@ .timeline-popover { z-index: 1000; -} + opacity: 1.944; -.timeline-popover:before { - display: none !important; + :before, :after { + display: none !important; + } } -.timeline-popover:after { - display: none !important; -} +//.timeline-popover:before { +// display: none !important; +//} +// +//.timeline-popover:after { +// display: none !important; +//} .p-multiselect-header .p-multiselect-close { position: absolute; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.js new file mode 100644 index 0000000000000000000000000000000000000000..c3d41e4413cc67d25f68253ab6a74b3b55c9398e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.js @@ -0,0 +1,109 @@ +import {Dropdown} from "primereact/dropdown"; +import React, {useEffect, useState} from "react"; +import {updateStore} from "../../../../services/store.helper"; +import {Spinner} from "reactstrap"; +import UIConstants from "../../../../utils/ui.constants"; +import {Button} from "primereact/button"; +import {getMovePossibilities, getZoomPossibilities, getZoomTimes, moveTimeline} from "../../helpers/zoomandmove.helper"; + +function getIconButton(title, onClickCallback, iconClassName, disabled = false) { + let tooltipText = disabled ? "Maximum reached. Cannot " + title : title + return <button className="p-link" + title={tooltipText} + onClick={onClickCallback} + disabled={disabled} + data-testid={title}> + <i className={`pi ${iconClassName}`}/> + </button> +} + +function getZoomAndMoveActions(zoomLevelName, setZoomLevelName, visibleStartTime, setVisibleStartTime, visibleEndTime, setVisibleEndTime) { + const zoomLevelIndex = UIConstants.ALL_ZOOM_LEVELS.findIndex(level => level.name === zoomLevelName); + const zoomPossibilities = getZoomPossibilities(zoomLevelIndex) + const movePossibilities = getMovePossibilities(visibleStartTime, visibleEndTime) + + return <div className="move-container"> + <label>Move</label> + <div> + {getIconButton("Move Left", () => moveTimeline(setVisibleStartTime, setVisibleEndTime, -30), + "pi-angle-left", !movePossibilities.moveLeft)} + {getIconButton("Zoom out", () => setZoomLevelName(UIConstants.ALL_ZOOM_LEVELS[zoomLevelIndex + 1].name), + "pi-minus-circle", !zoomPossibilities.canZoomOut)} + {getIconButton("Zoom in", () => setZoomLevelName(UIConstants.ALL_ZOOM_LEVELS[zoomLevelIndex - 1].name), + "pi-plus-circle", !zoomPossibilities.canZoomIn)} + {getIconButton("Move Right", () => moveTimeline(setVisibleStartTime, setVisibleEndTime, 30), + "pi-angle-right", !movePossibilities.moveRight)} + </div> + </div> +} + +function getZoomSelect(currentZoomLevelName, allOptions, setZoomLevelName) { + return <div className="zoom-selector-container" data-testid="zoom-select"> + <label title="Set the amount of time surrounding the current time">Span 'now'</label> + <div> + <Dropdown optionLabel="name" optionValue="name" + value={currentZoomLevelName} + options={allOptions} + filter + showClear={false} + filterBy="name" + onChange={(e) => setZoomLevelName(e.value)} + placeholder="Zoom"/> + </div> + </div> +} + + +function getResetButton(onClickResetCallback) { + return <div className="reset-container"> + <label>Reset</label> + <div> + <Button icon="pi pi-undo" + className="p-button p-button-primary" + onClick={onClickResetCallback} + title="Reset Zoom to 1 day" + data-testid="zoom-reset-button" + /> + </div> + </div> +} + + +export default function ZoomAndMove(props) { + const { + timelineStore, + visibleStartTime, + setVisibleStartTime, + visibleEndTime, + setVisibleEndTime + } = props + + const [zoomLevelName, setZoomLevelName] = useState(timelineStore.zoomLevel === undefined ? UIConstants.DEFAULT_ZOOM_LEVEL.name : timelineStore.zoomLevel) + + useEffect(() => { + updateStore(UIConstants.STORE_KEY_TIMELINE, timelineStore, {["zoomLevel"]: zoomLevelName}) + const selectedZoomLevel = UIConstants.ALL_ZOOM_LEVELS.find(level => level.name === zoomLevelName); + if (selectedZoomLevel && setVisibleStartTime && setVisibleEndTime) { + let zoomTimes = getZoomTimes(selectedZoomLevel); + if (zoomTimes.start && zoomTimes.end) { + setVisibleStartTime(zoomTimes.start) + setVisibleEndTime(zoomTimes.end) + } + } + }, [zoomLevelName]) + + if (setVisibleStartTime === undefined || setVisibleEndTime === undefined || visibleStartTime === undefined || visibleEndTime === undefined) { + return <div className="group group--row"> + <Spinner className="m-4" style={{color: "var(--primary-300)"}} data-testid="zoom-loading-spinner"/> + </div> + } + + return <div className="timeline-zoom-and-move section"> + <div className="header">Zoom</div> + <div className="group"> + {getZoomSelect(zoomLevelName, UIConstants.ALL_ZOOM_LEVELS_WEEK, setZoomLevelName)} + {getResetButton(() => setZoomLevelName(UIConstants.DEFAULT_ZOOM_LEVEL.name))} + {getZoomAndMoveActions(zoomLevelName, setZoomLevelName, visibleStartTime, setVisibleStartTime, visibleEndTime, setVisibleEndTime)} + </div> + </div> +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.test.js new file mode 100644 index 0000000000000000000000000000000000000000..38f24718f48a783d840da01e76eae3bbf3854351 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/components/toolbar/ZoomAndMove.test.js @@ -0,0 +1,84 @@ +import {getMovePossibilities, getZoomPossibilities, getZoomTimes, moveTimeline} from "../../helpers/zoomandmove.helper"; +import ZoomAndMove from "./ZoomAndMove"; +import {render} from "@testing-library/react"; +import {clickItem, removeReact18ConsoleErrors} from "../../../../utils/test.helper"; + +removeReact18ConsoleErrors() + +jest.mock('../../../../services/store.helper', () => ({ + updateStore: jest.fn(), +})); +jest.mock('../../helpers/zoomandmove.helper', () => ({ + moveTimeline: jest.fn(), + getZoomPossibilities: jest.fn(), + getMovePossibilities: jest.fn(), + getZoomTimes: jest.fn(), +})); + +describe('ZoomAndMove', () => { + const mockTimelineStore = { + zoomLevel: undefined, + }; + const mockSetVisibleStartTime = jest.fn(); + const mockSetVisibleEndTime = jest.fn(); + const mockVisibleStartTime = 'mockVisibleStartTime'; + const mockVisibleEndTime = 'mockVisibleEndTime'; + + beforeEach(() => { + getZoomTimes.mockImplementation(() => { + return {start: mockVisibleStartTime, end: mockVisibleEndTime} + }); + getZoomPossibilities.mockImplementation(() => { + return {canZoomIn: true, canZoomOut: true} + }); + getMovePossibilities.mockImplementation(() => { + return {moveLeft: true, moveRight: true} + }) + + }); + + afterEach(() => { + jest.clearAllMocks(); + + }) + + it('renders loading spinner when props are not yet available', () => { + const pageContent = render( + <ZoomAndMove + timelineStore={mockTimelineStore} + /> + ); + + expect(pageContent.getByTestId("zoom-loading-spinner")).toBeInTheDocument(); + }); + + it('renders components and clicks them when data is available', () => { + moveTimeline.mockImplementation((_, __, minuteAmount) => { + if (minuteAmount === -30) { + mockSetVisibleStartTime.mockReturnValue(mockVisibleStartTime); + mockSetVisibleEndTime.mockReturnValue(mockVisibleEndTime); + } + }); + + const pageContent = render( + <ZoomAndMove + timelineStore={mockTimelineStore} + visibleStartTime={mockSetVisibleStartTime} + setVisibleStartTime={mockSetVisibleStartTime} + visibleEndTime={mockSetVisibleEndTime} + setVisibleEndTime={mockSetVisibleEndTime} + /> + ); + + expect(pageContent.getByTestId('zoom-select')).toBeInTheDocument(); + clickItem(pageContent.getByTestId('Zoom out')) + expect(mockSetVisibleStartTime).toHaveBeenNthCalledWith(1, mockVisibleStartTime); + expect(mockSetVisibleEndTime).toHaveBeenNthCalledWith(1, mockVisibleEndTime); + clickItem(pageContent.getByTestId('Move Left')) + expect(mockSetVisibleStartTime).toHaveBeenNthCalledWith(2, mockVisibleStartTime); + expect(mockSetVisibleEndTime).toHaveBeenNthCalledWith(2, mockVisibleEndTime); + clickItem(pageContent.getByTestId('zoom-reset-button')) + expect(mockSetVisibleStartTime).toHaveBeenNthCalledWith(3, mockVisibleStartTime); + expect(mockSetVisibleEndTime).toHaveBeenNthCalledWith(3, mockVisibleEndTime); + }); +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.js new file mode 100644 index 0000000000000000000000000000000000000000..e34272026c70a8436ecb15df28833cbd035b828d --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.js @@ -0,0 +1,129 @@ +import moment from "moment/moment"; +import UIConstants from "../../../utils/ui.constants"; + +const MIN_VISIBLE_START_TIME = moment(moment().startOf('day').format(UIConstants.CALENDAR_DATETIME_FORMAT)) +const MAX_VISIBLE_END_TIME = moment(moment().endOf('day').format(UIConstants.CALENDAR_DATETIME_FORMAT)) + +/** + * Determines whether zooming in/out is possible based on the available zoomLevels + * @param zoomLevelIndex current zoom level its index + * @return {{canZoomIn: boolean, canZoomOut: boolean}} + */ +export function getZoomPossibilities(zoomLevelIndex) { + let zoomPossibilities = {} + zoomPossibilities.canZoomIn = false + zoomPossibilities.canZoomOut = false + if (zoomLevelIndex > -1) { //only for valid zoom levels, enabdle the zoom possibilities + if (zoomLevelIndex >= UIConstants.ALL_ZOOM_LEVELS_WEEK.length - 1) { //largest zoom level reached + zoomPossibilities.canZoomIn = true + zoomPossibilities.canZoomOut = false + } else if (zoomLevelIndex === 0) { //smallest zoom level reached + zoomPossibilities.canZoomIn = false + zoomPossibilities.canZoomOut = true + } else { + zoomPossibilities.canZoomIn = true + zoomPossibilities.canZoomOut = true + } + } + + return zoomPossibilities +} + +/** + * Determines whether moving left/right is possible based on the visible start/end time + * @param visibleStartTime start time of header value in utc + * @param visibleEndTime end time of header value in utc + * @return {{moveLeft: boolean, moveRight: boolean}} + */ +export function getMovePossibilities(visibleStartTime, visibleEndTime) { + let movePossibilities = {} + movePossibilities.moveLeft = false + movePossibilities.moveRight = false + + if (visibleStartTime && visibleEndTime) { //only for valid times, enable the move possibilities + if (visibleStartTime.isAfter(MIN_VISIBLE_START_TIME)) { + movePossibilities.moveLeft = true + } + if (visibleEndTime.isBefore(MAX_VISIBLE_END_TIME)) { + movePossibilities.moveRight = true + } + } + + return movePossibilities +} + +/** + * Sets the new visible start/end times by adding or subtracting the 'minuteAmount'. + * It takes into account the extrema the start (00:00:00) and end times (23:59:99) can have + * @param setVisibleStartTime state callback function + * @param setVisibleEndTime state callback function + * @param minuteAmount number (negative means subtract, positive means add) + */ +export function moveTimeline(setVisibleStartTime, setVisibleEndTime, minuteAmount) { + setVisibleStartTime(previousVisibleStartTime => { + const newStartTime = moment(previousVisibleStartTime).add(minuteAmount, 'minutes'); + return getNewTimeOrExtrema(newStartTime) + }) + setVisibleEndTime(previousVisibleEndTime => { + const newEndTime = moment(previousVisibleEndTime).add(minuteAmount, 'minutes'); + return getNewTimeOrExtrema(newEndTime) + }) +} + +function getZoomTimesHoursMinutes(selectedZoomLevel) { + let zoomTimes = {} + if (selectedZoomLevel.hours === undefined || selectedZoomLevel.minutes === undefined) { + console.error("Couldn't zoom the time line because the selectedZoomLevel is invalid:", selectedZoomLevel) + } else { + let now = moment.utc().format(UIConstants.CALENDAR_DATETIME_FORMAT); + let newStartTime = now; + let newEndTime = now; + + ['hours', 'minutes'].forEach(timeKey => { + if (selectedZoomLevel[timeKey] > 0) { + const amount = selectedZoomLevel[timeKey] * 0.5; + newStartTime = moment(newStartTime).subtract(amount, timeKey); + newEndTime = moment(newEndTime).add(amount, timeKey); + } + }) + zoomTimes.start = getNewTimeOrExtrema(newStartTime) + zoomTimes.end = getNewTimeOrExtrema(newEndTime) + } + return zoomTimes +} + +/** + * Transforms the selected zoom level into the visible start and end times based on the current time ('now') + * If the zoomlevel is a 'day' value, it sets it to the extrema (00:00:00, 23:59:99) + * If the zoom level has 'hours/minutes' then: + * - start = now - (1/2 * hours/minutes) + * - end = now + (1/2 * hours/minutes) + * @param selectedZoomLevel {name: string, days: number, hours: number, minutes: number} + * @return {{start: moment, end: moment}} + */ +export function getZoomTimes(selectedZoomLevel) { + let zoomTimes = {} + if (selectedZoomLevel.days > 0) { + zoomTimes.start = MIN_VISIBLE_START_TIME + zoomTimes.end = MAX_VISIBLE_END_TIME + } else { + zoomTimes = getZoomTimesHoursMinutes(selectedZoomLevel) + } + return zoomTimes; +} + +/** + * If the new start time is before the minimum time (00:00:00) it return the minimum time + * If the new end time is after the maximum time (23:59:99) it return the maximum time + * @param newTime + * @return {*|moment.Moment} + */ +export function getNewTimeOrExtrema(newTime) { + if (newTime.isBefore(MIN_VISIBLE_START_TIME)) { + return MIN_VISIBLE_START_TIME + } + if (newTime.isAfter(MAX_VISIBLE_END_TIME)) { + return MAX_VISIBLE_END_TIME + } + return newTime +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.test.js new file mode 100644 index 0000000000000000000000000000000000000000..5ca2aaad934db7cf4ed359328d2b77b7c51a0c2f --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/zoomandmove.helper.test.js @@ -0,0 +1,224 @@ +import { + getMovePossibilities, + getNewTimeOrExtrema, + getZoomPossibilities, + getZoomTimes, + moveTimeline +} from "./zoomandmove.helper"; +import moment from "moment"; +import UIConstants from "../../../utils/ui.constants"; + + +describe('getNewTimeOrExtrema', () => { + const testFormat = 'HH:mm:ss'; + const MIN_VISIBLE_START = moment('26-05-1992T00:00:00', testFormat); + const MAX_VISIBLE_END = moment('26-05-1992-T23:59:59', testFormat); + + it('returns the extrema start of day if new time is before the start of day', () => { + const newTime = MIN_VISIBLE_START.subtract(1, 'hours') + const result = getNewTimeOrExtrema(newTime); + expect(result).toBe(MIN_VISIBLE_START); + }); + + it('returns the extrema end of day if new time is after the end of day', () => { + const newTime = MAX_VISIBLE_END.add(1, 'second'); + const result = getNewTimeOrExtrema(newTime); + expect(result).toBe(MAX_VISIBLE_END); + }); + + test.each(['12:34:56', '00:00:00', '23:59:99'])('returns the new time if it is within the visible range: %s', (time) => { + const newTime = moment(time, testFormat); + const result = getNewTimeOrExtrema(newTime); + expect(result).toBe(newTime) + }) +}); + +describe('getZoomTimes', () => { + let dateNowSpy + + beforeEach(() => { + //needs to be a timestamp in milliseconds for moment methods to work + //1692087751000 === Tuesday, August 15, 2023 8:22:31 + const timeStamp = 1692087751000 + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => timeStamp); + }); + + afterEach(() => { + dateNowSpy.mockRestore(); + }); + + + it('returns extrema times when selectedZoomLevel has days', () => { + const selectedZoomLevel = {days: 1}; + const result = getZoomTimes(selectedZoomLevel); + expect(result.start.format('HH:mm:ss')).toEqual('00:00:00'); + expect(result.end.format('HH:mm:ss')).toEqual('23:59:59'); + }); + + it('returns calculated times when selectedZoomLevel has minutes', () => { + const selectedZoomLevel = {hours: 0, minutes: 30}; + + const result = getZoomTimes(selectedZoomLevel); + + const expectedStart = '08:07:31'; + const expectedEnd = '08:37:31'; + + expect(result.start.format('HH:mm:ss')).toEqual(expectedStart); + expect(result.end.format('HH:mm:ss')).toEqual(expectedEnd); + }); + + it('returns calculated times when selectedZoomLevel has hours', () => { + const selectedZoomLevel = {hours: 3, minutes: 0}; + + const result = getZoomTimes(selectedZoomLevel); + + const expectedStart = '06:52:31'; + const expectedEnd = '09:52:31'; + + expect(result.start.format('HH:mm:ss')).toEqual(expectedStart); + expect(result.end.format('HH:mm:ss')).toEqual(expectedEnd); + }); + + it('returns calculated times when selectedZoomLevel has hours and minutes', () => { + const selectedZoomLevel = {hours: 3, minutes: 30}; + + const result = getZoomTimes(selectedZoomLevel); + + const expectedStart = '06:37:31'; + const expectedEnd = '10:07:31'; + + expect(result.start.format('HH:mm:ss')).toEqual(expectedStart); + expect(result.end.format('HH:mm:ss')).toEqual(expectedEnd); + }); + + it('handles invalid selectedZoomLevel', () => { + const selectedZoomLevel = {hours: 2}; // Missing 'minutes' + console.error = jest.fn(); + + const result = getZoomTimes(selectedZoomLevel); + + expect(result).toEqual({}); + expect(console.error).toHaveBeenCalledWith( + "Couldn't zoom the time line because the selectedZoomLevel is invalid:", + selectedZoomLevel + ); + }); +}); + +describe('moveTimeline', () => { + let dateNowSpy + const mockVisibleStartTime = moment('10:00:00', 'HH:mm:ss'); + const mockVisibleEndTime = moment('12:00:00', 'HH:mm:ss'); + + const setVisibleStartTime = jest.fn(callback => callback(mockVisibleStartTime)); + const setVisibleEndTime = jest.fn(callback => callback(mockVisibleEndTime)); + + beforeEach(() => { + //needs to be a timestamp in milliseconds for moment methods to work + //1692087751000 === Tuesday, August 15, 2023 8:22:31 + const timeStamp = 1692087751000 + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => timeStamp); + }); + + afterEach(() => { + dateNowSpy.mockRestore(); + }); + + it('moves timeline forward by adding 30 minutes', () => { + moveTimeline(setVisibleStartTime, setVisibleEndTime, 30); + + expect(setVisibleStartTime).toHaveBeenCalledWith(expect.any(Function)); + expect(setVisibleEndTime).toHaveBeenCalledWith(expect.any(Function)); + + const newStartTimeUpdater = setVisibleStartTime.mock.calls[0][0]; + const newEndTimeUpdater = setVisibleEndTime.mock.calls[0][0]; + + const newStartTime = newStartTimeUpdater(mockVisibleStartTime); + const newEndTime = newEndTimeUpdater(mockVisibleEndTime); + + expect(newStartTime.format('HH:mm:ss')).toEqual('10:30:00'); + expect(newEndTime.format('HH:mm:ss')).toEqual('12:30:00'); + }); + + + it('moves timeline backward by subtracting 15 minutes', () => { + moveTimeline(setVisibleStartTime, setVisibleEndTime, -15); + + expect(setVisibleStartTime).toHaveBeenCalledWith(expect.any(Function)); + expect(setVisibleEndTime).toHaveBeenCalledWith(expect.any(Function)); + + const newStartTimeUpdater = setVisibleStartTime.mock.calls[0][0]; + const newEndTimeUpdater = setVisibleEndTime.mock.calls[0][0]; + + const newStartTime = newStartTimeUpdater(mockVisibleStartTime); + const newEndTime = newEndTimeUpdater(mockVisibleEndTime); + + expect(newStartTime.format('HH:mm:ss')).toEqual('09:45:00'); + expect(newEndTime.format('HH:mm:ss')).toEqual('11:45:00'); + }); +}); + +describe('getMovePossibilities', () => { + it('returns move possibilities as false for invalid times', () => { + const movePossibilities = getMovePossibilities(null, null); + expect(movePossibilities).toEqual({moveLeft: false, moveRight: false}); + }); + + it('returns move possibilities as true when both times are within bounds', () => { + const visibleStartTime = moment('10:00:00', 'HH:mm:ss'); + const visibleEndTime = moment('15:00:00', 'HH:mm:ss'); + + const movePossibilities = getMovePossibilities(visibleStartTime, visibleEndTime); + expect(movePossibilities).toEqual({moveLeft: true, moveRight: true}); + }); + + it('returns move left as false when visibleStartTime is at the minimum', () => { + const visibleStartTime = moment('00:00:00', 'HH:mm:ss'); + const visibleEndTime = moment('15:00:00', 'HH:mm:ss'); + + const movePossibilities = getMovePossibilities(visibleStartTime, visibleEndTime); + expect(movePossibilities).toEqual({moveLeft: false, moveRight: true}); + }); + + it('returns move right as false when visibleEndTime is at the maximum', () => { + const visibleStartTime = moment('10:00:00', 'HH:mm:ss'); + const visibleEndTime = moment('23:59:59', 'HH:mm:ss'); + + const movePossibilities = getMovePossibilities(visibleStartTime, visibleEndTime); + expect(movePossibilities).toEqual({moveLeft: true, moveRight: false}); + }); + + it('returns move possibilities as false when both times are at extrema', () => { + const visibleStartTime = moment('00:00:00', 'HH:mm:ss'); + const visibleEndTime = moment('23:59:59', 'HH:mm:ss'); + + const movePossibilities = getMovePossibilities(visibleStartTime, visibleEndTime); + expect(movePossibilities).toEqual({moveLeft: false, moveRight: false}); + }); +}); + +describe('getZoomPossibilities', () => { + it('returns zoom possibilities as false for invalid zoomLevelIndex', () => { + const zoomLevelIndex = -1; + const zoomPossibilities = getZoomPossibilities(zoomLevelIndex); + expect(zoomPossibilities).toEqual({canZoomIn: false, canZoomOut: false}); + }); + + it('returns zoom out as false when zoomLevelIndex is at largest zoom level', () => { + const zoomLevelIndex = UIConstants.ALL_ZOOM_LEVELS_WEEK.length - 1; + const zoomPossibilities = getZoomPossibilities(zoomLevelIndex); + expect(zoomPossibilities).toEqual({canZoomIn: true, canZoomOut: false}); + }); + + it('returns zoom in as false when zoomLevelIndex is at smallest zoom level', () => { + const zoomLevelIndex = 0; + const zoomPossibilities = getZoomPossibilities(zoomLevelIndex); + expect(zoomPossibilities).toEqual({canZoomIn: false, canZoomOut: true}); + }); + + it('returns zoom possibilities when zoomLevelIndex is within bounds', () => { + const zoomLevelIndex = 2; + const zoomPossibilities = getZoomPossibilities(zoomLevelIndex); + expect(zoomPossibilities).toEqual({canZoomIn: true, canZoomOut: true}); + }); +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js index 7f6d47cabd865259a333c970a94a1ffbeef1e66c..837f81ca6bb24bbc9958461605b2a737afb81eec 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js @@ -1,19 +1,54 @@ const UIConstants = { - tooltipOptions: {position: 'left', event: 'hover', className:"p-tooltip-custom"}, + tooltipOptions: {position: 'left', event: 'hover', className: "p-tooltip-custom"}, timeline: { - types: { NORMAL: "NORMAL", WEEKVIEW:"WEEKVIEW"} + types: {NORMAL: "NORMAL", WEEKVIEW: "WEEKVIEW"} }, httpStatusMessages: { - 400: {severity: 'error', summary: 'Error', sticky: true, detail: 'Request data may be incorrect. Please try again or contact system admin'}, - 401: {severity: 'error', summary: 'Error', sticky: true, detail: 'Not authenticated, please login with valid credential'}, - 403: {severity: 'error', summary: 'Error', sticky: true, detail: "You don't have permissions to this action, please contact system admin"}, - 404: {severity: 'error', summary: 'Error', sticky: true, detail: 'URL is not recognized, please contact system admin'}, - 408: {severity: 'error', summary: 'Error', sticky: true, detail: 'Request is taking more time to response, please try again or contact system admin'}, - 500: {severity: 'error', summary: 'Error', sticky: true, detail: 'Server could not process the request, please check the data submitted is correct or contact system admin'}, - 503: {severity: 'error', summary: 'Error', sticky: true, detail: 'Server is not available, please try again or contact system admin'}, + 400: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'Request data may be incorrect. Please try again or contact system admin' + }, + 401: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'Not authenticated, please login with valid credential' + }, + 403: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: "You don't have permissions to this action, please contact system admin" + }, + 404: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'URL is not recognized, please contact system admin' + }, + 408: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'Request is taking more time to response, please try again or contact system admin' + }, + 500: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'Server could not process the request, please check the data submitted is correct or contact system admin' + }, + 503: { + severity: 'error', + summary: 'Error', + sticky: true, + detail: 'Server is not available, please try again or contact system admin' + }, }, CALENDAR_DATE_FORMAT: 'yy-mm-dd', - CALENDAR_DATETIME_FORMAT : 'YYYY-MM-DD HH:mm:ss', + CALENDAR_DATETIME_FORMAT: 'YYYY-MM-DD HH:mm:ss', CALENDAR_TIME_FORMAT: 'HH:mm:ss', CALENDAR_DEFAULTDATE_FORMAT: 'YYYY-MM-DD', UTC_DATE_TIME_FORMAT: "YYYY-MM-DDTHH:mm:ss", @@ -21,20 +56,42 @@ const UIConstants = { CALENDAR_GROUP_FORMAT: "MMM DD - ddd", FILTER_MAP: { 'AutoField': '', - 'CharFilter':'', - 'ForeignKey':'', - 'DateTimeFilter':'fromdatetime', - 'BooleanFilter':'switch', - 'ModelChoiceFilter':'select', - 'NumberFilter':'', - 'PropertyIsoDateTimeFromToRangeFilter':'', - }, - SU_STATUS:['cancelled', 'error', 'defining', 'defined', 'schedulable', 'scheduled','started', 'observing', 'observed', 'processing', 'processed', 'ingesting','finished', 'unschedulable'], - CURRENT_WORKFLOW_STAGE:['Waiting To Be Scheduled','Scheduled','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingesting','Unpin Data','Done'], - SU_PRIORITY_QUEUE:['A','B'], - TARGET_OBSERVATION_NAMES: ['target observation','parallel calibrator target and beamforming observation', - 'parallel target and beamforming observation','parallel calibrator target observation', 'beamforming observation'], + 'CharFilter': '', + 'ForeignKey': '', + 'DateTimeFilter': 'fromdatetime', + 'BooleanFilter': 'switch', + 'ModelChoiceFilter': 'select', + 'NumberFilter': '', + 'PropertyIsoDateTimeFromToRangeFilter': '', + }, + SU_STATUS: ['cancelled', 'error', 'defining', 'defined', 'schedulable', 'scheduled', 'started', 'observing', 'observed', 'processing', 'processed', 'ingesting', 'finished', 'unschedulable'], + CURRENT_WORKFLOW_STAGE: ['Waiting To Be Scheduled', 'Scheduled', 'QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance', 'Ingesting', 'Unpin Data', 'Done'], + SU_PRIORITY_QUEUE: ['A', 'B'], + TARGET_OBSERVATION_NAMES: ['target observation', 'parallel calibrator target and beamforming observation', + 'parallel target and beamforming observation', 'parallel calibrator target observation', 'beamforming observation'], SU_NOT_STARTED_STATUSES: ['defined', 'schedulable', 'scheduled', 'unschedulable'], SU_ACTIVE_STATUSES: ['started', 'observing', 'observed', 'processing', 'processed', 'ingesting', 'ingested'], + STORE_KEY_TIMELINE: "TIMELINE_UI_ATTR", + ALL_ZOOM_LEVELS_WEEK: [{name: '30 Minutes', days: 0, hours: 0, minutes: 30}, + {name: '1 Hour', days: 0, hours: 1, minutes: 0}, + {name: '3 Hours', days: 0, hours: 3, minutes: 0}, + {name: '6 Hours', days: 0, hours: 6, minutes: 0}, + {name: '12 Hours', days: 0, hours: 12, minutes: 0}, + {name: '1 Day', days: 1, hours: 0, minutes: 0}], + ALL_ZOOM_LEVELS: [{name: '30 Minutes', days: 0, hours: 0, minutes: 30}, + {name: '1 Hour', days: 0, hours: 1, minutes: 0}, + {name: '3 Hours', days: 0, hours: 3, minutes: 0}, + {name: '6 Hours', days: 0, hours: 6, minutes: 0}, + {name: '12 Hours', days: 0, hours: 12, minutes: 0}, + {name: '1 Day', days: 1, hours: 0, minutes: 0}, + {name: '2 Days', value: 2 * 24 * 60 * 60}, + {name: '3 Days', value: 3 * 24 * 60 * 60}, + {name: '5 Days', value: 5 * 24 * 60 * 60}, + {name: '5 Days', value: 5 * 24 * 60 * 60}, + {name: '1 Week', value: 7 * 24 * 60 * 60}, + {name: '2 Weeks', value: 14 * 24 * 60 * 60}, + {name: '4 Weeks', value: 28 * 24 * 60 * 60}, + {name: 'Custom', value: 24 * 60 * 60}], + DEFAULT_ZOOM_LEVEL: {name: '1 Day', days: 1, hours: 0, minutes: 0} } export default UIConstants; \ No newline at end of file