From 17df46786720a51e3412362cdf17f6de47333adc Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Mon, 21 Mar 2022 11:41:55 +0300
Subject: [PATCH] Filter timelines by question's dates (#21095)

---
 .../src/metabase/query_builder/actions.js     |   8 +-
 .../query_builder/components/QueryModals.jsx  |   8 --
 .../query_builder/components/view/View.jsx    |   2 +
 .../TimelineSidebar/TimelineSidebar.tsx       |  10 +-
 .../src/metabase/query_builder/constants.js   |   1 -
 .../query_builder/containers/QueryBuilder.jsx |  10 +-
 .../src/metabase/query_builder/selectors.js   | 103 +++++++++++++-----
 .../components/TimelineCard/TimelineCard.tsx  |   4 +-
 .../TimelineCard/TimelineCard.unit.spec.tsx   |  17 +--
 .../TimelineEmptyState/TimelineEmptyState.tsx |   6 +-
 .../TimelineEmptyState.unit.spec.tsx          |   2 +-
 .../TimelinePanel/TimelinePanel.tsx           |   7 +-
 .../TimelinePanel/TimelinePanel.unit.spec.tsx |   3 +-
 .../EditEventModal/EditEventModal.tsx         |   3 +
 .../NewEventModal/NewEventModal.tsx           |  14 ++-
 .../NewEventWithTimelineModal.tsx             |  28 -----
 .../NewEventWithTimelineModal/index.ts        |   1 -
 .../TimelinePanel/TimelinePanel.tsx           |   6 -
 .../metabase/visualizations/lib/timelines.js  |  26 +----
 19 files changed, 118 insertions(+), 141 deletions(-)
 delete mode 100644 frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
 delete mode 100644 frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/index.ts

diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index 436fca365e4..11470876a0f 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -61,7 +61,7 @@ import {
   getResultsMetadata,
   getSnippetCollectionId,
   getTableForeignKeys,
-  getTimelines,
+  getFetchedTimelines,
   getTransformedSeries,
   getZoomedObjectId,
   isBasedOnExistingQuestion,
@@ -1678,10 +1678,10 @@ export const showTimelinesForCollection = collectionId => (
   dispatch,
   getState,
 ) => {
-  const availableTimelines = getTimelines(getState());
+  const fetchedTimelines = getFetchedTimelines(getState());
   const collectionTimelines = collectionId
-    ? availableTimelines.filter(t => t.collection_id === collectionId)
-    : availableTimelines.filter(t => t.collection_id == null);
+    ? fetchedTimelines.filter(t => t.collection_id === collectionId)
+    : fetchedTimelines.filter(t => t.collection_id == null);
 
   dispatch(showTimelines(collectionTimelines));
 };
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index 438459e46d2..370300ac33e 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -23,7 +23,6 @@ import { ImpossibleToCreateModelModal } from "metabase/query_builder/components/
 import NewDatasetModal from "metabase/query_builder/components/NewDatasetModal";
 import EntityCopyModal from "metabase/entities/containers/EntityCopyModal";
 import NewEventModal from "metabase/timelines/questions/containers/NewEventModal";
-import NewEventWithTimelineModal from "metabase/timelines/questions/containers/NewEventWithTimelineModal";
 import EditEventModal from "metabase/timelines/questions/containers/EditEventModal";
 
 export default class QueryModals extends React.Component {
@@ -230,13 +229,6 @@ export default class QueryModals extends React.Component {
           onClose={onCloseModal}
         />
       </Modal>
-    ) : modal === MODAL_TYPES.NEW_EVENT_WITH_TIMELINE ? (
-      <Modal onClose={onCloseModal}>
-        <NewEventWithTimelineModal
-          collectionId={question.collectionId()}
-          onClose={onCloseModal}
-        />
-      </Modal>
     ) : modal === MODAL_TYPES.EDIT_EVENT ? (
       <Modal onClose={onCloseModal}>
         <EditEventModal eventId={modalContext} onClose={onCloseModal} />
diff --git a/frontend/src/metabase/query_builder/components/view/View.jsx b/frontend/src/metabase/query_builder/components/view/View.jsx
index 7d1a5baec36..62aba2be059 100644
--- a/frontend/src/metabase/query_builder/components/view/View.jsx
+++ b/frontend/src/metabase/query_builder/components/view/View.jsx
@@ -158,6 +158,7 @@ export default class View extends React.Component {
   getRightSidebarForStructuredQuery = () => {
     const {
       question,
+      timelines,
       isResultDirty,
       isShowingSummarySidebar,
       isShowingFilterSidebar,
@@ -192,6 +193,7 @@ export default class View extends React.Component {
       return (
         <TimelineSidebar
           question={question}
+          timelines={timelines}
           visibleTimelineIds={visibleTimelineIds}
           selectedTimelineEventIds={selectedTimelineEventIds}
           onShowTimelines={showTimelines}
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
index 051debf060e..2bbf54e1691 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
@@ -8,6 +8,7 @@ import { Timeline, TimelineEvent } from "metabase-types/api";
 
 export interface TimelineSidebarProps {
   question: Question;
+  timelines: Timeline[];
   visibleTimelineIds: number[];
   selectedTimelineEventIds: number[];
   onShowTimelines?: (timelines: Timeline[]) => void;
@@ -18,6 +19,7 @@ export interface TimelineSidebarProps {
 
 const TimelineSidebar = ({
   question,
+  timelines,
   visibleTimelineIds,
   selectedTimelineEventIds,
   onOpenModal,
@@ -29,10 +31,6 @@ const TimelineSidebar = ({
     onOpenModal?.(MODAL_TYPES.NEW_EVENT);
   }, [onOpenModal]);
 
-  const handleNewEventWithTimeline = useCallback(() => {
-    onOpenModal?.(MODAL_TYPES.NEW_EVENT_WITH_TIMELINE);
-  }, [onOpenModal]);
-
   const handleEditEvent = useCallback(
     (event: TimelineEvent) => {
       onOpenModal?.(MODAL_TYPES.EDIT_EVENT, event.id);
@@ -54,11 +52,11 @@ const TimelineSidebar = ({
   return (
     <SidebarContent title={t`Events`} onClose={onClose}>
       <TimelinePanel
+        timelines={timelines}
+        collectionId={question.collectionId()}
         visibleTimelineIds={visibleTimelineIds}
         selectedEventIds={selectedTimelineEventIds}
-        collectionId={question.collectionId()}
         onNewEvent={handleNewEvent}
-        onNewEventWithTimeline={handleNewEventWithTimeline}
         onEditEvent={handleEditEvent}
         onToggleTimeline={handleToggleTimeline}
       />
diff --git a/frontend/src/metabase/query_builder/constants.js b/frontend/src/metabase/query_builder/constants.js
index 2f81e41039c..56aa05eee4d 100644
--- a/frontend/src/metabase/query_builder/constants.js
+++ b/frontend/src/metabase/query_builder/constants.js
@@ -15,6 +15,5 @@ export const MODAL_TYPES = {
   TURN_INTO_DATASET: "turn-into-dataset",
   CAN_NOT_CREATE_MODEL: "can-not-create-model",
   NEW_EVENT: "new-event",
-  NEW_EVENT_WITH_TIMELINE: "new-event-with-timeline",
   EDIT_EVENT: "edit-event",
 };
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 188ac913261..9d647931972 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -65,9 +65,9 @@ import {
   getNativeEditorSelectedText,
   getIsBookmarked,
   getVisibleTimelineIds,
-  getVisibleTimelines,
   getVisibleTimelineEvents,
   getSelectedTimelineEventIds,
+  getFilteredTimelines,
 } from "../selectors";
 import * as actions from "../actions";
 
@@ -116,10 +116,10 @@ const mapStateToProps = (state, props) => {
     query: getQuery(state),
     metadata: getMetadata(state),
 
-    timelines: getVisibleTimelines(state, props),
-    timelineEvents: getVisibleTimelineEvents(state, props),
-    visibleTimelineIds: getVisibleTimelineIds(state, props),
-    selectedTimelineEventIds: getSelectedTimelineEventIds(state, props),
+    timelines: getFilteredTimelines(state),
+    timelineEvents: getVisibleTimelineEvents(state),
+    visibleTimelineIds: getVisibleTimelineIds(state),
+    selectedTimelineEventIds: getSelectedTimelineEventIds(state),
 
     result: getFirstQueryResult(state),
     results: getQueryResults(state),
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index 5d414182788..0239f9e848c 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -1,5 +1,6 @@
 /*eslint no-use-before-define: "error"*/
 
+import d3 from "d3";
 import { createSelector } from "reselect";
 import _ from "underscore";
 import { assocIn, getIn, merge, updateIn } from "icepick";
@@ -26,6 +27,10 @@ import Timelines from "metabase/entities/timelines";
 import { getMetadata } from "metabase/selectors/metadata";
 import { getAlerts } from "metabase/alert/selectors";
 import { parseTimestamp } from "metabase/lib/time";
+import {
+  getXValues,
+  isTimeseries,
+} from "metabase/visualizations/lib/renderer_utils";
 
 export const getUiControls = state => state.qb.uiControls;
 
@@ -286,35 +291,6 @@ export const getQuestion = createSelector(
   },
 );
 
-export const getTimelines = createSelector([getEntities], entities => {
-  const entityQuery = { include: "events" };
-  return Timelines.selectors.getList({ entities }, { entityQuery }) ?? [];
-});
-
-export const getVisibleTimelines = createSelector(
-  [getQuestion, getTimelines, getVisibleTimelineIds],
-  (question, timelines, timelineIds) => {
-    if (!question) {
-      return [];
-    }
-
-    return timelines.filter(t => timelineIds.includes(t.id));
-  },
-);
-
-export const getVisibleTimelineEvents = createSelector(
-  [getVisibleTimelines],
-  timelines => {
-    return _.chain(timelines)
-      .map(timeline => timeline.events ?? [])
-      .flatten()
-      .filter(event => !event.archived)
-      .map(event => updateIn(event, ["timestamp"], parseTimestamp))
-      .sortBy(event => event.timestamp)
-      .value();
-  },
-);
-
 function normalizeClause(clause) {
   return typeof clause?.raw === "function" ? clause.raw() : clause;
 }
@@ -618,6 +594,75 @@ const getNativeEditorSelectedRange = createSelector(
   uiControls => uiControls && uiControls.nativeEditorSelectedRange,
 );
 
+function isEventWithinDomain(event, xDomain) {
+  return event.timestamp.isBetween(xDomain[0], xDomain[1], undefined, "[]");
+}
+
+const getIsTimeseries = createSelector(
+  [getVisualizationSettings],
+  settings => settings && isTimeseries(settings),
+);
+
+const getTimeseriesXValues = createSelector(
+  [getIsTimeseries, getTransformedSeries, getVisualizationSettings],
+  (isTimeseries, series, settings) =>
+    isTimeseries && series && settings && getXValues({ series, settings }),
+);
+
+const getTimeseriesXDomain = createSelector(
+  [getIsTimeseries, getTimeseriesXValues],
+  (isTimeseries, xValues) => xValues && isTimeseries && d3.extent(xValues),
+);
+
+export const getFetchedTimelines = createSelector([getEntities], entities => {
+  const entityQuery = { include: "events" };
+  return Timelines.selectors.getList({ entities }, { entityQuery }) ?? [];
+});
+
+export const getTransformedTimelines = createSelector(
+  [getFetchedTimelines],
+  timelines => {
+    return timelines.map(timeline =>
+      updateIn(timeline, ["events"], (events = []) =>
+        _.chain(events)
+          .map(event => updateIn(event, ["timestamp"], parseTimestamp))
+          .filter(event => !event.archived)
+          .sortBy(event => event.timestamp)
+          .value(),
+      ),
+    );
+  },
+);
+
+export const getFilteredTimelines = createSelector(
+  [getTransformedTimelines, getTimeseriesXDomain],
+  (timelines, xDomain) => {
+    if (!xDomain) {
+      return [];
+    }
+
+    return timelines
+      .map(timeline =>
+        updateIn(timeline, ["events"], events =>
+          events.filter(event => isEventWithinDomain(event, xDomain)),
+        ),
+      )
+      .filter(timeline => timeline.events.length > 0);
+  },
+);
+
+export const getVisibleTimelines = createSelector(
+  [getFilteredTimelines, getVisibleTimelineIds],
+  (timelines, timelineIds) => {
+    return timelines.filter(t => timelineIds.includes(t.id));
+  },
+);
+
+export const getVisibleTimelineEvents = createSelector(
+  [getVisibleTimelines],
+  timelines => timelines.flatMap(timeline => timeline.events),
+);
+
 function getOffsetForQueryAndPosition(queryText, { row, column }) {
   const queryLines = queryText.split("\n");
   return (
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
index 8213e65dead..cb996ca7f46 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
@@ -7,7 +7,6 @@ import React, {
   useEffect,
 } from "react";
 import _ from "underscore";
-import { parseTimestamp } from "metabase/lib/time";
 import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import EventCard from "../EventCard";
 import {
@@ -96,8 +95,7 @@ const TimelineCard = ({
 
 const getEvents = (events: TimelineEvent[] = []) => {
   return _.chain(events)
-    .filter(e => !e.archived)
-    .sortBy(e => parseTimestamp(e.timestamp))
+    .sortBy(e => e.timestamp)
     .reverse()
     .value();
 };
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.unit.spec.tsx b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.unit.spec.tsx
index cd1f1840c36..d4a378d9281 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.unit.spec.tsx
@@ -13,32 +13,25 @@ describe("TimelineCard", () => {
     const props = getProps({
       timeline: createMockTimeline({
         name: "Releases",
-        events: [
-          createMockTimelineEvent({ name: "RC1" }),
-          createMockTimelineEvent({ name: "RC2" }),
-          createMockTimelineEvent({ name: "RC3", archived: true }),
-        ],
+        events: [createMockTimelineEvent({ name: "RC" })],
       }),
     });
 
     render(<TimelineCard {...props} />);
-    expect(screen.queryByText("RC1")).not.toBeInTheDocument();
-    expect(screen.queryByText("RC3")).not.toBeInTheDocument();
+    expect(screen.queryByText("RC")).not.toBeInTheDocument();
 
     userEvent.click(screen.getByText("Releases"));
-    expect(screen.getByText("RC1")).toBeInTheDocument();
-    expect(screen.queryByText("RC3")).not.toBeInTheDocument();
+    expect(screen.getByText("RC")).toBeInTheDocument();
 
     userEvent.click(screen.getByText("Releases"));
-    expect(screen.queryByText("RC1")).not.toBeInTheDocument();
-    expect(screen.queryByText("RC3")).not.toBeInTheDocument();
+    expect(screen.queryByText("RC")).not.toBeInTheDocument();
   });
 
   it("should toggle visibility of the card", () => {
     const props = getProps({
       timeline: createMockTimeline({
         name: "Releases",
-        events: [createMockTimelineEvent({ name: "RC1" })],
+        events: [createMockTimelineEvent({ name: "RC" })],
       }),
     });
 
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.tsx b/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.tsx
index 9adfb3049ce..09d8e0e154e 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.tsx
@@ -10,12 +10,12 @@ import {
 
 export interface TimelineEmptyStateProps {
   collection: Collection;
-  onNewEventWithTimeline?: () => void;
+  onNewEvent?: () => void;
 }
 
 const TimelineEmptyState = ({
   collection,
-  onNewEventWithTimeline,
+  onNewEvent,
 }: TimelineEmptyStateProps): JSX.Element => {
   const canWrite = collection.can_write;
 
@@ -28,7 +28,7 @@ const TimelineEmptyState = ({
           : t`Events in Metabase let you see helpful context alongside your data.`}
       </EmptyStateText>
       {canWrite && (
-        <EmptyStateButton primary onClick={onNewEventWithTimeline}>
+        <EmptyStateButton primary onClick={onNewEvent}>
           {t`Add an event`}
         </EmptyStateButton>
       )}
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx b/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx
index 077d1765016..83fb9dd7773 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineEmptyState/TimelineEmptyState.unit.spec.tsx
@@ -37,6 +37,6 @@ const getProps = (
   opts?: Partial<TimelineEmptyStateProps>,
 ): TimelineEmptyStateProps => ({
   collection: createMockCollection(),
-  onNewEventWithTimeline: jest.fn(),
+  onNewEvent: jest.fn(),
   ...opts,
 });
diff --git a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
index e1887d4a155..89480c842bb 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
@@ -12,7 +12,6 @@ export interface TimelinePanelProps {
   visibleTimelineIds?: number[];
   selectedEventIds?: number[];
   onNewEvent?: () => void;
-  onNewEventWithTimeline?: () => void;
   onEditEvent?: (event: TimelineEvent) => void;
   onArchiveEvent?: (event: TimelineEvent) => void;
   onToggleTimeline?: (timeline: Timeline, isVisible: boolean) => void;
@@ -24,7 +23,6 @@ const TimelinePanel = ({
   visibleTimelineIds,
   selectedEventIds,
   onNewEvent,
-  onNewEventWithTimeline,
   onEditEvent,
   onArchiveEvent,
   onToggleTimeline,
@@ -50,10 +48,7 @@ const TimelinePanel = ({
           onArchiveEvent={onArchiveEvent}
         />
       ) : (
-        <TimelineEmptyState
-          collection={collection}
-          onNewEventWithTimeline={onNewEventWithTimeline}
-        />
+        <TimelineEmptyState collection={collection} onNewEvent={onNewEvent} />
       )}
     </PanelRoot>
   );
diff --git a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.unit.spec.tsx b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.unit.spec.tsx
index affb9e89b4c..0a9cc628204 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.unit.spec.tsx
@@ -17,7 +17,7 @@ describe("TimelinePanel", () => {
     render(<TimelinePanel {...props} />);
     userEvent.click(screen.getByText("Add an event"));
 
-    expect(props.onNewEventWithTimeline).toHaveBeenCalled();
+    expect(props.onNewEvent).toHaveBeenCalled();
   });
 
   it("should allow creating an event within existing timelines", () => {
@@ -48,7 +48,6 @@ const getProps = (opts?: Partial<TimelinePanelProps>): TimelinePanelProps => ({
   timelines: [],
   collection: createMockCollection(),
   onNewEvent: jest.fn(),
-  onNewEventWithTimeline: jest.fn(),
   onEditEvent: jest.fn(),
   onArchiveEvent: jest.fn(),
   onToggleTimeline: jest.fn(),
diff --git a/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
index 3cc30e040d4..67327b68842 100644
--- a/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
@@ -1,6 +1,8 @@
 import { connect } from "react-redux";
+import { t } from "ttag";
 import _ from "underscore";
 import TimelineEvents from "metabase/entities/timeline-events";
+import { addUndo } from "metabase/redux/undo";
 import { TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import EditEventModal from "../../components/EditEventModal";
@@ -17,6 +19,7 @@ const timelineEventProps = {
 const mapDispatchToProps = (dispatch: any) => ({
   onSubmit: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.update(event));
+    dispatch(addUndo({ message: t`Updated event` }));
   },
   onArchive: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.setArchived(event, true));
diff --git a/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
index 4b860e732b8..90564ffac3b 100644
--- a/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
@@ -1,9 +1,11 @@
 import { connect } from "react-redux";
+import { t } from "ttag";
 import _ from "underscore";
 import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { TimelineEvent } from "metabase-types/api";
+import { addUndo } from "metabase/redux/undo";
+import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import NewEventModal from "../../components/NewEventModal";
 
@@ -26,8 +28,14 @@ const collectionProps = {
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (values: Partial<TimelineEvent>) => {
-    await dispatch(TimelineEvents.actions.create(values));
+  onSubmit: async (values: Partial<TimelineEvent>, collection: Collection) => {
+    if (values.timeline_id) {
+      await dispatch(TimelineEvents.actions.create(values));
+    } else {
+      await dispatch(Timelines.actions.createWithEvent(values, collection));
+    }
+
+    dispatch(addUndo({ message: t`Created event` }));
   },
 });
 
diff --git a/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx b/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
deleted file mode 100644
index 6cab685b8bb..00000000000
--- a/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { connect } from "react-redux";
-import _ from "underscore";
-import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
-import Timelines from "metabase/entities/timelines";
-import { Collection, TimelineEvent } from "metabase-types/api";
-import { State } from "metabase-types/store";
-import NewEventModal from "../../components/NewEventModal";
-
-interface TimelinePanelProps {
-  collectionId?: number;
-}
-
-const collectionProps = {
-  id: (state: State, props: TimelinePanelProps) => {
-    return props.collectionId ?? ROOT_COLLECTION.id;
-  },
-};
-
-const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (values: Partial<TimelineEvent>, collection: Collection) => {
-    await dispatch(Timelines.actions.createWithEvent(values, collection));
-  },
-});
-
-export default _.compose(
-  Collections.load(collectionProps),
-  connect(null, mapDispatchToProps),
-)(NewEventModal);
diff --git a/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/index.ts b/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/index.ts
deleted file mode 100644
index dab3201d695..00000000000
--- a/frontend/src/metabase/timelines/questions/containers/NewEventWithTimelineModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./NewEventWithTimelineModal";
diff --git a/frontend/src/metabase/timelines/questions/containers/TimelinePanel/TimelinePanel.tsx b/frontend/src/metabase/timelines/questions/containers/TimelinePanel/TimelinePanel.tsx
index 5de09c3b2ce..2a8797a7c5d 100644
--- a/frontend/src/metabase/timelines/questions/containers/TimelinePanel/TimelinePanel.tsx
+++ b/frontend/src/metabase/timelines/questions/containers/TimelinePanel/TimelinePanel.tsx
@@ -1,7 +1,6 @@
 import { connect } from "react-redux";
 import _ from "underscore";
 import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
-import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
 import { TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
@@ -11,10 +10,6 @@ interface TimelinePanelProps {
   collectionId?: number;
 }
 
-const timelineProps = {
-  query: { include: "events" },
-};
-
 const collectionProps = {
   id: (state: State, props: TimelinePanelProps) => {
     return props.collectionId ?? ROOT_COLLECTION.id;
@@ -28,7 +23,6 @@ const mapDispatchToProps = (dispatch: any) => ({
 });
 
 export default _.compose(
-  Timelines.loadList(timelineProps),
   Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(TimelinePanel);
diff --git a/frontend/src/metabase/visualizations/lib/timelines.js b/frontend/src/metabase/visualizations/lib/timelines.js
index 8e88e78cd2a..ecaa238160f 100644
--- a/frontend/src/metabase/visualizations/lib/timelines.js
+++ b/frontend/src/metabase/visualizations/lib/timelines.js
@@ -9,18 +9,6 @@ const EVENT_ICON_MARGIN_TOP = 10;
 const EVENT_GROUP_COUNT_MARGIN_LEFT = 10;
 const EVENT_GROUP_COUNT_MARGIN_TOP = EVENT_ICON_MARGIN_TOP + 8;
 
-function isAxisEvent(event, [xAxisMin, xAxisMax]) {
-  return (
-    event.timestamp.isSame(xAxisMin) ||
-    event.timestamp.isBetween(xAxisMin, xAxisMax) ||
-    event.timestamp.isSame(xAxisMax)
-  );
-}
-
-function getAxisEvents(events, xDomain) {
-  return events.filter(event => isAxisEvent(event, xDomain));
-}
-
 function getEventGroups(events, xInterval) {
   return _.groupBy(events, event =>
     event.timestamp
@@ -178,16 +166,12 @@ export function renderEvents(
   },
 ) {
   const xAxis = getXAxis(chart);
-  if (!xAxis || !isTimeseries) {
+  if (!xAxis || !isTimeseries || !timelineEvents.length) {
     return;
   }
 
-  const events = getAxisEvents(timelineEvents, xDomain);
-  const eventGroups = getEventGroups(events, xInterval);
+  const eventGroups = getEventGroups(timelineEvents, xInterval);
   const eventTicks = getEventTicks(eventGroups);
-  if (!events.length) {
-    return;
-  }
 
   const eventAxis = getEventAxis(xAxis, xDomain, xInterval, eventTicks);
   renderEventTicks(chart, {
@@ -202,9 +186,5 @@ export function renderEvents(
 }
 
 export function hasEventAxis({ timelineEvents = [], xDomain, isTimeseries }) {
-  if (!isTimeseries) {
-    return false;
-  }
-
-  return timelineEvents.some(event => isAxisEvent(event, xDomain));
+  return isTimeseries && timelineEvents.length > 0;
 }
-- 
GitLab