diff --git a/frontend/src/metabase-lib/lib/parameters/utils/click-behavior.js b/frontend/src/metabase-lib/lib/parameters/utils/click-behavior.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a1a815ea83d5dc26cdf5dcf8868c873df758e5a
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/parameters/utils/click-behavior.js
@@ -0,0 +1,292 @@
+import _ from "underscore";
+import { getIn } from "icepick";
+
+import { parseTimestamp } from "metabase/lib/time";
+import { formatDateTimeForParameter } from "metabase/lib/formatting/date";
+import { isValidImplicitActionClickBehavior } from "metabase/writeback/utils";
+import {
+  dimensionFilterForParameter,
+  variableFilterForParameter,
+} from "metabase-lib/lib/parameters/utils/filters";
+import { isa, isDate } from "metabase-lib/lib/types/utils/isa";
+import { TYPE } from "metabase-lib/lib/types/constants";
+import Question from "metabase-lib/lib/Question";
+import TemplateTagVariable from "metabase-lib/lib/variables/TemplateTagVariable";
+import { TemplateTagDimension } from "metabase-lib/lib/Dimension";
+
+export function getDataFromClicked({
+  extraData: { dashboard, parameterValuesBySlug, userAttributes } = {},
+  dimensions = [],
+  data = [],
+}) {
+  const column = [
+    ...dimensions,
+    ...data.map(d => ({
+      column: d.col,
+      // When the data is changed to a display value for use in tooltips, we can set clickBehaviorValue to the raw value for filtering.
+      value: d.clickBehaviorValue || d.value,
+    })),
+  ]
+    .filter(d => d.column != null)
+    .reduce(
+      (acc, { column, value }) =>
+        acc[name] === undefined
+          ? { ...acc, [column.name.toLowerCase()]: { value, column } }
+          : acc,
+      {},
+    );
+
+  const parameterByName =
+    dashboard == null
+      ? {}
+      : _.chain(dashboard.parameters)
+          .filter(p => parameterValuesBySlug[p.slug] != null)
+          .map(p => [
+            p.name.toLowerCase(),
+            { value: parameterValuesBySlug[p.slug] },
+          ])
+          .object()
+          .value();
+
+  const parameterBySlug = _.mapObject(parameterValuesBySlug, value => ({
+    value,
+  }));
+
+  const parameter =
+    dashboard == null
+      ? {}
+      : _.chain(dashboard.parameters)
+          .filter(p => parameterValuesBySlug[p.slug] != null)
+          .map(p => [p.id, { value: parameterValuesBySlug[p.slug] }])
+          .object()
+          .value();
+
+  const userAttribute = _.mapObject(userAttributes, value => ({ value }));
+
+  return { column, parameter, parameterByName, parameterBySlug, userAttribute };
+}
+
+const { Text, Number, Temporal } = TYPE;
+
+function notRelativeDateOrRange({ type }) {
+  return type !== "date/range" && type !== "date/relative";
+}
+
+export function getTargetsWithSourceFilters({
+  isDash,
+  isAction,
+  dashcard,
+  object,
+  metadata,
+}) {
+  if (isAction) {
+    return getTargetsForAction(object);
+  }
+  return isDash
+    ? getTargetsForDashboard(object, dashcard)
+    : getTargetsForQuestion(object, metadata);
+}
+
+function getTargetsForAction(action) {
+  const parameters = Object.values(action.parameters);
+  return parameters.map(parameter => {
+    const { id, name } = parameter;
+    return {
+      id,
+      name,
+      target: { type: "parameter", id },
+
+      // We probably don't want to allow everything
+      // and will need to add some filters eventually
+      sourceFilters: {
+        column: () => true,
+        parameter: () => true,
+        userAttribute: () => true,
+      },
+    };
+  });
+}
+
+function getTargetsForQuestion(question, metadata) {
+  const query = new Question(question, metadata).query();
+  return query
+    .dimensionOptions()
+    .all()
+    .concat(query.variables())
+    .map(o => {
+      let id, target;
+      if (
+        o instanceof TemplateTagVariable ||
+        o instanceof TemplateTagDimension
+      ) {
+        let name;
+        ({ id, name } = o.tag());
+        target = { type: "variable", id: name };
+      } else {
+        const dimension = ["dimension", o.mbql()];
+        id = JSON.stringify(dimension);
+        target = { type: "dimension", id, dimension };
+      }
+      let parentType;
+      let parameterSourceFilter = () => true;
+      const columnSourceFilter = c => isa(c.base_type, parentType);
+      if (o instanceof TemplateTagVariable) {
+        parentType = { text: Text, number: Number, date: Temporal }[
+          o.tag().type
+        ];
+        parameterSourceFilter = parameter =>
+          variableFilterForParameter(parameter)(o);
+      } else if (o.field() != null) {
+        const { base_type } = o.field();
+        parentType =
+          [Temporal, Number, Text].find(t => isa(base_type, t)) || base_type;
+        parameterSourceFilter = parameter =>
+          dimensionFilterForParameter(parameter)(o);
+      }
+
+      return {
+        id,
+        target,
+        name: o.displayName({ includeTable: true }),
+        sourceFilters: {
+          column: columnSourceFilter,
+          parameter: parameterSourceFilter,
+          userAttribute: () => parentType === Text,
+        },
+      };
+    });
+}
+
+function getTargetsForDashboard(dashboard, dashcard) {
+  return dashboard.parameters.map(parameter => {
+    const { type, id, name } = parameter;
+    const filter = baseTypeFilterForParameterType(type);
+    return {
+      id,
+      name,
+      target: { type: "parameter", id },
+      sourceFilters: {
+        column: c => notRelativeDateOrRange(parameter) && filter(c.base_type),
+        parameter: sourceParam => {
+          // parameter IDs are generated client-side, so they might not be unique
+          // if dashboard is a clone, it will have identical parameter IDs to the original
+          const isSameParameter =
+            dashboard.id === dashcard.dashboard_id &&
+            parameter.id === sourceParam.id;
+          return parameter.type === sourceParam.type && !isSameParameter;
+        },
+        userAttribute: () => !parameter.type.startsWith("date"),
+      },
+    };
+  });
+}
+
+function baseTypeFilterForParameterType(parameterType) {
+  const [typePrefix] = parameterType.split("/");
+  const allowedTypes = {
+    date: [TYPE.Temporal],
+    id: [TYPE.Integer, TYPE.UUID],
+    category: [TYPE.Text, TYPE.Integer],
+    location: [TYPE.Text],
+  }[typePrefix];
+  if (allowedTypes === undefined) {
+    // default to showing everything
+    return () => true;
+  }
+  return baseType =>
+    allowedTypes.some(allowedType => isa(baseType, allowedType));
+}
+
+export function clickBehaviorIsValid(clickBehavior) {
+  // opens action menu
+  if (clickBehavior == null) {
+    return true;
+  }
+  const {
+    type,
+    parameterMapping = {},
+    linkType,
+    targetId,
+    linkTemplate,
+  } = clickBehavior;
+  if (type === "crossfilter") {
+    return Object.keys(parameterMapping).length > 0;
+  }
+  if (type === "action") {
+    return isValidImplicitActionClickBehavior(clickBehavior);
+  }
+  // if it's not a crossfilter/action, it's a link
+  if (linkType === "url") {
+    return (linkTemplate || "").length > 0;
+  }
+  // if we're linking to a Metabase entity we just need a targetId
+  if (
+    linkType === "dashboard" ||
+    linkType === "question" ||
+    linkType === "page"
+  ) {
+    return targetId != null;
+  }
+  // we've picked "link" without picking a link type
+  return false;
+}
+
+export function formatSourceForTarget(
+  source,
+  target,
+  { data, extraData, clickBehavior },
+) {
+  const datum = data[source.type][source.id.toLowerCase()] || [];
+  if (datum.column && isDate(datum.column)) {
+    if (target.type === "parameter") {
+      // we should serialize differently based on the target parameter type
+      const parameter = getParameter(target, { extraData, clickBehavior });
+      if (parameter) {
+        return formatDateForParameterType(
+          datum.value,
+          parameter.type,
+          datum.column.unit,
+        );
+      }
+    } else {
+      // If the target is a dimension or variable,, we serialize as a date to remove the timestamp.
+      // TODO: provide better serialization for field filter widget types
+      return formatDateForParameterType(datum.value, "date/single");
+    }
+  }
+  return datum.value;
+}
+
+function formatDateForParameterType(value, parameterType, unit) {
+  const m = parseTimestamp(value);
+  if (!m.isValid()) {
+    return String(value);
+  }
+  if (parameterType === "date/month-year") {
+    return m.format("YYYY-MM");
+  } else if (parameterType === "date/quarter-year") {
+    return m.format("[Q]Q-YYYY");
+  } else if (parameterType === "date/single") {
+    return m.format("YYYY-MM-DD");
+  } else if (parameterType === "date/all-options") {
+    return formatDateTimeForParameter(value, unit);
+  }
+  return value;
+}
+
+export function getTargetForQueryParams(target, { extraData, clickBehavior }) {
+  if (target.type === "parameter") {
+    const parameter = getParameter(target, { extraData, clickBehavior });
+    return parameter && parameter.slug;
+  }
+  return target.id;
+}
+
+function getParameter(target, { extraData, clickBehavior }) {
+  const parameterPath =
+    clickBehavior.type === "crossfilter"
+      ? ["dashboard", "parameters"]
+      : ["dashboards", clickBehavior.targetId, "parameters"];
+  const parameters = getIn(extraData, parameterPath) || [];
+  return parameters.find(p => p.id === target.id);
+}
diff --git a/frontend/src/metabase-lib/lib/queries/drills/dashboard-click-drill.js b/frontend/src/metabase-lib/lib/queries/drills/dashboard-click-drill.js
new file mode 100644
index 0000000000000000000000000000000000000000..71d6427476dacca96358d592d598edc3951939f5
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/drills/dashboard-click-drill.js
@@ -0,0 +1,221 @@
+import _ from "underscore";
+import { getIn } from "icepick";
+import querystring from "querystring";
+import * as Urls from "metabase/lib/urls";
+import { renderLinkURLForClick } from "metabase/lib/formatting/link";
+import {
+  formatSourceForTarget,
+  getDataFromClicked,
+  getTargetForQueryParams,
+} from "metabase-lib/lib/parameters/utils/click-behavior";
+import Question from "metabase-lib/lib/Question";
+
+export function getDashboardDrillType(clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  if (clickBehavior == null) {
+    return null;
+  }
+
+  const { type, linkType, targetId, extraData } = getClickBehaviorData(
+    clicked,
+    clickBehavior,
+  );
+  if (!hasLinkTargetData(clickBehavior, extraData)) {
+    return null;
+  }
+
+  if (type === "crossfilter") {
+    return "dashboard-filter";
+  } else if (type === "link") {
+    if (linkType === "url") {
+      return "link-url";
+    } else if (linkType === "dashboard") {
+      if (extraData.dashboard.id === targetId) {
+        return "dashboard-reset";
+      } else {
+        return "dashboard-url";
+      }
+    } else if (linkType === "page") {
+      const { location, routerParams } = extraData;
+
+      const isInDataApp =
+        Urls.isDataAppPagePath(location.pathname) ||
+        Urls.isDataAppPath(location.pathname);
+      if (!isInDataApp) {
+        return null;
+      }
+
+      const dataAppId = Urls.extractEntityId(routerParams.slug);
+      if (!dataAppId) {
+        return null;
+      }
+
+      return "page-url";
+    } else if (linkType === "question" && extraData && extraData.questions) {
+      return "question-url";
+    }
+  }
+
+  return null;
+}
+
+export function getDashboardDrillParameters(clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  const { data, parameterMapping, extraData } = getClickBehaviorData(
+    clicked,
+    clickBehavior,
+  );
+
+  return getParameterIdValuePairs(parameterMapping, {
+    data,
+    extraData,
+    clickBehavior,
+  });
+}
+
+export function getDashboardDrillLinkUrl(clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  const { data } = getClickBehaviorData(clicked, clickBehavior);
+
+  return renderLinkURLForClick(clickBehavior.linkTemplate || "", data);
+}
+
+export function getDashboardDrillUrl(clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  const { data, extraData, parameterMapping, targetId } = getClickBehaviorData(
+    clicked,
+    clickBehavior,
+  );
+
+  const queryParams = getParameterValuesBySlug(parameterMapping, {
+    data,
+    extraData,
+    clickBehavior,
+  });
+
+  const path = Urls.dashboard({ id: targetId });
+  return `${path}?${querystring.stringify(queryParams)}`;
+}
+
+export function getDashboardDrillPageUrl(clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  const { data, extraData, parameterMapping, targetId } = getClickBehaviorData(
+    clicked,
+    clickBehavior,
+  );
+
+  const { routerParams } = extraData;
+  const dataAppId = Urls.extractEntityId(routerParams.slug);
+  const path = Urls.dataAppPage({ id: dataAppId }, { id: targetId });
+
+  const queryParams = getParameterValuesBySlug(parameterMapping, {
+    data,
+    extraData,
+    clickBehavior,
+  });
+
+  return `${path}?${querystring.stringify(queryParams)}`;
+}
+
+export function getDashboardDrillQuestionUrl(question, clicked) {
+  const clickBehavior = getClickBehavior(clicked);
+  const { data, extraData, parameterMapping, targetId } = getClickBehaviorData(
+    clicked,
+    clickBehavior,
+  );
+
+  const targetQuestion = new Question(
+    extraData.questions[targetId],
+    question.metadata(),
+  ).lockDisplay();
+
+  const parameters = _.chain(parameterMapping)
+    .values()
+    .map(({ target, id, source }) => ({
+      target: target.dimension,
+      id,
+      slug: id,
+      type: getTypeForSource(source, extraData),
+    }))
+    .value();
+
+  const queryParams = getParameterValuesBySlug(parameterMapping, {
+    data,
+    extraData,
+    clickBehavior,
+  });
+
+  return targetQuestion.isStructured()
+    ? targetQuestion.getUrlWithParameters(parameters, queryParams)
+    : `${targetQuestion.getUrl()}?${querystring.stringify(queryParams)}`;
+}
+
+function getClickBehavior(clicked) {
+  const settings = (clicked && clicked.settings) || {};
+  const columnSettings =
+    (clicked &&
+      clicked.column &&
+      settings.column &&
+      settings.column(clicked.column)) ||
+    {};
+
+  return columnSettings.click_behavior || settings.click_behavior;
+}
+
+function getClickBehaviorData(clicked, clickBehavior) {
+  const data = getDataFromClicked(clicked);
+  const { type, linkType, parameterMapping, targetId } = clickBehavior;
+  const { extraData } = clicked || {};
+
+  return { type, linkType, data, extraData, parameterMapping, targetId };
+}
+
+function getParameterIdValuePairs(
+  parameterMapping,
+  { data, extraData, clickBehavior },
+) {
+  return _.values(parameterMapping).map(({ source, target, id }) => {
+    return [
+      id,
+      formatSourceForTarget(source, target, {
+        data,
+        extraData,
+        clickBehavior,
+      }),
+    ];
+  });
+}
+
+function getParameterValuesBySlug(
+  parameterMapping,
+  { data, extraData, clickBehavior },
+) {
+  return _.chain(parameterMapping)
+    .values()
+    .map(({ source, target }) => [
+      getTargetForQueryParams(target, { extraData, clickBehavior }),
+      formatSourceForTarget(source, target, { data, extraData, clickBehavior }),
+    ])
+    .filter(([key, value]) => value != null)
+    .object()
+    .value();
+}
+
+function getTypeForSource(source, extraData) {
+  if (source.type === "parameter") {
+    const parameters = getIn(extraData, ["dashboard", "parameters"]) || [];
+    const { type = "text" } = parameters.find(p => p.id === source.id) || {};
+    return type;
+  }
+  return "text";
+}
+
+function hasLinkTargetData(clickBehavior, extraData) {
+  const { linkType, targetId } = clickBehavior;
+  if (linkType === "question") {
+    return getIn(extraData, ["questions", targetId]) != null;
+  } else if (linkType === "dashboard") {
+    return getIn(extraData, ["dashboards", targetId]) != null;
+  }
+  return true;
+}
diff --git a/frontend/src/metabase/dashboard/actions/save.js b/frontend/src/metabase/dashboard/actions/save.js
index 0ad697f79f5c2eecfdab694131882ad86f032b35..68fa3a73fc799709ae9185448feaf1338bd44a53 100644
--- a/frontend/src/metabase/dashboard/actions/save.js
+++ b/frontend/src/metabase/dashboard/actions/save.js
@@ -5,9 +5,8 @@ import { createThunkAction } from "metabase/lib/redux";
 
 import Dashboards from "metabase/entities/dashboards";
 
