diff --git a/frontend/src/metabase-types/api/mocks/timeline.ts b/frontend/src/metabase-types/api/mocks/timeline.ts
index 4f794d4c5b1073954fa82823ca96b7ae26b85661..809eee1107cbead675b89a25d5743f91705edc54 100644
--- a/frontend/src/metabase-types/api/mocks/timeline.ts
+++ b/frontend/src/metabase-types/api/mocks/timeline.ts
@@ -20,7 +20,7 @@ export const createMockTimelineEvent = (
   name: "Christmas",
   description: null,
   icon: "star",
-  timestamp: "2021-12-25T14:48:00.000Z",
+  timestamp: "2021-12-25T00:00:00Z",
   timezone: "UTC",
   time_matters: false,
   creator: createMockUser(),
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
index 5ef293656717ea7477c818345dfd27c352ce7f49..0f7fa860b8423622f6e5b17b6e0908d2d381a1e1 100644
--- a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
@@ -70,11 +70,11 @@ function PermissionsLink({
 }
 
 function TimelinesLink({ collection }) {
-  const tooltip = t`Events`;
+  const title = t`Events`;
   const link = Urls.timelinesInCollection(collection);
 
   return (
-    <Tooltip tooltip={tooltip}>
+    <Tooltip tooltip={title}>
       <Link to={link}>
         <IconWrapper>
           <Icon name="calendar" size={20} />
diff --git a/frontend/src/metabase/components/Modal.jsx b/frontend/src/metabase/components/Modal.jsx
index aa1a24a11ff992d1276a160a0df26d3d61ead4ca..edde2f125a6684ee86eb56c9a827cb02d771ac47 100644
--- a/frontend/src/metabase/components/Modal.jsx
+++ b/frontend/src/metabase/components/Modal.jsx
@@ -30,11 +30,13 @@ export class WindowModal extends Component {
   static propTypes = {
     isOpen: PropTypes.bool,
     enableMouseEvents: PropTypes.bool,
+    enableTransition: PropTypes.bool,
   };
 
   static defaultProps = {
     className: "Modal",
     backdropClassName: "Modal-backdrop",
+    enableTransition: true,
   };
 
   constructor(props) {
@@ -81,7 +83,13 @@ export class WindowModal extends Component {
   }
 
   render() {
-    const { enableMouseEvents, backdropClassName, isOpen, style } = this.props;
+    const {
+      enableMouseEvents,
+      backdropClassName,
+      isOpen,
+      style,
+      enableTransition,
+    } = this.props;
     const backdropClassnames =
       "flex justify-center align-center fixed top left bottom right";
 
@@ -92,9 +100,11 @@ export class WindowModal extends Component {
       >
         <CSSTransitionGroup
           transitionName="Modal"
-          transitionAppear={true}
+          transitionAppear={enableTransition}
           transitionAppearTimeout={250}
+          transitionEnter={enableTransition}
           transitionEnterTimeout={250}
+          transitionLeave={enableTransition}
           transitionLeaveTimeout={250}
         >
           {isOpen && (
diff --git a/frontend/src/metabase/core/components/DateSelector/DateSelector.tsx b/frontend/src/metabase/core/components/DateSelector/DateSelector.tsx
index b91527f45b31e72c6f498c132e44031f86e58fad..b671485255f1223dbfffbcc15565e8bb8fc69ae9 100644
--- a/frontend/src/metabase/core/components/DateSelector/DateSelector.tsx
+++ b/frontend/src/metabase/core/components/DateSelector/DateSelector.tsx
@@ -45,13 +45,9 @@ const DateSelector = forwardRef(function DateSelector(
 
   const handleDateChange = useCallback(
     (unused1: string, unused2: string, date: Moment) => {
-      const newDate = moment({
-        year: date.year(),
-        month: date.month(),
-        day: date.day(),
-        hours: value?.hours(),
-        minutes: value?.minutes(),
-      });
+      const newDate = date.clone();
+      newDate.hours(value?.hours() ?? 0);
+      newDate.minutes(value?.minutes() ?? 0);
       onChange?.(newDate);
     },
     [value, onChange],
@@ -96,7 +92,7 @@ const DateSelector = forwardRef(function DateSelector(
           </SelectorTimeButton>
         )}
         <SelectorSubmitButton primary onClick={onSubmit}>
-          {t`Save`}
+          {t`Done`}
         </SelectorSubmitButton>
       </SelectorFooter>
     </div>
diff --git a/frontend/src/metabase/core/components/Input/Input.styled.tsx b/frontend/src/metabase/core/components/Input/Input.styled.tsx
index eb7dec8296d56cc07c174438166b8fc96b7f15a5..3896e2fd096f0647ab1d11d5faf467a0d8b6093a 100644
--- a/frontend/src/metabase/core/components/Input/Input.styled.tsx
+++ b/frontend/src/metabase/core/components/Input/Input.styled.tsx
@@ -62,13 +62,14 @@ export const InputField = styled.input<InputProps>`
 export const InputButton = styled(IconButtonWrapper)`
   position: absolute;
   color: ${color("text-light")};
+  padding: 0.75rem;
   border-radius: 50%;
 `;
 
 export const InputLeftButton = styled(InputButton)`
-  left: 0.75rem;
+  left: 0;
 `;
 
 export const InputRightButton = styled(InputButton)`
-  right: 0.75rem;
+  right: 0;
 `;
diff --git a/frontend/src/metabase/entities/containers/EntityObjectLoader.jsx b/frontend/src/metabase/entities/containers/EntityObjectLoader.jsx
index d86bd14a90c666306d929a3bd1bdab5f8d81e4ac..d8e72a234f9e15ad6ec37578e900071a75a60869 100644
--- a/frontend/src/metabase/entities/containers/EntityObjectLoader.jsx
+++ b/frontend/src/metabase/entities/containers/EntityObjectLoader.jsx
@@ -18,6 +18,7 @@ const CONSUMED_PROPS = [
   "wrapped",
   "properties",
   "loadingAndErrorWrapper",
+  "LoadingAndErrorWrapper",
   "selectorName",
 ];
 
@@ -54,6 +55,7 @@ const getMemoizedEntityQuery = createMemoizedSelector(
 export default class EntityObjectLoader extends React.Component {
   static defaultProps = {
     loadingAndErrorWrapper: true,
+    LoadingAndErrorWrapper: LoadingAndErrorWrapper,
     reload: false,
     wrapped: false,
     dispatchApiErrorEvent: true,
@@ -123,7 +125,14 @@ export default class EntityObjectLoader extends React.Component {
     });
   };
   render() {
-    const { entityId, fetched, error, loadingAndErrorWrapper } = this.props;
+    const {
+      entityId,
+      fetched,
+      error,
+      loadingAndErrorWrapper,
+      LoadingAndErrorWrapper,
+    } = this.props;
+
     return loadingAndErrorWrapper ? (
       <LoadingAndErrorWrapper
         loading={!fetched && entityId != null}
diff --git a/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.tsx b/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.tsx
index ef949c329fbb67a9570cefea6584aeac7c537954..b2f0f32cd4080bae81fd122cd339b692a27a7a14 100644
--- a/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.tsx
+++ b/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.tsx
@@ -9,20 +9,26 @@ export interface DeleteEventModalProps {
   event: TimelineEvent;
   timeline: Timeline;
   collection: Collection;
-  onSubmit: (event: TimelineEvent) => void;
+  onSubmit: (
+    event: TimelineEvent,
+    timeline: Timeline,
+    collection: Collection,
+  ) => void;
   onCancel: () => void;
   onClose?: () => void;
 }
 
 const DeleteEventModal = ({
   event,
+  timeline,
+  collection,
   onSubmit,
   onCancel,
   onClose,
 }: DeleteEventModalProps): JSX.Element => {
   const handleSubmit = useCallback(async () => {
-    await onSubmit(event);
-  }, [event, onSubmit]);
+    await onSubmit(event, timeline, collection);
+  }, [event, timeline, collection, onSubmit]);
 
   return (
     <div>
diff --git a/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..51200403351bb8519fee9c085c6046eff2dfd79a
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import DeleteEventModal, { DeleteEventModalProps } from "./DeleteEventModal";
+
+describe("DeleteEventModal", () => {
+  it("should submit modal", () => {
+    const props = getProps();
+
+    render(<DeleteEventModal {...props} />);
+    userEvent.click(screen.getByText("Delete"));
+
+    expect(props.onSubmit).toHaveBeenCalled();
+  });
+});
+
+const getProps = (
+  opts?: Partial<DeleteEventModalProps>,
+): DeleteEventModalProps => ({
+  event: createMockTimelineEvent(),
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  onSubmit: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.tsx
index 46773da0e65a9f8a87890d3f63d424ffc59e3aee..6de1781a76baa9a00627d6881226f9bc1165c690 100644
--- a/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.tsx
@@ -2,20 +2,32 @@ import React, { useCallback } from "react";
 import { t } from "ttag";
 import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timeline-events/forms";
-import { TimelineEvent } from "metabase-types/api";
+import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import ModalHeader from "../ModalHeader";
 import { ModalBody, ModalDangerButton } from "./EditEventModal.styled";
 
 export interface EditEventModalProps {
   event: TimelineEvent;
-  onSubmit: (values: Partial<TimelineEvent>) => void;
-  onArchive: (event: TimelineEvent) => void;
+  timeline: Timeline;
+  collection: Collection;
+  onSubmit: (
+    values: Partial<TimelineEvent>,
+    timeline: Timeline,
+    collection: Collection,
+  ) => void;
+  onArchive: (
+    event: TimelineEvent,
+    timeline: Timeline,
+    collection: Collection,
+  ) => void;
   onCancel: () => void;
   onClose?: () => void;
 }
 
 const EditEventModal = ({
   event,
+  timeline,
+  collection,
   onSubmit,
   onArchive,
   onCancel,
@@ -23,14 +35,14 @@ const EditEventModal = ({
 }: EditEventModalProps): JSX.Element => {
   const handleSubmit = useCallback(
     async (values: Partial<TimelineEvent>) => {
-      await onSubmit(values);
+      await onSubmit(values, timeline, collection);
     },
-    [onSubmit],
+    [timeline, collection, onSubmit],
   );
 
   const handleArchive = useCallback(async () => {
-    await onArchive(event);
-  }, [event, onArchive]);
+    await onArchive(event, timeline, collection);
+  }, [event, timeline, collection, onArchive]);
 
   return (
     <div>
diff --git a/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..961f8c0b65e69e3c95c97f2b75ed842001e26f6d
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/EditEventModal/EditEventModal.unit.spec.tsx
@@ -0,0 +1,41 @@
+import React, { FormHTMLAttributes } from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import EditEventModal, { EditEventModalProps } from "./EditEventModal";
+
+const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
+  <form {...props}>
+    <button>Update</button>
+  </form>
+);
+
+jest.mock("metabase/containers/Form", () => FormMock);
+
+describe("EditEventModal", () => {
+  it("should submit modal", () => {
+    const props = getProps();
+
+    render(<EditEventModal {...props} />);
+    userEvent.click(screen.getByText("Update"));
+
+    expect(props.onSubmit).toHaveBeenCalled();
+  });
+});
+
+const getProps = (
+  opts?: Partial<EditEventModalProps>,
+): EditEventModalProps => ({
+  event: createMockTimelineEvent(),
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  onSubmit: jest.fn(),
+  onArchive: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.tsx b/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.tsx
index 47f1d3e51daab81a41ffb9a7afc652bc2195eb9c..8d93f0873ad9920207e82bbc833ed4b92b210906 100644
--- a/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.tsx
@@ -2,20 +2,22 @@ import React, { useCallback } from "react";
 import { t } from "ttag";
 import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timelines/forms";
-import { Timeline } from "metabase-types/api";
+import { Collection, Timeline } from "metabase-types/api";
 import ModalHeader from "../ModalHeader";
-import { ModalBody, ModalDangerButton } from "./EditTimelineModal.styled";
+import { ModalDangerButton, ModalBody } from "./EditTimelineModal.styled";
 
 export interface EditTimelineModalProps {
   timeline: Timeline;
-  onSubmit: (values: Partial<Timeline>) => void;
-  onArchive: (timeline: Timeline) => void;
+  collection: Collection;
+  onSubmit: (values: Partial<Timeline>, collection: Collection) => void;
+  onArchive: (timeline: Timeline, collection: Collection) => void;
   onCancel: () => void;
   onClose?: () => void;
 }
 
 const EditTimelineModal = ({
   timeline,
+  collection,
   onSubmit,
   onArchive,
   onCancel,
@@ -23,14 +25,14 @@ const EditTimelineModal = ({
 }: EditTimelineModalProps): JSX.Element => {
   const handleSubmit = useCallback(
     async (values: Partial<Timeline>) => {
-      await onSubmit(values);
+      await onSubmit(values, collection);
     },
-    [onSubmit],
+    [collection, onSubmit],
   );
 
   const handleArchive = useCallback(async () => {
-    await onArchive(timeline);
-  }, [timeline, onArchive]);
+    await onArchive(timeline, collection);
+  }, [timeline, collection, onArchive]);
 
   return (
     <div>
diff --git a/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9ad1d2338656c9b41c230dac5a8ea452ede070ff
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
@@ -0,0 +1,39 @@
+import React, { FormHTMLAttributes } from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {
+  createMockCollection,
+  createMockTimeline,
+} from "metabase-types/api/mocks";
+import EditTimelineModal, { EditTimelineModalProps } from "./EditTimelineModal";
+
+const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
+  <form {...props}>
+    <button>Update</button>
+  </form>
+);
+
+jest.mock("metabase/containers/Form", () => FormMock);
+
+describe("EditTimelineModal", () => {
+  it("should submit modal", () => {
+    const props = getProps();
+
+    render(<EditTimelineModal {...props} />);
+    userEvent.click(screen.getByText("Update"));
+
+    expect(props.onSubmit).toHaveBeenCalled();
+  });
+});
+
+const getProps = (
+  opts?: Partial<EditTimelineModalProps>,
+): EditTimelineModalProps => ({
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  onSubmit: jest.fn(),
+  onArchive: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/EventCard/EventCard.styled.tsx b/frontend/src/metabase/timelines/components/EventCard/EventCard.styled.tsx
index ecd3d5583f8fdd9b916b75a5506100d9e07baf13..a44ac1051e125dc9f4945169a1c97a8037c8992f 100644
--- a/frontend/src/metabase/timelines/components/EventCard/EventCard.styled.tsx
+++ b/frontend/src/metabase/timelines/components/EventCard/EventCard.styled.tsx
@@ -38,7 +38,7 @@ export const CardThreadStroke = styled.div`
 
 export const CardBody = styled.div`
   flex: 1 1 auto;
-  padding: 0.25rem 0.75rem 0;
+  padding: 0.25rem 0.75rem 0.5rem;
 `;
 
 export const CardTitle = styled.div`
diff --git a/frontend/src/metabase/timelines/components/EventCard/EventCard.unit.spec.tsx b/frontend/src/metabase/timelines/components/EventCard/EventCard.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dff8115b4fafdd5b927e584a646c6babac862c33
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/EventCard/EventCard.unit.spec.tsx
@@ -0,0 +1,107 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+  createMockUser,
+} from "metabase-types/api/mocks";
+import EventCard, { EventCardProps } from "./EventCard";
+import userEvent from "@testing-library/user-event";
+
+describe("EventCard", () => {
+  it("should format a day-only event", () => {
+    const props = getProps({
+      event: createMockTimelineEvent({
+        timestamp: "2020-12-20T00:00:00Z",
+        time_matters: false,
+      }),
+    });
+
+    render(<EventCard {...props} />);
+
+    expect(screen.getByText("December 20, 2020")).toBeInTheDocument();
+  });
+
+  it("should format a time-sensitive event", () => {
+    const props = getProps({
+      event: createMockTimelineEvent({
+        timestamp: "2020-12-20T10:00:00Z",
+        time_matters: true,
+      }),
+    });
+
+    render(<EventCard {...props} />);
+
+    expect(screen.getByText("December 20, 2020, 10:00 AM")).toBeInTheDocument();
+  });
+
+  it("should format an event with the user who created the event", () => {
+    const props = getProps({
+      event: createMockTimelineEvent({
+        creator: createMockUser({
+          common_name: "Testy Test",
+        }),
+        created_at: "2020-12-20T10:00:00Z",
+      }),
+    });
+
+    render(<EventCard {...props} />);
+
+    expect(
+      screen.getByText("Testy Test added this on December 20, 2020"),
+    ).toBeInTheDocument();
+  });
+
+  it("should not render the menu for read-only users", () => {
+    const props = getProps({
+      collection: createMockCollection({
+        can_write: false,
+      }),
+    });
+
+    render(<EventCard {...props} />);
+
+    expect(screen.queryByLabelText("ellipsis icon")).not.toBeInTheDocument();
+  });
+
+  it("should render the menu for users with write permissions", async () => {
+    const props = getProps({
+      collection: createMockCollection({
+        can_write: true,
+      }),
+    });
+
+    render(<EventCard {...props} />);
+    userEvent.click(screen.getByLabelText("ellipsis icon"));
+
+    expect(screen.getByText("Edit event")).toBeInTheDocument();
+    expect(screen.getByText("Archive event")).toBeInTheDocument();
+  });
+
+  it("should render the menu for an archived event", () => {
+    const props = getProps({
+      event: createMockTimelineEvent({
+        archived: true,
+      }),
+      collection: createMockCollection({
+        can_write: true,
+      }),
+    });
+
+    render(<EventCard {...props} />);
+    userEvent.click(screen.getByLabelText("ellipsis icon"));
+
+    expect(screen.getByText("Unarchive event")).toBeInTheDocument();
+    expect(screen.getByText("Delete event")).toBeInTheDocument();
+  });
+});
+
+export const getProps = (opts?: Partial<EventCardProps>): EventCardProps => ({
+  event: createMockTimelineEvent(),
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  onArchive: jest.fn(),
+  onUnarchive: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/EventEmptyState/EventEmptyState.unit.spec.tsx b/frontend/src/metabase/timelines/components/EventEmptyState/EventEmptyState.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fb2c32dc1b292519280ac87202dcbe4ad88e7942
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/EventEmptyState/EventEmptyState.unit.spec.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import EventEmptyState from "./EventEmptyState";
+
+describe("EventEmptyState", () => {
+  it("should render correctly", () => {
+    render(<EventEmptyState />);
+
+    expect(screen.getByLabelText("star icon")).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/timelines/components/EventList/EventList.unit.spec.tsx b/frontend/src/metabase/timelines/components/EventList/EventList.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..98b5e54b5941bdf26183f709daa359b5c7266f51
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/EventList/EventList.unit.spec.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import EventList, { EventListProps } from "./EventList";
+
+describe("EventList", () => {
+  it("should render a list of events", () => {
+    const props = getProps({
+      events: [
+        createMockTimelineEvent({ id: 1, name: "RC1" }),
+        createMockTimelineEvent({ id: 2, name: "RC2" }),
+      ],
+    });
+
+    render(<EventList {...props} />);
+
+    expect(screen.getByText("RC1")).toBeInTheDocument();
+    expect(screen.getByText("RC2")).toBeInTheDocument();
+  });
+});
+
+const getProps = (opts?: Partial<EventListProps>): EventListProps => ({
+  events: [],
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.styled.tsx b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e23d7928a62c34b178717df9b1eead350292f2a3
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.styled.tsx
@@ -0,0 +1,8 @@
+import styled from "@emotion/styled";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+
+export const LoadingAndErrorRoot = styled(LoadingAndErrorWrapper)`
+  display: flex;
+  flex-direction: column;
+  min-height: 565px;
+`;
diff --git a/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.tsx b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4d96caadda5a60bc4eeec326816b95dd33633409
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper.tsx
@@ -0,0 +1,2 @@
+import { LoadingAndErrorRoot } from "./LoadingAndErrorWrapper.styled";
+export default LoadingAndErrorRoot;
diff --git a/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/index.ts b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bace309e44669f10bfb20949c7daf82adc0804b0
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/LoadingAndErrorWrapper/index.ts
@@ -0,0 +1 @@
+export { default } from "./LoadingAndErrorWrapper";
diff --git a/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.styled.tsx b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.styled.tsx
index a98c3f212339d1e09c0247454a66e5cc757d371d..c4d330404fa29786c4ced5434aee722c603057c8 100644
--- a/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.styled.tsx
+++ b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.styled.tsx
@@ -21,6 +21,17 @@ export const HeaderActions = styled.div`
   margin-right: 1rem;
 `;
 
+export const HeaderBackButton = styled(IconButtonWrapper)`
+  flex: 0 0 auto;
+  color: ${color("text-dark")};
+  margin: 0 0.5rem 0 0;
+  padding: 0.25rem 0;
+
+  &:hover {
+    color: ${color("brand")};
+  }
+`;
+
 export const HeaderCloseButton = styled(IconButtonWrapper)`
   flex: 0 0 auto;
   color: ${color("text-light")};
diff --git a/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.tsx b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.tsx
index 84cac5f598c4ab4df9dbdb509210160e81192ed5..8744407f87b52832d068b7f7de93e5bcb11e33b3 100644
--- a/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.tsx
+++ b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.tsx
@@ -2,6 +2,7 @@ import React, { ReactNode } from "react";
 import Icon from "metabase/components/Icon";
 import {
   HeaderActions,
+  HeaderBackButton,
   HeaderCloseButton,
   HeaderRoot,
   HeaderTitle,
@@ -11,15 +12,22 @@ export interface ModalHeaderProps {
   title?: ReactNode;
   children?: ReactNode;
   onClose?: () => void;
+  onGoBack?: () => void;
 }
 
 const ModalHeader = ({
   title,
   children,
   onClose,
+  onGoBack,
 }: ModalHeaderProps): JSX.Element => {
   return (
     <HeaderRoot>
+      {onGoBack && (
+        <HeaderBackButton onClick={onGoBack}>
+          <Icon name="chevronleft" />
+        </HeaderBackButton>
+      )}
       <HeaderTitle>{title}</HeaderTitle>
       {children && <HeaderActions>{children}</HeaderActions>}
       {onClose && (
diff --git a/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.unit.spec.tsx b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3e6cdedc52a5025fac8d5e8eec0bfa4cc22600a6
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/ModalHeader/ModalHeader.unit.spec.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import ModalHeader from "./ModalHeader";
+
+describe("ModalHeader", () => {
+  it("should render title", () => {
+    render(<ModalHeader title="Events" />);
+
+    expect(screen.getByText("Events")).toBeInTheDocument();
+  });
+
+  it("should render with actions", () => {
+    render(<ModalHeader title="Events">Actions</ModalHeader>);
+
+    expect(screen.getByText("Actions")).toBeInTheDocument();
+  });
+
+  it("should render with close button", () => {
+    const onClose = jest.fn();
+
+    render(<ModalHeader title="Events" onClose={onClose} />);
+
+    expect(screen.getByLabelText("close icon")).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.tsx
index 43f7d390512e7b819393572368fd4e46ae686772..8e519cd45ff122a7212b2033ea9a9007732c774b 100644
--- a/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.tsx
@@ -10,7 +10,11 @@ import { ModalBody } from "./NewEventModal.styled";
 export interface NewEventModalProps {
   timeline?: Timeline;
   collection: Collection;
-  onSubmit: (values: Partial<TimelineEvent>, collection: Collection) => void;
+  onSubmit: (
+    values: Partial<TimelineEvent>,
+    collection: Collection,
+    timeline?: Timeline,
+  ) => void;
   onCancel: (location: string) => void;
   onClose?: () => void;
 }
@@ -33,9 +37,9 @@ const NewEventModal = ({
 
   const handleSubmit = useCallback(
     async (values: Partial<TimelineEvent>) => {
-      await onSubmit(values, collection);
+      await onSubmit(values, collection, timeline);
     },
-    [collection, onSubmit],
+    [timeline, collection, onSubmit],
   );
 
   return (
diff --git a/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d943ba636aca1847ac4b845def9185f75cceee4
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/NewEventModal/NewEventModal.unit.spec.tsx
@@ -0,0 +1,32 @@
+import React, { FormHTMLAttributes } from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { createMockCollection } from "metabase-types/api/mocks";
+import NewEventModal, { NewEventModalProps } from "./NewEventModal";
+
+const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
+  <form {...props}>
+    <button>Create</button>
+  </form>
+);
+
+jest.mock("metabase/containers/Form", () => FormMock);
+
+describe("NewEventModal", () => {
+  it("should submit modal", () => {
+    const props = getProps();
+
+    render(<NewEventModal {...props} />);
+    userEvent.click(screen.getByText("Create"));
+
+    expect(props.onSubmit).toHaveBeenCalled();
+  });
+});
+
+const getProps = (opts?: Partial<NewEventModalProps>): NewEventModalProps => ({
+  collection: createMockCollection(),
+  onSubmit: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5cb844e46dcc434039810a3d44b5e6238052fabf
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx
@@ -0,0 +1,34 @@
+import React, { FormHTMLAttributes } from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { createMockCollection } from "metabase-types/api/mocks";
+import NewTimelineModal, { NewTimelineModalProps } from "./NewTimelineModal";
+
+const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
+  <form {...props}>
+    <button>Create</button>
+  </form>
+);
+
+jest.mock("metabase/containers/Form", () => FormMock);
+
+describe("NewTimelineModal", () => {
+  it("should submit modal", () => {
+    const props = getProps();
+
+    render(<NewTimelineModal {...props} />);
+    userEvent.click(screen.getByText("Create"));
+
+    expect(props.onSubmit).toHaveBeenCalled();
+  });
+});
+
+const getProps = (
+  opts?: Partial<NewTimelineModalProps>,
+): NewTimelineModalProps => ({
+  collection: createMockCollection(),
+  onSubmit: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/TimelineCard/TimelineCard.unit.spec.tsx b/frontend/src/metabase/timelines/components/TimelineCard/TimelineCard.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..50901b8be6c2dcc85f7bcbd60be107d9864c9e0a
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/TimelineCard/TimelineCard.unit.spec.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import TimelineCard, { TimelineCardProps } from "./TimelineCard";
+
+describe("TimelineCard", () => {
+  it("should render timeline", () => {
+    const props = getProps({
+      timeline: createMockTimeline({
+        events: [createMockTimelineEvent(), createMockTimelineEvent()],
+      }),
+    });
+
+    render(<TimelineCard {...props} />);
+
+    expect(screen.getByText("2 events")).toBeInTheDocument();
+  });
+});
+
+const getProps = (opts?: Partial<TimelineCardProps>): TimelineCardProps => ({
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
index bf51721d3e3af250192a29750142a4b3705b360d..05d427d7359ded1252044cdaa599e0a5b28df6e9 100644
--- a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.styled.tsx
@@ -5,7 +5,7 @@ import TextInput from "metabase/components/TextInput";
 export const ModalRoot = styled.div`
   display: flex;
   flex-direction: column;
-  min-height: 565px;
+  min-height: 573px;
   max-height: 90vh;
 `;
 
diff --git a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
index 17d8cbb8ce11b4c428f3b678972f59b23cf4b511..1fac9413868110c76ef9cfdd215225d1bb35c5e3 100644
--- a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from "react";
+import React, { useCallback, useMemo, useState } from "react";
 import { t } from "ttag";
 import _ from "underscore";
 import { parseTimestamp } from "metabase/lib/time";
@@ -25,18 +25,22 @@ export interface TimelineDetailsModalProps {
   timeline: Timeline;
   collection: Collection;
   isArchive?: boolean;
+  isDefault?: boolean;
   onArchive?: (event: TimelineEvent) => void;
   onUnarchive?: (event: TimelineEvent) => void;
   onClose?: () => void;
+  onGoBack?: (collection: Collection) => void;
 }
 
 const TimelineDetailsModal = ({
   timeline,
   collection,
   isArchive = false,
+  isDefault = false,
   onArchive,
   onUnarchive,
   onClose,
+  onGoBack,
 }: TimelineDetailsModalProps): JSX.Element => {
   const title = isArchive ? t`Archived events` : timeline.name;
   const [inputText, setInputText] = useState("");
@@ -54,13 +58,21 @@ const TimelineDetailsModal = ({
     return getMenuItems(timeline, collection, isArchive);
   }, [timeline, collection, isArchive]);
 
+  const handleGoBack = useCallback(() => {
+    onGoBack?.(collection);
+  }, [collection, onGoBack]);
+
   const isNotEmpty = events.length > 0;
   const isSearching = searchText.length > 0;
   const canWrite = collection.can_write;
 
   return (
     <ModalRoot>
-      <ModalHeader title={title} onClose={onClose}>
+      <ModalHeader
+        title={title}
+        onClose={onClose}
+        onGoBack={!isDefault ? handleGoBack : undefined}
+      >
         {menuItems.length > 0 && (
           <EntityMenu items={menuItems} triggerIcon="ellipsis" />
         )}
diff --git a/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f9976b1dce94dd9ef2591d2c141670e0b672a0e8
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {
+  createMockCollection,
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import TimelineDetailsModal, {
+  TimelineDetailsModalProps,
+} from "./TimelineDetailsModal";
+
+describe("TimelineDetailsModal", () => {
+  it("should search a list of events", async () => {
+    const props = getProps({
+      timeline: createMockTimeline({
+        events: [
+          createMockTimelineEvent({ name: "RC1" }),
+          createMockTimelineEvent({ name: "RC2" }),
+          createMockTimelineEvent({ name: "Release" }),
+        ],
+      }),
+    });
+
+    render(<TimelineDetailsModal {...props} />);
+
+    userEvent.type(screen.getByPlaceholderText("Search for an event"), "RC");
+    await waitFor(() => {
+      expect(screen.getByText("RC1")).toBeInTheDocument();
+      expect(screen.getByText("RC2")).toBeInTheDocument();
+      expect(screen.queryByText("Release")).not.toBeInTheDocument();
+    });
+
+    userEvent.type(screen.getByPlaceholderText("Search for an event"), "1");
+    await waitFor(() => {
+      expect(screen.getByText("RC1")).toBeInTheDocument();
+      expect(screen.queryByText("RC2")).not.toBeInTheDocument();
+    });
+  });
+});
+
+const getProps = (
+  opts?: Partial<TimelineDetailsModalProps>,
+): TimelineDetailsModalProps => ({
+  timeline: createMockTimeline(),
+  collection: createMockCollection(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx b/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..27349b6d7cf0c9adbfd02dd24bc3aa20a55a1a7c
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { createMockCollection } from "metabase-types/api/mocks";
+import TimelineEmptyState, {
+  TimelineEmptyStateProps,
+} from "./TimelineEmptyState";
+
+describe("TimelineEmptyState", () => {
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(new Date(2015, 0, 1));
+  });
+
+  afterEach(() => {
+    jest.useRealTimers();
+  });
+
+  it("should render an empty state with the current date", () => {
+    const props = getProps();
+
+    render(<TimelineEmptyState {...props} />);
+
+    expect(screen.getByText("January 1, 2015"));
+  });
+});
+
+const getProps = (
+  opts?: Partial<TimelineEmptyStateProps>,
+): TimelineEmptyStateProps => ({
+  collection: createMockCollection(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/components/TimelineEntryModal/index.ts b/frontend/src/metabase/timelines/components/TimelineEntryModal/index.ts
deleted file mode 100644
index 1e014f3eb5e111f5551353016f5d6134993b3a4c..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/timelines/components/TimelineEntryModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./TimelineEntryModal";
diff --git a/frontend/src/metabase/timelines/components/TimelineEntryModal/TimelineEntryModal.tsx b/frontend/src/metabase/timelines/components/TimelineIndexModal/TimelineIndexModal.tsx
similarity index 60%
rename from frontend/src/metabase/timelines/components/TimelineEntryModal/TimelineEntryModal.tsx
rename to frontend/src/metabase/timelines/components/TimelineIndexModal/TimelineIndexModal.tsx
index bf453657725b5021153491d64da481aac5ded7b1..07254de13a992dd9d9a5d1276f393e2177934157 100644
--- a/frontend/src/metabase/timelines/components/TimelineEntryModal/TimelineEntryModal.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineIndexModal/TimelineIndexModal.tsx
@@ -4,23 +4,28 @@ import TimelineDetailsModal from "../../containers/TimelineDetailsModal";
 import TimelineListModal from "../../containers/TimelineListModal";
 import { ModalParams } from "../../types";
 
-export interface TimelineEntryModalProps {
+export interface TimelineIndexModalProps {
   timelines: Timeline[];
   params: ModalParams;
   onClose?: () => void;
 }
 
-const TimelineEntryModal = ({
+const TimelineIndexModal = ({
   timelines,
   params,
   onClose,
-}: TimelineEntryModalProps): JSX.Element => {
+}: TimelineIndexModalProps): JSX.Element => {
   if (timelines.length === 1) {
-    const newParams = { ...params, timelineId: timelines[0].id };
-    return <TimelineDetailsModal params={newParams} onClose={onClose} />;
+    return (
+      <TimelineDetailsModal
+        params={{ ...params, timelineId: timelines[0].id }}
+        isDefault={true}
+        onClose={onClose}
+      />
+    );
   } else {
     return <TimelineListModal params={params} onClose={onClose} />;
   }
 };
 
-export default TimelineEntryModal;
+export default TimelineIndexModal;
diff --git a/frontend/src/metabase/timelines/components/TimelineIndexModal/index.ts b/frontend/src/metabase/timelines/components/TimelineIndexModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b5ab6de59d80787065f6305d36c84f859e3d88ae
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/TimelineIndexModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./TimelineIndexModal";
diff --git a/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.styled.tsx b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.styled.tsx
index 5918fe3b3a33cdf0f01f6097abb2412d244d4e1d..c371ad31f0654b0f49d1149f71eaddc8a27e97cd 100644
--- a/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.styled.tsx
+++ b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.styled.tsx
@@ -3,7 +3,7 @@ import styled from "@emotion/styled";
 export const ModalRoot = styled.div`
   display: flex;
   flex-direction: column;
-  min-height: 565px;
+  min-height: 573px;
   max-height: 90vh;
 `;
 
diff --git a/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.unit.spec.tsx b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d226634f99ee479cc4d316cef66762f8dad7aa60
--- /dev/null
+++ b/frontend/src/metabase/timelines/components/TimelineListModal/TimelineListModal.unit.spec.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import {
+  createMockCollection,
+  createMockTimeline,
+} from "metabase-types/api/mocks";
+import TimelineListModal, { TimelineListModalProps } from "./TimelineListModal";
+
+describe("TimelineListModal", () => {
+  it("should render a list of timelines", () => {
+    const props = getProps({
+      timelines: [createMockTimeline({ name: "Releases" })],
+      collection: createMockCollection({ can_write: true }),
+    });
+
+    render(<TimelineListModal {...props} />);
+
+    expect(screen.getByText("Releases")).toBeInTheDocument();
+  });
+
+  it("should render an empty state when there are no timelines", () => {
+    const props = getProps({
+      timelines: [],
+      collection: createMockCollection({ can_write: true }),
+    });
+
+    render(<TimelineListModal {...props} />);
+
+    expect(screen.getByText("Add an event")).toBeInTheDocument();
+  });
+});
+
+const getProps = (
+  opts?: Partial<TimelineListModalProps>,
+): TimelineListModalProps => ({
+  timelines: [],
+  collection: createMockCollection(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/containers/DeleteEventModal/DeleteEventModal.tsx b/frontend/src/metabase/timelines/containers/DeleteEventModal/DeleteEventModal.tsx
index b3ca052764716226554369b95dfcb61a3b7cb2e9..354bfb13671a3809330d020dbc17946056d8a262 100644
--- a/frontend/src/metabase/timelines/containers/DeleteEventModal/DeleteEventModal.tsx
+++ b/frontend/src/metabase/timelines/containers/DeleteEventModal/DeleteEventModal.tsx
@@ -1,23 +1,40 @@
 import { connect } from "react-redux";
-import { goBack } from "react-router-redux";
+import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
+import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { TimelineEvent } from "metabase-types/api";
+import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import DeleteEventModal from "../../components/DeleteEventModal";
 import { ModalProps } from "../../types";
 
+const timelineProps = {
+  id: (state: State, props: ModalProps) =>
+    Urls.extractEntityId(props.params.timelineId),
+  query: { include: "events" },
+};
+
 const timelineEventProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineEventId),
   entityAlias: "event",
 };
 
+const collectionProps = {
+  id: (state: State, props: ModalProps) =>
+    Urls.extractCollectionId(props.params.slug),
+};
+
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (event: TimelineEvent) => {
+  onSubmit: async (
+    event: TimelineEvent,
+    timeline: Timeline,
+    collection: Collection,
+  ) => {
     await dispatch(TimelineEvents.actions.delete(event));
-    dispatch(goBack());
+    dispatch(push(Urls.timelineArchiveInCollection(timeline, collection)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -25,6 +42,8 @@ const mapDispatchToProps = (dispatch: any) => ({
 });
 
 export default _.compose(
+  Timelines.load(timelineProps),
   TimelineEvents.load(timelineEventProps),
+  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(DeleteEventModal);
diff --git a/frontend/src/metabase/timelines/containers/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/containers/EditEventModal/EditEventModal.tsx
index af34f9f6efdfdc7132fbee2cc91539291cba9a69..4cc20c1763de91891087132ef86ccfd24bf757d5 100644
--- a/frontend/src/metabase/timelines/containers/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/containers/EditEventModal/EditEventModal.tsx
@@ -1,27 +1,50 @@
 import { connect } from "react-redux";
-import { goBack } from "react-router-redux";
+import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
+import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { TimelineEvent } from "metabase-types/api";
+import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import EditEventModal from "../../components/EditEventModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
+const timelineProps = {
+  id: (state: State, props: ModalProps) =>
+    Urls.extractEntityId(props.params.timelineId),
+  query: { include: "events" },
+};
+
 const timelineEventProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineEventId),
   entityAlias: "event",
+  LoadingAndErrorWrapper,
+};
+
+const collectionProps = {
+  id: (state: State, props: ModalProps) =>
+    Urls.extractCollectionId(props.params.slug),
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (event: TimelineEvent) => {
+  onSubmit: async (
+    event: TimelineEvent,
+    timeline: Timeline,
+    collection: Collection,
+  ) => {
     await dispatch(TimelineEvents.actions.update(event));
-    dispatch(goBack());
+    dispatch(push(Urls.timelineInCollection(timeline, collection)));
   },
-  onArchive: async (event: TimelineEvent) => {
+  onArchive: async (
+    event: TimelineEvent,
+    timeline: Timeline,
+    collection: Collection,
+  ) => {
     await dispatch(TimelineEvents.actions.setArchived(event, true));
-    dispatch(goBack());
+    dispatch(push(Urls.timelineInCollection(timeline, collection)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -29,6 +52,8 @@ const mapDispatchToProps = (dispatch: any) => ({
 });
 
 export default _.compose(
+  Timelines.load(timelineProps),
   TimelineEvents.load(timelineEventProps),
+  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(EditEventModal);
diff --git a/frontend/src/metabase/timelines/containers/EditTimelineModal/EditTimelineModal.tsx b/frontend/src/metabase/timelines/containers/EditTimelineModal/EditTimelineModal.tsx
index c8722d3f1b64fdbda50765af44f6c3c9c2455e65..96a72e59b4675772b513ef38c72143ad17f90ea8 100644
--- a/frontend/src/metabase/timelines/containers/EditTimelineModal/EditTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/containers/EditTimelineModal/EditTimelineModal.tsx
@@ -1,27 +1,35 @@
 import { connect } from "react-redux";
-import { goBack } from "react-router-redux";
+import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
-import { Timeline } from "metabase-types/api";
+import { Collection, Timeline } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import EditTimelineModal from "../../components/EditTimelineModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const timelineProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
+  LoadingAndErrorWrapper,
+};
+
+const collectionProps = {
+  id: (state: State, props: ModalProps) =>
+    Urls.extractCollectionId(props.params.slug),
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (timeline: Timeline) => {
+  onSubmit: async (timeline: Timeline, collection: Collection) => {
     await dispatch(Timelines.actions.update(timeline));
-    dispatch(goBack());
+    dispatch(push(Urls.timelineInCollection(timeline, collection)));
   },
-  onArchive: async (timeline: Timeline) => {
+  onArchive: async (timeline: Timeline, collection: Collection) => {
     await dispatch(Timelines.actions.setArchived(timeline, true));
-    dispatch(goBack());
+    dispatch(push(Urls.timelinesInCollection(collection)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -30,5 +38,6 @@ const mapDispatchToProps = (dispatch: any) => ({
 
 export default _.compose(
   Timelines.load(timelineProps),
+  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(EditTimelineModal);
diff --git a/frontend/src/metabase/timelines/containers/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/containers/NewEventModal/NewEventModal.tsx
index cf790e4acd3c6eaecfbdc42f01d3f3ea5556f23e..af8695ef495466249afeaa3921de00f0a286b0d3 100644
--- a/frontend/src/metabase/timelines/containers/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/containers/NewEventModal/NewEventModal.tsx
@@ -8,12 +8,14 @@ import TimelineEvents from "metabase/entities/timeline-events";
 import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import NewEventModal from "../../components/NewEventModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const timelineProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
+  LoadingAndErrorWrapper,
 };
 
 const collectionProps = {
@@ -22,9 +24,13 @@ const collectionProps = {
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (values: Partial<TimelineEvent>) => {
+  onSubmit: async (
+    values: Partial<TimelineEvent>,
+    collection: Collection,
+    timeline: Timeline,
+  ) => {
     await dispatch(TimelineEvents.actions.create(values));
-    dispatch(goBack());
+    dispatch(push(Urls.timelineInCollection(timeline, collection)));
   },
   onCancel: () => {
     dispatch(goBack());
diff --git a/frontend/src/metabase/timelines/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx b/frontend/src/metabase/timelines/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
index 68afd53f5a5a3747509257110789cfbd61691a03..729d3642cc0ca159397b2b0969e47ae69582007f 100644
--- a/frontend/src/metabase/timelines/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
@@ -1,5 +1,5 @@
 import { connect } from "react-redux";
-import { goBack } from "react-router-redux";
+import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Collections from "metabase/entities/collections";
@@ -7,11 +7,13 @@ import Timelines from "metabase/entities/timelines";
 import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import NewEventModal from "../../components/NewEventModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const collectionProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractCollectionId(props.params.slug),
+  LoadingAndErrorWrapper,
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
@@ -19,7 +21,7 @@ const mapDispatchToProps = (dispatch: any) => ({
     const action = Timelines.actions.createWithEvent(values, collection);
     const response = await dispatch(action);
     const timeline = Timelines.HACK_getObjectFromAction(response);
-    dispatch(goBack());
+    dispatch(push(Urls.timelineInCollection(timeline, collection)));
   },
   onCancel: () => {
     dispatch(goBack());
diff --git a/frontend/src/metabase/timelines/containers/NewTimelineModal/NewTimelineModal.tsx b/frontend/src/metabase/timelines/containers/NewTimelineModal/NewTimelineModal.tsx
index 5ff6198c8a36b71c5fb6b8f1442b981f6d9ee9ff..64fc36693f1298935e6b2c59b5d50bdb5613b414 100644
--- a/frontend/src/metabase/timelines/containers/NewTimelineModal/NewTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/containers/NewTimelineModal/NewTimelineModal.tsx
@@ -7,11 +7,13 @@ import Timelines from "metabase/entities/timelines";
 import { Collection, Timeline } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import NewTimelineModal from "../../components/NewTimelineModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const collectionProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractCollectionId(props.params.slug),
+  LoadingAndErrorWrapper,
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
diff --git a/frontend/src/metabase/timelines/containers/TimelineArchiveModal/TimelineArchiveModal.tsx b/frontend/src/metabase/timelines/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
index ca1918b93275ebecce530328fe0850db81c06063..90b2d1346d30591726ab463c3dcdcb2592a3188d 100644
--- a/frontend/src/metabase/timelines/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
+++ b/frontend/src/metabase/timelines/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
@@ -1,18 +1,21 @@
 import { connect } from "react-redux";
+import { push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { TimelineEvent } from "metabase-types/api";
+import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import TimelineDetailsModal from "../../components/TimelineDetailsModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const timelineProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events", archived: true },
+  LoadingAndErrorWrapper,
 };
 
 const collectionProps = {
@@ -28,6 +31,9 @@ const mapDispatchToProps = (dispatch: any) => ({
   onUnarchive: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.setArchived(event, false));
   },
+  onGoBack: (collection: Collection) => {
+    dispatch(push(Urls.timelinesInCollection(collection)));
+  },
 });
 
 export default _.compose(
diff --git a/frontend/src/metabase/timelines/containers/TimelineDetailsModal/TimelineDetailsModal.tsx b/frontend/src/metabase/timelines/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
index 519886cc80c9946bdf93f5eb189054a2246690c8..8f5b45bd447f65548d4cd002b98d6b10f71c5e12 100644
--- a/frontend/src/metabase/timelines/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
+++ b/frontend/src/metabase/timelines/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
@@ -1,18 +1,21 @@
 import { connect } from "react-redux";
+import { push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { TimelineEvent } from "metabase-types/api";
+import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import TimelineDetailsModal from "../../components/TimelineDetailsModal";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalProps } from "../../types";
 
 const timelineProps = {
   id: (state: State, props: ModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
+  LoadingAndErrorWrapper,
 };
 
 const collectionProps = {
@@ -24,6 +27,9 @@ const mapDispatchToProps = (dispatch: any) => ({
   onArchive: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.setArchived(event, true));
   },
+  onGoBack: (collection: Collection) => {
+    dispatch(push(Urls.timelinesInCollection(collection)));
+  },
 });
 
 export default _.compose(
diff --git a/frontend/src/metabase/timelines/containers/TimelineEntryModal/index.ts b/frontend/src/metabase/timelines/containers/TimelineEntryModal/index.ts
deleted file mode 100644
index 1e014f3eb5e111f5551353016f5d6134993b3a4c..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/timelines/containers/TimelineEntryModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./TimelineEntryModal";
diff --git a/frontend/src/metabase/timelines/containers/TimelineEntryModal/TimelineEntryModal.tsx b/frontend/src/metabase/timelines/containers/TimelineIndexModal/TimelineIndexModal.tsx
similarity index 74%
rename from frontend/src/metabase/timelines/containers/TimelineEntryModal/TimelineEntryModal.tsx
rename to frontend/src/metabase/timelines/containers/TimelineIndexModal/TimelineIndexModal.tsx
index 2d2d68a5c917854c329b36c6eeff75641ae60c66..9a0316b370c395ae69219b011e3a04824d4a6a7a 100644
--- a/frontend/src/metabase/timelines/containers/TimelineEntryModal/TimelineEntryModal.tsx
+++ b/frontend/src/metabase/timelines/containers/TimelineIndexModal/TimelineIndexModal.tsx
@@ -1,7 +1,7 @@
 import * as Urls from "metabase/lib/urls";
 import Timelines from "metabase/entities/timelines";
 import { State } from "metabase-types/store";
-import TimelineEntryModal from "../../components/TimelineEntryModal";
+import TimelineIndexModal from "../../components/TimelineIndexModal";
 import { ModalProps } from "../../types";
 
 const timelineProps = {
@@ -11,4 +11,4 @@ const timelineProps = {
   }),
 };
 
-export default Timelines.loadList(timelineProps)(TimelineEntryModal);
+export default Timelines.loadList(timelineProps)(TimelineIndexModal);
diff --git a/frontend/src/metabase/timelines/containers/TimelineIndexModal/index.ts b/frontend/src/metabase/timelines/containers/TimelineIndexModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b5ab6de59d80787065f6305d36c84f859e3d88ae
--- /dev/null
+++ b/frontend/src/metabase/timelines/containers/TimelineIndexModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./TimelineIndexModal";
diff --git a/frontend/src/metabase/timelines/routes.tsx b/frontend/src/metabase/timelines/routes.tsx
index 907bf15136d75da89ebc46cffaf73c469693f420..213f0d12e994accb3bca917c991b92441d54ea67 100644
--- a/frontend/src/metabase/timelines/routes.tsx
+++ b/frontend/src/metabase/timelines/routes.tsx
@@ -8,7 +8,7 @@ import NewEventWithTimelineModal from "./containers/NewEventWithTimelineModal";
 import NewTimelineModal from "./containers/NewTimelineModal";
 import TimelineArchiveModal from "./containers/TimelineArchiveModal";
 import TimelineDetailsModal from "./containers/TimelineDetailsModal";
-import TimelineEntryModal from "./containers/TimelineEntryModal";
+import TimelineIndexModal from "./containers/TimelineIndexModal";
 
 const getRoutes = () => {
   return (
@@ -16,55 +16,64 @@ const getRoutes = () => {
       <ModalRoute
         {...{
           path: "timelines",
-          modal: TimelineEntryModal,
+          modal: TimelineIndexModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/new",
           modal: NewTimelineModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId",
           modal: TimelineDetailsModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/edit",
           modal: EditTimelineModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/archive",
           modal: TimelineArchiveModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/new/events/new",
           modal: NewEventWithTimelineModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/events/new",
           modal: NewEventModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/events/:timelineEventId/edit",
           modal: EditEventModal,
+          modalProps: { enableTransition: false },
         }}
       />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/events/:timelineEventId/delete",
           modal: DeleteEventModal,
+          modalProps: { enableTransition: false },
         }}
       />
     </Fragment>
diff --git a/frontend/test/__support__/e2e/commands.js b/frontend/test/__support__/e2e/commands.js
index 068b428b2b3a02e98693d710d8c0758235883219..8c019985ed9f48d0e8cb57f1309af06574cbbc4f 100644
--- a/frontend/test/__support__/e2e/commands.js
+++ b/frontend/test/__support__/e2e/commands.js
@@ -10,11 +10,13 @@ import "./commands/api/collection";
 import "./commands/api/moderation";
 import "./commands/api/pulse";
 import "./commands/api/user";
+import "./commands/api/timeline";
 
 import "./commands/api/composite/createQuestionAndDashboard";
 import "./commands/api/composite/createNativeQuestionAndDashboard";
 import "./commands/api/composite/createQuestionAndAddToDashboard";
 import "./commands/api/composite/createDashboardWithQuestions";
+import "./commands/api/composite/createTimelineWithEvents";
 
 import "./commands/user/createUser";
 import "./commands/user/authentication";
diff --git a/frontend/test/__support__/e2e/commands/api/composite/createTimelineWithEvents.js b/frontend/test/__support__/e2e/commands/api/composite/createTimelineWithEvents.js
new file mode 100644
index 0000000000000000000000000000000000000000..96110a8ee9eb1a5a9f3dae18107ccc4d8cfd2090
--- /dev/null
+++ b/frontend/test/__support__/e2e/commands/api/composite/createTimelineWithEvents.js
@@ -0,0 +1,16 @@
+import { cypressWaitAll } from "__support__/e2e/cypress";
+
+Cypress.Commands.add("createTimelineWithEvents", ({ timeline, events }) => {
+  return cy.createTimeline(timeline).then(({ body: timeline }) => {
+    return cypressWaitAll(
+      events.map(query =>
+        cy.createTimelineEvent({ ...query, timeline_id: timeline.id }),
+      ),
+    ).then(events => {
+      return {
+        timeline,
+        events,
+      };
+    });
+  });
+});
diff --git a/frontend/test/__support__/e2e/commands/api/timeline.js b/frontend/test/__support__/e2e/commands/api/timeline.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab614e70c8fbd7d30e466cecac1ef66cfaeabf69
--- /dev/null
+++ b/frontend/test/__support__/e2e/commands/api/timeline.js
@@ -0,0 +1,43 @@
+Cypress.Commands.add(
+  "createTimeline",
+  ({
+    name = "Timeline",
+    description,
+    icon = "star",
+    collection_id,
+    archived = false,
+  } = {}) => {
+    return cy.request("POST", "/api/timeline", {
+      name,
+      description,
+      icon,
+      collection_id,
+      archived,
+    });
+  },
+);
+
+Cypress.Commands.add(
+  "createTimelineEvent",
+  ({
+    name = "Event",
+    description,
+    icon = "star",
+    timestamp = "2020-01-01T00:00:00Z",
+    time_matters = false,
+    timezone = "UTC",
+    timeline_id,
+    archived = false,
+  }) => {
+    return cy.request("POST", "/api/timeline-event", {
+      name,
+      description,
+      icon,
+      timestamp,
+      time_matters,
+      timezone,
+      timeline_id,
+      archived,
+    });
+  },
+);
diff --git a/frontend/test/metabase-visual/collections/timelines.cy.spec.js b/frontend/test/metabase-visual/collections/timelines.cy.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2ee8a57362f00bad5e4c34679fd2a9c6a07e040
--- /dev/null
+++ b/frontend/test/metabase-visual/collections/timelines.cy.spec.js
@@ -0,0 +1,30 @@
+import { restore } from "__support__/e2e/cypress";
+
+const EVENTS = [
+  { name: "Event 1", timestamp: "2020-01-01", icon: "star" },
+  { name: "Event 2", timestamp: "2020-03-01", icon: "mail" },
+  { name: "Event 3", timestamp: "2020-02-01", icon: "balloons" },
+];
+
+describe("timelines", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+  });
+
+  it("should display empty state", () => {
+    cy.visit("/collection/root/timelines");
+
+    cy.findByText("Our analytics events");
+    cy.percySnapshot();
+  });
+
+  it("should display timeline events", () => {
+    cy.createTimelineWithEvents({ events: EVENTS }).then(({ timeline }) => {
+      cy.visit(`/collection/root/timelines/${timeline.id}`);
+
+      cy.findByText("Timeline");
+      cy.percySnapshot();
+    });
+  });
+});
diff --git a/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js b/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b71433e7be8b7c78801df594b878d27caba5850
--- /dev/null
+++ b/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js
@@ -0,0 +1,274 @@
+import { restore } from "__support__/e2e/cypress";
+
+describe("scenarios > collections > timelines", () => {
+  beforeEach(() => {
+    restore();
+  });
+
+  describe("as admin", () => {
+    beforeEach(() => {
+      cy.signInAsAdmin();
+    });
+
+    it("should create the first event and timeline", () => {
+      cy.visit("/collection/root");
+
+      cy.findByLabelText("calendar icon").click();
+
+      cy.findByText("Add an event").click();
+      cy.findByLabelText("Event name").type("RC1");
+      cy.findByLabelText("Date").type("10/20/2020");
+      cy.button("Create").click();
+
+      cy.findByText("RC1");
+      cy.findByText("October 20, 2020");
+      cy.findByLabelText("star icon");
+
+      cy.findByText("Add an event").click();
+      cy.findByLabelText("Event name").type("RC2");
+      cy.findByLabelText("Date").type("5/12/2021");
+      cy.findByText("Star").click();
+      cy.findByText("Balloons").click();
+      cy.button("Create").click();
+
+      cy.findByText("RC2");
+      cy.findByText("May 12, 2021");
+      cy.findByLabelText("balloons icon");
+    });
+
+    it("should search for events", () => {
+      cy.createTimelineWithEvents({
+        events: [
+          { name: "RC1" },
+          { name: "RC2" },
+          { name: "v1.0" },
+          { name: "v1.1" },
+        ],
+      });
+
+      cy.visit("/collection/root/timelines");
+
+      cy.findByPlaceholderText("Search for an event").type("V1");
+      cy.findByText("v1.0");
+      cy.findByText("v1.1");
+      cy.findByText("RC1").should("not.exist");
+      cy.findByText("RC2").should("not.exist");
+    });
+
+    it("should create an event with date", () => {
+      cy.visit("/collection/root");
+
+      cy.findByLabelText("calendar icon").click();
+      cy.findByText("Add an event").click();
+
+      cy.findByLabelText("Event name").type("RC1");
+      cy.findByRole("button", { name: "calendar icon" }).click();
+      cy.findByText("15").click();
+      cy.findByText("Done").click();
+      cy.findByText("Create").click();
+
+      cy.findByText("Our analytics events");
+      cy.findByText("RC1");
+      cy.findByText(/15/);
+    });
+
+    it("should create an event with date and time", () => {
+      cy.visit("/collection/root");
+
+      cy.findByLabelText("calendar icon").click();
+      cy.findByText("Add an event").click();
+
+      cy.findByLabelText("Event name").type("RC1");
+      cy.findByRole("button", { name: "calendar icon" }).click();
+      cy.findByText("15").click();
+      cy.findByText("Add time").click();
+      cy.findByLabelText("Hours")
+        .clear()
+        .type("10");
+      cy.findByLabelText("Minutes")
+        .clear()
+        .type("20");
+      cy.findByText("Done").click();
+      cy.findByText("Create").click();
+
+      cy.findByText("Our analytics events");
+      cy.findByText("RC1");
+      cy.findByText(/15/);
+      cy.findByText(/10:20 AM/);
+    });
+
+    it("should edit an event", () => {
+      cy.createTimelineWithEvents({ events: [{ name: "RC1" }] });
+      cy.visit("/collection/root/timelines");
+
+      openEventMenu("RC1");
+      cy.findByText("Edit event").click();
+      cy.findByLabelText("Event name")
+        .clear()
+        .type("RC2");
+      cy.button("Update").click();
+
+      cy.findByText("RC2");
+    });
+
+    it("should archive an event when editing this event", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }, { name: "RC2" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+
+      openEventMenu("RC1");
+      cy.findByText("Edit event").click();
+      cy.findByText("Archive event").click();
+
+      cy.findByText("Releases");
+      cy.findByText("RC1").should("not.exist");
+    });
+
+    it("should archive an event from the timeline and undo", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }, { name: "RC2" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+
+      openEventMenu("RC1");
+      cy.findByText("Archive event").click();
+      cy.findByText("RC1").should("not.exist");
+      cy.findByText("Undo").click();
+      cy.findByText("RC1");
+    });
+
+    it("should unarchive an event from the archive and undo", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1", archived: true }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      openTimelineMenu("Releases");
+      cy.findByText("View archived events").click();
+
+      cy.findByText("Archived events");
+      openEventMenu("RC1");
+      cy.findByText("Unarchive event").click();
+      cy.findByText("No events found");
+
+      cy.findByText("Undo").click();
+      cy.findByText("RC1");
+    });
+
+    it("should delete an event", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1", archived: true }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      openTimelineMenu("Releases");
+      cy.findByText("View archived events").click();
+
+      cy.findByText("Archived events");
+      openEventMenu("RC1");
+      cy.findByText("Delete event").click();
+      cy.findByText("Delete").click();
+      cy.findByText("No events found");
+    });
+
+    it("should create an additional timeline", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      openTimelineMenu("Releases");
+      cy.findByText("New timeline").click();
+      cy.findByLabelText("Timeline name").type("Launches");
+      cy.findByText("Create").click();
+
+      cy.findByText("Launches");
+      cy.findByText("Add an event");
+    });
+
+    it("should edit a timeline", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      openTimelineMenu("Releases");
+      cy.findByText("Edit timeline details").click();
+      cy.findByLabelText("Timeline name")
+        .clear()
+        .type("Launches");
+      cy.findByText("Update").click();
+
+      cy.findByText("Launches");
+    });
+
+    it("should archive and unarchive a timeline", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }, { name: "RC2" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      openTimelineMenu("Releases");
+      cy.findByText("Edit timeline details").click();
+      cy.findByText("Archive timeline and all events").click();
+      cy.findByText("Our analytics events");
+      cy.findByText("Add an event");
+
+      cy.findByText("Undo").click();
+      cy.findByText("Releases");
+      cy.findByText("RC1");
+      cy.findByText("RC2");
+    });
+  });
+
+  describe("as readonly user", () => {
+    it("should not allow creating new timelines in collections", () => {
+      cy.signIn("readonly");
+      cy.visit("/collection/root");
+
+      cy.findByLabelText("calendar icon").click();
+      cy.findByText("Our analytics events");
+      cy.findByText("Add an event").should("not.exist");
+    });
+
+    it("should not allow creating new events in existing timelines", () => {
+      cy.signInAsAdmin();
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }],
+      });
+      cy.signOut();
+
+      cy.signIn("readonly");
+      cy.visit("/collection/root");
+      cy.findByLabelText("calendar icon").click();
+      cy.findByText("Releases");
+      cy.findByText("Add an event").should("not.exist");
+    });
+  });
+});
+
+const openEventMenu = name => {
+  return cy
+    .findByText(name)
+    .parent()
+    .parent()
+    .within(() => cy.findByLabelText("ellipsis icon").click());
+};
+
+const openTimelineMenu = name => {
+  return cy
+    .findByText(name)
+    .parent()
+    .within(() => cy.findByLabelText("ellipsis icon").click());
+};