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