-import { clickBehaviorIsValid } from "metabase/lib/click-behavior";
-
 import { DashboardApi, CardApi } from "metabase/services";
+import { clickBehaviorIsValid } from "metabase-lib/lib/parameters/utils/click-behavior";
 
 import { getDashboardBeforeEditing } from "../selectors";
 
diff --git a/frontend/src/metabase/dashboard/components/AddActionSidebar/ButtonOptions.tsx b/frontend/src/metabase/dashboard/components/AddActionSidebar/ButtonOptions.tsx
index c5b8912db14c59ddb4fb406034e246d55faca8b2..7ff35aa71bd20137f3dd693c8c203f73d61081b6 100644
--- a/frontend/src/metabase/dashboard/components/AddActionSidebar/ButtonOptions.tsx
+++ b/frontend/src/metabase/dashboard/components/AddActionSidebar/ButtonOptions.tsx
@@ -6,11 +6,11 @@ import type {
   CustomDestinationClickBehavior,
   DashboardOrderedCard,
 } from "metabase-types/api";
-import { clickBehaviorIsValid } from "metabase/lib/click-behavior";
 
 import { BehaviorOption } from "metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector";
 import LinkOptions from "metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions";
 import Icon from "metabase/components/Icon";
+import { clickBehaviorIsValid } from "metabase-lib/lib/parameters/utils/click-behavior";
 
 import {
   ClickBehaviorPickerText,
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
index 129b1b42b4ba89f530e0d31d090ffb47553879b4..a81834c7f366ff49197b89c51445ebf8e896cbd0 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
@@ -1,11 +1,6 @@
 import React, { useCallback, useEffect, useMemo, useState } from "react";
 import { getIn } from "icepick";
 
-import {
-  isTableDisplay,
-  clickBehaviorIsValid,
-} from "metabase/lib/click-behavior";
-
 import { useOnMount } from "metabase/hooks/use-on-mount";
 import { usePrevious } from "metabase/hooks/use-previous";
 
@@ -23,6 +18,8 @@ import type {
   DatasetData,
 } from "metabase-types/api";
 import type { Column } from "metabase-types/types/Dataset";
+import { isTableDisplay } from "metabase/lib/click-behavior";
+import { clickBehaviorIsValid } from "metabase-lib/lib/parameters/utils/click-behavior";
 import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
 
 import { getClickBehaviorForColumn } from "./utils";
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
index 60a6bb4e4d311bf8b07e70abea3c2d24e4e74596..b1c835b72a0479179ff26f05c07b94ab53e63394 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
@@ -1,8 +1,6 @@
 import React, { useMemo } from "react";
 import { getIn } from "icepick";
 
-import { isTableDisplay } from "metabase/lib/click-behavior";
-
 import { isMappedExplicitActionButton } from "metabase/writeback/utils";
 
 import type { UiParameter } from "metabase/parameters/types";
@@ -16,6 +14,7 @@ import type {
 } from "metabase-types/api";
 import type { Column } from "metabase-types/types/Dataset";
 
+import { isTableDisplay } from "metabase/lib/click-behavior";
 import { getClickBehaviorForColumn } from "./utils";
 import ClickBehaviorSidebarMainView from "./ClickBehaviorSidebarMainView";
 import TableClickBehaviorView from "./TableClickBehaviorView";
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
index a1a339f268bfa300c7f4464fcb7c31da4e3c5d3b..7947f01d54c65a37427e600bb33253f00f6c209b 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
@@ -3,7 +3,6 @@ import { t, jt } from "ttag";
 
 import Icon from "metabase/components/Icon";
 
-import { isTableDisplay } from "metabase/lib/click-behavior";
 import {
   isActionDashCard,
   getActionButtonLabel,
@@ -12,6 +11,7 @@ import {
 import type { DashboardOrderedCard } from "metabase-types/api";
 import type { Column } from "metabase-types/types/Dataset";
 
+import { isTableDisplay } from "metabase/lib/click-behavior";
 import { Heading, SidebarHeader } from "../ClickBehaviorSidebar.styled";
 import {
   ColumnClickBehaviorHeader,
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.tsx
index c21364f3664ff486b9227c78ea613124a7da4870..c0e37ee57a3b2b2e0454c7d4444ee02246ff661f 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.tsx
@@ -5,17 +5,14 @@ import InputBlurChange from "metabase/components/InputBlurChange";
 import ModalContent from "metabase/components/ModalContent";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 
-import {
-  isTableDisplay,
-  clickBehaviorIsValid,
-} from "metabase/lib/click-behavior";
-
 import type { UiParameter } from "metabase/parameters/types";
 import type {
   ArbitraryCustomDestinationClickBehavior,
   ClickBehavior,
   DashboardOrderedCard,
 } from "metabase-types/api";
+import { isTableDisplay } from "metabase/lib/click-behavior";
+import { clickBehaviorIsValid } from "metabase-lib/lib/parameters/utils/click-behavior";
 
 import { SidebarItem } from "../SidebarItem";
 import CustomLinkText from "./CustomLinkText";
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.tsx
index 1cd9346cd3e776e239c9688fb218a222c1ebb78a..fe1a3f8216f47fbbc970650cca29697ca074266e 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.tsx
@@ -1,8 +1,6 @@
 import React, { useCallback } from "react";
 import { t } from "ttag";
 
-import { isTableDisplay } from "metabase/lib/click-behavior";
-
 import type { UiParameter } from "metabase/parameters/types";
 import type {
   DashboardOrderedCard,
@@ -11,6 +9,7 @@ import type {
   CustomDestinationClickBehavior,
   CustomDestinationClickBehaviorLinkType,
 } from "metabase-types/api";
+import { isTableDisplay } from "metabase/lib/click-behavior";
 import { SidebarContent } from "../ClickBehaviorSidebar.styled";
 import CustomLinkText from "./CustomLinkText";
 import LinkedEntityPicker from "./LinkedEntityPicker";
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
index 751b3169756258520b5f2e5bd0de6ea99d82fa76..1a09ce97cd99940c6424bd7fac82430b777530b0 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
@@ -2,8 +2,6 @@ import React, { useMemo, useCallback } from "react";
 import { t } from "ttag";
 import _ from "underscore";
 
-import { hasActionsMenu } from "metabase/lib/click-behavior";
-
 import type {
   DashboardOrderedCard,
   ClickBehavior,
@@ -11,6 +9,7 @@ import type {
 } from "metabase-types/api";
 import type { Column as IColumn } from "metabase-types/types/Dataset";
 
+import { hasActionsMenu } from "metabase/lib/click-behavior";
 import Column from "./Column";
 
 const COLUMN_SORTING_ORDER_BY_CLICK_BEHAVIOR_TYPE = [
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
index 27dd4af4ca85d1c2dac9ab76d27abd71fc0f72ef..9a35adba1bfb8f78a4a45cdf84d6c2cd53d1e7e3 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
@@ -1,13 +1,12 @@
 import { t } from "ttag";
 import { getIn } from "icepick";
 
-import { hasActionsMenu } from "metabase/lib/click-behavior";
-
 import type {
   ClickBehaviorType,
   DashboardOrderedCard,
 } from "metabase-types/api";
 import type { Column } from "metabase-types/types/Dataset";
+import { hasActionsMenu } from "metabase/lib/click-behavior";
 import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
 
 type ClickBehaviorOption = {
diff --git a/frontend/src/metabase/dashboard/components/ClickMappings.jsx b/frontend/src/metabase/dashboard/components/ClickMappings.jsx
index b278757d0cde9e078dffbbc2b3cb5943b62579e5..bb2025d8eecef92102f03b9ba985bec62e008913 100644
--- a/frontend/src/metabase/dashboard/components/ClickMappings.jsx
+++ b/frontend/src/metabase/dashboard/components/ClickMappings.jsx
@@ -10,12 +10,12 @@ import Select from "metabase/core/components/Select";
 
 import MetabaseSettings from "metabase/lib/settings";
 import { isPivotGroupColumn } from "metabase/lib/data_grid";
-import { getTargetsWithSourceFilters } from "metabase/lib/click-behavior";
 import { GTAPApi } from "metabase/services";
 
 import { loadMetadataForQuery } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 import { getParameters } from "metabase/dashboard/selectors";
+import { getTargetsWithSourceFilters } from "metabase-lib/lib/parameters/utils/click-behavior";
 import Question from "metabase-lib/lib/Question";
 
 class ClickMappingsInner extends React.Component {
diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx
index ef25af8905f2a9801431243b4c68861b62ef3325..0fe079b5cc4be0eab9c07a6a90764154321685a7 100644
--- a/frontend/src/metabase/dashboard/components/DashCard.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCard.jsx
@@ -27,12 +27,12 @@ import Tooltip from "metabase/components/Tooltip";
 import { isVirtualDashCard } from "metabase/dashboard/utils";
 
 import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
-import { getClickBehaviorDescription } from "metabase/lib/click-behavior";
 
 import { isActionCard } from "metabase/writeback/utils";
 
 import { getParameterValuesBySlug } from "metabase/parameters/utils/parameter-values";
 import Utils from "metabase/lib/utils";
+import { getClickBehaviorDescription } from "metabase/lib/click-behavior";
 import DashCardParameterMapper from "./DashCardParameterMapper";
 import { DashCardRoot } from "./DashCard.styled";
 
diff --git a/frontend/src/metabase/lib/click-behavior.js b/frontend/src/metabase/lib/click-behavior.js
index b53ca8252a2ea344190154d00304713b315746b8..debd4fc8766367b89374b95b9af5426512f653c4 100644
--- a/frontend/src/metabase/lib/click-behavior.js
+++ b/frontend/src/metabase/lib/click-behavior.js
@@ -1,202 +1,5 @@
-import _ from "underscore";
+import { msgid, ngettext, t } from "ttag";
 import { getIn } from "icepick";
-import { t, ngettext, msgid } from "ttag";
-
-import { parseTimestamp } from "metabase/lib/time";
-import { formatDateTimeForParameter } from "metabase/lib/formatting/date";
-import { isValidImplicitActionClickBehavior } from "metabase/writeback/utils";
-import {
-  dimensionFilterForParameter,
-  variableFilterForParameter,
-} from "metabase-lib/lib/parameters/utils/filters";
-import { isa, isDate } from "metabase-lib/lib/types/utils/isa";
-import { TYPE } from "metabase-lib/lib/types/constants";
-import Question from "metabase-lib/lib/Question";
-import TemplateTagVariable from "metabase-lib/lib/variables/TemplateTagVariable";
-import { TemplateTagDimension } from "metabase-lib/lib/Dimension";
-
-export function getDataFromClicked({
-  extraData: { dashboard, parameterValuesBySlug, userAttributes } = {},
-  dimensions = [],
-  data = [],
-}) {
-  const column = [
-    ...dimensions,
-    ...data.map(d => ({
-      column: d.col,
-      // When the data is changed to a display value for use in tooltips, we can set clickBehaviorValue to the raw value for filtering.
-      value: d.clickBehaviorValue || d.value,
-    })),
-  ]
-    .filter(d => d.column != null)
-    .reduce(
-      (acc, { column, value }) =>
-        acc[name] === undefined
-          ? { ...acc, [column.name.toLowerCase()]: { value, column } }
-          : acc,
-      {},
-    );
-
-  const parameterByName =
-    dashboard == null
-      ? {}
-      : _.chain(dashboard.parameters)
-          .filter(p => parameterValuesBySlug[p.slug] != null)
-          .map(p => [
-            p.name.toLowerCase(),
-            { value: parameterValuesBySlug[p.slug] },
-          ])
-          .object()
-          .value();
-
-  const parameterBySlug = _.mapObject(parameterValuesBySlug, value => ({
-    value,
-  }));
-
-  const parameter =
-    dashboard == null
-      ? {}
-      : _.chain(dashboard.parameters)
-          .filter(p => parameterValuesBySlug[p.slug] != null)
-          .map(p => [p.id, { value: parameterValuesBySlug[p.slug] }])
-          .object()
-          .value();
-
-  const userAttribute = _.mapObject(userAttributes, value => ({ value }));
-
-  return { column, parameter, parameterByName, parameterBySlug, userAttribute };
-}
-
-const { Text, Number, Temporal } = TYPE;
-
-function notRelativeDateOrRange({ type }) {
-  return type !== "date/range" && type !== "date/relative";
-}
-
-export function getTargetsWithSourceFilters({
-  isDash,
-  isAction,
-  dashcard,
-  object,
-  metadata,
-}) {
-  if (isAction) {
-    return getTargetsForAction(object);
-  }
-  return isDash
-    ? getTargetsForDashboard(object, dashcard)
-    : getTargetsForQuestion(object, metadata);
-}
-
-function getTargetsForAction(action) {
-  const parameters = Object.values(action.parameters);
-  return parameters.map(parameter => {
-    const { id, name } = parameter;
-    return {
-      id,
-      name,
-      target: { type: "parameter", id },
-
-      // We probably don't want to allow everything
-      // and will need to add some filters eventually
-      sourceFilters: {
-        column: () => true,
-        parameter: () => true,
-        userAttribute: () => true,
-      },
-    };
-  });
-}
-
-function getTargetsForQuestion(question, metadata) {
-  const query = new Question(question, metadata).query();
-  return query
-    .dimensionOptions()
-    .all()
-    .concat(query.variables())
-    .map(o => {
-      let id, target;
-      if (
-        o instanceof TemplateTagVariable ||
-        o instanceof TemplateTagDimension
-      ) {
-        let name;
-        ({ id, name } = o.tag());
-        target = { type: "variable", id: name };
-      } else {
-        const dimension = ["dimension", o.mbql()];
-        id = JSON.stringify(dimension);
-        target = { type: "dimension", id, dimension };
-      }
-      let parentType;
-      let parameterSourceFilter = () => true;
-      const columnSourceFilter = c => isa(c.base_type, parentType);
-      if (o instanceof TemplateTagVariable) {
-        parentType = { text: Text, number: Number, date: Temporal }[
-          o.tag().type
-        ];
-        parameterSourceFilter = parameter =>
-          variableFilterForParameter(parameter)(o);
-      } else if (o.field() != null) {
-        const { base_type } = o.field();
-        parentType =
-          [Temporal, Number, Text].find(t => isa(base_type, t)) || base_type;
-        parameterSourceFilter = parameter =>
-          dimensionFilterForParameter(parameter)(o);
-      }
-
-      return {
-        id,
-        target,
-        name: o.displayName({ includeTable: true }),
-        sourceFilters: {
-          column: columnSourceFilter,
-          parameter: parameterSourceFilter,
-          userAttribute: () => parentType === Text,
-        },
-      };
-    });
-}
-
-function getTargetsForDashboard(dashboard, dashcard) {
-  return dashboard.parameters.map(parameter => {
-    const { type, id, name } = parameter;
-    const filter = baseTypeFilterForParameterType(type);
-    return {
-      id,
-      name,
-      target: { type: "parameter", id },
-      sourceFilters: {
-        column: c => notRelativeDateOrRange(parameter) && filter(c.base_type),
-        parameter: sourceParam => {
-          // parameter IDs are generated client-side, so they might not be unique
-          // if dashboard is a clone, it will have identical parameter IDs to the original
-          const isSameParameter =
-            dashboard.id === dashcard.dashboard_id &&
-            parameter.id === sourceParam.id;
-          return parameter.type === sourceParam.type && !isSameParameter;
-        },
-        userAttribute: () => !parameter.type.startsWith("date"),
-      },
-    };
-  });
-}
-
-function baseTypeFilterForParameterType(parameterType) {
-  const [typePrefix] = parameterType.split("/");
-  const allowedTypes = {
-    date: [TYPE.Temporal],
-    id: [TYPE.Integer, TYPE.UUID],
-    category: [TYPE.Text, TYPE.Integer],
-    location: [TYPE.Text],
-  }[typePrefix];
-  if (allowedTypes === undefined) {
-    // default to showing everything
-    return () => true;
-  }
-  return baseType =>
-    allowedTypes.some(allowedType => isa(baseType, allowedType));
-}
 
 export function getClickBehaviorDescription(dashcard) {
   const noBehaviorMessage = hasActionsMenu(dashcard)
@@ -233,40 +36,6 @@ export function getClickBehaviorDescription(dashcard) {
   return t`Filter this dashboard`;
 }
 
-export function clickBehaviorIsValid(clickBehavior) {
-  // opens action menu
-  if (clickBehavior == null) {
-    return true;
-  }
-  const {
-    type,
-    parameterMapping = {},
-    linkType,
-    targetId,
-    linkTemplate,
-  } = clickBehavior;
-  if (type === "crossfilter") {
-    return Object.keys(parameterMapping).length > 0;
-  }
-  if (type === "action") {
-    return isValidImplicitActionClickBehavior(clickBehavior);
-  }
-  // if it's not a crossfilter/action, it's a link
-  if (linkType === "url") {
-    return (linkTemplate || "").length > 0;
-  }
-  // if we're linking to a Metabase entity we just need a targetId
-  if (
-    linkType === "dashboard" ||
-    linkType === "question" ||
-    linkType === "page"
-  ) {
-    return targetId != null;
-  }
-  // we've picked "link" without picking a link type
-  return false;
-}
-
 export function hasActionsMenu(dashcard) {
   // This seems to work, but it isn't the right logic.
   // The right thing to do would be to check for any drills. However, we'd need a "clicked" object for that.
@@ -276,63 +45,3 @@ export function hasActionsMenu(dashcard) {
 export function isTableDisplay(dashcard) {
   return dashcard?.card?.display === "table";
 }
-
-export function formatSourceForTarget(
-  source,
-  target,
-  { data, extraData, clickBehavior },
-) {
-  const datum = data[source.type][source.id.toLowerCase()] || [];
-  if (datum.column && isDate(datum.column)) {
-    if (target.type === "parameter") {
-      // we should serialize differently based on the target parameter type
-      const parameter = getParameter(target, { extraData, clickBehavior });
-      if (parameter) {
-        return formatDateForParameterType(
-          datum.value,
-          parameter.type,
-          datum.column.unit,
-        );
-      }
-    } else {
-      // If the target is a dimension or variable,, we serialize as a date to remove the timestamp.
-      // TODO: provide better serialization for field filter widget types
-      return formatDateForParameterType(datum.value, "date/single");
-    }
-  }
-  return datum.value;
-}
-
-function formatDateForParameterType(value, parameterType, unit) {
-  const m = parseTimestamp(value);
-  if (!m.isValid()) {
-    return String(value);
-  }
-  if (parameterType === "date/month-year") {
-    return m.format("YYYY-MM");
-  } else if (parameterType === "date/quarter-year") {
-    return m.format("[Q]Q-YYYY");
-  } else if (parameterType === "date/single") {
-    return m.format("YYYY-MM-DD");
-  } else if (parameterType === "date/all-options") {
-    return formatDateTimeForParameter(value, unit);
-  }
-  return value;
-}
-
-export function getTargetForQueryParams(target, { extraData, clickBehavior }) {
-  if (target.type === "parameter") {
-    const parameter = getParameter(target, { extraData, clickBehavior });
-    return parameter && parameter.slug;
-  }
-  return target.id;
-}
-
-function getParameter(target, { extraData, clickBehavior }) {
-  const parameterPath =
-    clickBehavior.type === "crossfilter"
-      ? ["dashboard", "parameters"]
-      : ["dashboards", clickBehavior.targetId, "parameters"];
-  const parameters = getIn(extraData, parameterPath) || [];
-  return parameters.find(p => p.id === target.id);
-}
diff --git a/frontend/src/metabase/lib/formatting/email.tsx b/frontend/src/metabase/lib/formatting/email.tsx
index ad47f33e36c9528e64838965acce000b2676de7c..0f867956088eba5bda632c5b7811df2f7743597f 100644
--- a/frontend/src/metabase/lib/formatting/email.tsx
+++ b/frontend/src/metabase/lib/formatting/email.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 
 import ExternalLink from "metabase/core/components/ExternalLink";
-import { getDataFromClicked } from "metabase/lib/click-behavior";
+import { getDataFromClicked } from "metabase-lib/lib/parameters/utils/click-behavior";
 import { renderLinkTextForClick } from "./link";
 
 import { OptionsType } from "./types";
diff --git a/frontend/src/metabase/lib/formatting/strings.ts b/frontend/src/metabase/lib/formatting/strings.ts
index 15ab0d3740f424d1b39451ed07b6c718c6fb05e0..c84c3c7eac9e0e0b620a27b4bfd7ba7a4f63c99c 100644
--- a/frontend/src/metabase/lib/formatting/strings.ts
+++ b/frontend/src/metabase/lib/formatting/strings.ts
@@ -1,6 +1,6 @@
 import inflection from "inflection";
 
-import { getDataFromClicked } from "metabase/lib/click-behavior";
+import { getDataFromClicked } from "metabase-lib/lib/parameters/utils/click-behavior";
 import { formatUrl } from "./url";
 import { renderLinkTextForClick } from "./link";
 import { formatValue, getRemappedValue } from "./value";
diff --git a/frontend/src/metabase/lib/formatting/url.tsx b/frontend/src/metabase/lib/formatting/url.tsx
index 0d67dc1e37812bde4154e490b5e66be531fb018a..2507bc757e3bbef2ebb95d3a20ce8b56234eaa30 100644
--- a/frontend/src/metabase/lib/formatting/url.tsx
+++ b/frontend/src/metabase/lib/formatting/url.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 
 import ExternalLink from "metabase/core/components/ExternalLink";
-import { getDataFromClicked } from "metabase/lib/click-behavior";
+import { getDataFromClicked } from "metabase-lib/lib/parameters/utils/click-behavior";
 import { isURL } from "metabase-lib/lib/types/utils/isa";
 import { renderLinkTextForClick, renderLinkURLForClick } from "./link";
 import { formatValue, getRemappedValue } from "./value";
diff --git a/frontend/src/metabase/lib/formatting/value.tsx b/frontend/src/metabase/lib/formatting/value.tsx
index 2f281974f40cbb860ccdc398dd8b0dd8a93ad590..55b65138e39a768822f9b985dedf1d9a046c5718 100644
--- a/frontend/src/metabase/lib/formatting/value.tsx
+++ b/frontend/src/metabase/lib/formatting/value.tsx
@@ -4,12 +4,12 @@ import Mustache from "mustache";
 import moment, { Moment } from "moment-timezone";
 
 import ExternalLink from "metabase/core/components/ExternalLink";
+import { renderLinkTextForClick } from "metabase/lib/formatting/link";
+import { NULL_DISPLAY_VALUE, NULL_NUMERIC_VALUE } from "metabase/lib/constants";
 import {
   clickBehaviorIsValid,
   getDataFromClicked,
-} from "metabase/lib/click-behavior";
-import { renderLinkTextForClick } from "metabase/lib/formatting/link";
-import { NULL_DISPLAY_VALUE, NULL_NUMERIC_VALUE } from "metabase/lib/constants";
+} from "metabase-lib/lib/parameters/utils/click-behavior";
 import { rangeForValue } from "metabase-lib/lib/queries/utils/dataset";
 import {
   isBoolean,
diff --git a/frontend/src/metabase/modes/components/drill/ActionClickDrill/ActionClickDrill.tsx b/frontend/src/metabase/modes/components/drill/ActionClickDrill/ActionClickDrill.tsx
index 129a560d14863aebe94086773855a801897f31f6..18765a5ac4c4303a112d2304289be53d063ffb03 100644
--- a/frontend/src/metabase/modes/components/drill/ActionClickDrill/ActionClickDrill.tsx
+++ b/frontend/src/metabase/modes/components/drill/ActionClickDrill/ActionClickDrill.tsx
@@ -1,6 +1,5 @@
-import { getDataFromClicked } from "metabase/lib/click-behavior";
-
 import { openActionParametersModal } from "metabase/dashboard/actions";
+import { getDataFromClicked } from "metabase-lib/lib/parameters/utils/click-behavior";
 
 import type { ActionClickObject } from "./types";
 
diff --git a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
index d432d7fdff65534f9b4a24699dea429229234dcf..204e45f7270144fb51a7a0224edd0a18560ff0df 100644
--- a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
@@ -1,204 +1,62 @@
-/* eslint-disable react/prop-types */
-import { getIn } from "icepick";
-import _ from "underscore";
-import querystring from "querystring";
 import { push } from "react-router-redux";
-
 import {
   setOrUnsetParameterValues,
   setParameterValue,
 } from "metabase/dashboard/actions";
 import {
-  getDataFromClicked,
-  getTargetForQueryParams,
-  formatSourceForTarget,
-} from "metabase/lib/click-behavior";
-import { renderLinkURLForClick } from "metabase/lib/formatting/link";
-import * as Urls from "metabase/lib/urls";
-import Question from "metabase-lib/lib/Question";
-
-export default ({ question, clicked }) => {
-  const settings = (clicked && clicked.settings) || {};
-  const columnSettings =
-    (clicked &&
-      clicked.column &&
-      settings.column &&
-      settings.column(clicked.column)) ||
-    {};
-
-  const clickBehavior =
-    columnSettings.click_behavior || settings.click_behavior;
-
-  if (clickBehavior == null) {
-    return [];
-  }
-  const { extraData } = clicked || {};
-  const data = getDataFromClicked(clicked);
-  const { type, linkType, parameterMapping, targetId } = clickBehavior;
-
-  let behavior;
-
-  if (!hasLinkTargetData(clickBehavior, extraData)) {
-    return [];
-  }
-
-  if (type === "crossfilter") {
-    const parameterIdValuePairs = getParameterIdValuePairs(parameterMapping, {
-      data,
-      extraData,
-      clickBehavior,
-    });
-
-    behavior = {
-      action: () => setOrUnsetParameterValues(parameterIdValuePairs),
-    };
-  } else if (type === "link") {
-    if (linkType === "url") {
-      behavior = {
+  getDashboardDrillLinkUrl,
+  getDashboardDrillPageUrl,
+  getDashboardDrillParameters,
+  getDashboardDrillQuestionUrl,
+  getDashboardDrillType,
+  getDashboardDrillUrl,
+} from "metabase-lib/lib/queries/drills/dashboard-click-drill";
+
+function getAction(type, question, clicked) {
+  switch (type) {
+    case "link-url":
+      return {
         ignoreSiteUrl: true,
-        url: () =>
-          renderLinkURLForClick(clickBehavior.linkTemplate || "", data),
+        url: () => getDashboardDrillLinkUrl(clicked),
       };
-    } else if (linkType === "dashboard") {
-      if (extraData.dashboard.id === targetId) {
-        const parameterIdValuePairs = getParameterIdValuePairs(
-          parameterMapping,
-          { data, extraData, clickBehavior },
-        );
-
-        behavior = {
-          action: () => {
-            return dispatch =>
-              parameterIdValuePairs.forEach(([id, value]) => {
-                setParameterValue(id, value)(dispatch);
-              });
-          },
-        };
-      } else {
-        const queryParams = getParameterValuesBySlug(parameterMapping, {
-          data,
-          extraData,
-          clickBehavior,
-        });
-
-        const path = Urls.dashboard({ id: targetId });
-        const url = `${path}?${querystring.stringify(queryParams)}`;
-
-        behavior = { url: () => url };
-      }
-    } else if (linkType === "page") {
-      const { location, routerParams } = extraData;
-
-      const isInDataApp =
-        Urls.isDataAppPagePath(location.pathname) ||
-        Urls.isDataAppPath(location.pathname);
-
-      if (!isInDataApp) {
-        return [];
-      }
-
-      const dataAppId = Urls.extractEntityId(routerParams.slug);
-      if (!dataAppId) {
-        return [];
-      }
-
-      const queryParams = getParameterValuesBySlug(parameterMapping, {
-        data,
-        extraData,
-        clickBehavior,
-      });
-
-      const path = Urls.dataAppPage({ id: dataAppId }, { id: targetId });
-      const url = `${path}?${querystring.stringify(queryParams)}`;
-
-      behavior = { action: () => push(url) };
-    } else if (linkType === "question" && extraData && extraData.questions) {
-      const queryParams = getParameterValuesBySlug(parameterMapping, {
-        data,
-        extraData,
-        clickBehavior,
-      });
-
-      const targetQuestion = new Question(
-        extraData.questions[targetId],
-        question.metadata(),
-      ).lockDisplay();
-
-      const parameters = _.chain(parameterMapping)
-        .values()
-        .map(({ target, id, source }) => ({
-          target: target.dimension,
-          id,
-          slug: id,
-          type: getTypeForSource(source, extraData),
-        }))
-        .value();
-
-      const url = targetQuestion.isStructured()
-        ? targetQuestion.getUrlWithParameters(parameters, queryParams)
-        : `${targetQuestion.getUrl()}?${querystring.stringify(queryParams)}`;
+    case "question-url":
+      return {
+        url: () => getDashboardDrillQuestionUrl(question, clicked),
+      };
+    case "page-url":
+      return { action: () => push(getDashboardDrillPageUrl(clicked)) };
+    case "dashboard-url":
+      return { url: () => getDashboardDrillUrl(clicked) };
+    case "dashboard-filter":
+      return {
+        action: () => {
+          const parameterIdValuePairs = getDashboardDrillParameters(clicked);
+          return setOrUnsetParameterValues(parameterIdValuePairs);
+        },
+      };
+    case "dashboard-reset":
+      return {
+        action: () => dispatch => {
+          const parameterIdValuePairs = getDashboardDrillParameters(clicked);
+          parameterIdValuePairs
+            .map(([id, value]) => setParameterValue(id, value))
+            .forEach(action => dispatch(action));
+        },
+      };
+  }
+}
 
-      behavior = { url: () => url };
-    }
+export default ({ question, clicked }) => {
+  const type = getDashboardDrillType(clicked);
+  if (!type) {
+    return [];
   }
 
   return [
     {
       name: "click_behavior",
       defaultAlways: true,
-      ...behavior,
+      ...getAction(type, question, clicked),
     },
   ];
 };
-
-function getParameterIdValuePairs(
-  parameterMapping,
-  { data, extraData, clickBehavior },
-) {
-  const value = _.values(parameterMapping).map(({ source, target, id }) => {
-    return [
-      id,
-      formatSourceForTarget(source, target, {
-        data,
-        extraData,
-        clickBehavior,
-      }),
-    ];
-  });
-
-  return value;
-}
-
-function getParameterValuesBySlug(
-  parameterMapping,
-  { data, extraData, clickBehavior },
-) {
-  return _.chain(parameterMapping)
-    .values()
-    .map(({ source, target }) => [
-      getTargetForQueryParams(target, { extraData, clickBehavior }),
-      formatSourceForTarget(source, target, { data, extraData, clickBehavior }),
-    ])
-    .filter(([key, value]) => value != null)
-    .object()
-    .value();
-}
-
-function getTypeForSource(source, extraData) {
-  if (source.type === "parameter") {
-    const parameters = getIn(extraData, ["dashboard", "parameters"]) || [];
-    const { type = "text" } = parameters.find(p => p.id === source.id) || {};
-    return type;
-  }
-  return "text";
-}
-
-function hasLinkTargetData(clickBehavior, extraData) {
-  const { linkType, targetId } = clickBehavior;
-  if (linkType === "question") {
-    return getIn(extraData, ["questions", targetId]) != null;
-  } else if (linkType === "dashboard") {
-    return getIn(extraData, ["dashboards", targetId]) != null;
-  }
-  return true;
-}
diff --git a/frontend/test/metabase/lib/click-behavior.unit.spec.js b/frontend/test/metabase/lib/click-behavior.unit.spec.js
index 1031fe865cad3cca13b72e5871068f69a119361c..6874477096c0d3db30c494a6333cb09704743bd7 100644
--- a/frontend/test/metabase/lib/click-behavior.unit.spec.js
+++ b/frontend/test/metabase/lib/click-behavior.unit.spec.js
@@ -1,11 +1,11 @@
 import _ from "underscore";
+import { metadata, PRODUCTS } from "__support__/sample_database_fixture";
+import * as dateFormatUtils from "metabase/lib/formatting/date";
 import {
   getDataFromClicked,
   getTargetsWithSourceFilters,
   formatSourceForTarget,
-} from "metabase/lib/click-behavior";
-import { metadata, PRODUCTS } from "__support__/sample_database_fixture";
-import * as dateFormatUtils from "metabase/lib/formatting/date";
+} from "metabase-lib/lib/parameters/utils/click-behavior";
 
 describe("metabase/lib/click-behavior", () => {
   describe("getDataFromClicked", () => {