Repository: metabase/metabase
Pull request #57687: feat: add a link to field filter docs from dashboard sidebar with native question
Author: login: uladzimirdev
Labels: name: backport
description: Automatically create PR on current release branch on merge
name: .Team/Querying
name: PR-Env
description: See https://www.notion.so/metabase/Ephemeral-PR-Environments-c74a3710c87a460dbf4fc0007a001458
Created: 2025-05-07T08:28:53Z | Merged: 2025-05-08T07:30:39Z

<!-- Added by 'Add Issue References to PR' GitHub Action. To disable linking, add 'no-auto-issue-links' label to your PR. --> closes #56925
Closes QUE-930

### Description

adds a link to the docs to improve interactivity with native questions in dashboards


### Demo

(wrong link text `Filter` is used in video, but it's fixed in code)

https://github.com/user-attachments/assets/f5403906-3315-457b-9811-1cfc19beccac




Linked issue #56925: Make easier for users to discover SQL questions need Field Filters to use Linked Filters
**Is your feature request related to a problem? Please describe.**
When a user creates a SQL question with Text dropdown filters and adds to a dashboard, a message is shown that tells them what they can't do, but it doesn't provide any guidance on what they should do.

<img width="384" alt="Image" src="https://github.com/user-attachments/assets/74765bb4-914b-47c8-83f6-5ab06c86583a" />

**Describe the solution you'd like**
Add a sentence to this in-product guidance for Linked Filters with link to docs like:

> If the filter has values that are from another question or model, or a custom list, then this filter can't be limited by another dashboard filter.
> For Native Questions use [Field Filters](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/sql-in-metabase/field-filters) to make Linked Filters available here.

**Describe alternatives you've considered**
- Leave as-is - adds friction to user learning about Field Filters and getting Linked Filters to work

**How important is this feature to you?**
Customers confused by this monthly. And can remove friction with guidance on how to get the result they want (i.e. a Linked FIlter via Field Filter).



Diff:
diff --git a/frontend/src/metabase/common/hooks/use-docs-url/use-docs-url.ts b/frontend/src/metabase/common/hooks/use-docs-url/use-docs-url.ts
index 3000ba00c333..2750d47d47b0 100644
--- a/frontend/src/metabase/common/hooks/use-docs-url/use-docs-url.ts
+++ b/frontend/src/metabase/common/hooks/use-docs-url/use-docs-url.ts
@@ -1,5 +1,9 @@
 import { useSelector } from "metabase/lib/redux";
-import { type UtmProps, getDocsUrl } from "metabase/selectors/settings";
+import {
+  type UtmProps,
+  getDocsUrl,
+  getLearnUrl,
+} from "metabase/selectors/settings";
 import { getShowMetabaseLinks } from "metabase/selectors/whitelabel";
 
 type DocsUtmProps = Omit<UtmProps, "utm_medium"> & {
@@ -30,3 +34,10 @@ export const useDocsUrl = (
 
   return { url, showMetabaseLinks };
 };
+
+export const useLearnUrl = (page: string) => {
+  const showMetabaseLinks = useSelector(getShowMetabaseLinks);
+  const url = getLearnUrl(page);
+
+  return { url, showMetabaseLinks };
+};
diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
index 28b3bcf32929..b5477b05c2f3 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
@@ -134,7 +134,6 @@ export type DashboardProps = {
     parameterId: ParameterId,
     filteringParameters: ParameterId[],
   ) => void;
-  showAddParameterPopover: () => void;
   removeParameter: (id: ParameterId) => void;
 
   onReplaceAllDashCardVisualizationSettings: (
@@ -519,7 +518,6 @@ function Dashboard(props: DashboardProps) {
 
               <DashboardSidebars
                 dashboard={dashboard}
-                showAddParameterPopover={props.showAddParameterPopover}
                 removeParameter={props.removeParameter}
                 addCardToDashboard={props.addCardToDashboard}
                 clickBehaviorSidebarDashcard={
diff --git a/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx b/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx
index 9de49da960c5..bf8d724a4a31 100644
--- a/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx
+++ b/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx
@@ -36,7 +36,6 @@ import { DashboardSettingsSidebar } from "./DashboardSettingsSidebar";
 
 interface DashboardSidebarsProps {
   dashboard: IDashboard;
-  showAddParameterPopover: () => void;
   removeParameter: (id: ParameterId) => void;
   addCardToDashboard: (opts: {
     dashId: DashboardId;
@@ -93,7 +92,6 @@ interface DashboardSidebarsProps {
 
 export function DashboardSidebars({
   dashboard,
-  showAddParameterPopover,
   removeParameter,
   addCardToDashboard,
   clickBehaviorSidebarDashcard,
@@ -200,7 +198,6 @@ export function DashboardSidebars({
           onChangeSourceConfig={setParameterSourceConfig}
           onChangeFilteringParameters={setParameterFilteringParameters}
           onRemoveParameter={removeParameter}
-          onShowAddParameterPopover={showAddParameterPopover}
           onClose={closeSidebar}
           onChangeRequired={setParameterRequired}
           onChangeTemporalUnits={setParameterTemporalUnits}
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
index 6b3b309108db..5c27ac3ab32d 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.tsx
@@ -41,7 +41,6 @@ import {
   setParameterType,
   setSharing,
   setSidebar,
-  showAddParameterPopover,
   toggleSidebar,
   updateDashboardAndCards,
 } from "metabase/dashboard/actions";
@@ -155,7 +154,6 @@ const mapDispatchToProps = {
   setParameterSourceType,
   setParameterSourceConfig,
   setParameterFilteringParameters,
-  showAddParameterPopover,
   removeParameter,
   onReplaceAllDashCardVisualizationSettings,
   onUpdateDashCardVisualizationSettings,
@@ -228,7 +226,6 @@ const DashboardApp = (props: DashboardAppProps) => {
     setParameterSourceType,
     setParameterSourceConfig,
     setParameterFilteringParameters,
-    showAddParameterPopover,
     removeParameter,
     onReplaceAllDashCardVisualizationSettings,
     onUpdateDashCardVisualizationSettings,
@@ -388,7 +385,6 @@ const DashboardApp = (props: DashboardAppProps) => {
         setParameterSourceType={setParameterSourceType}
         setParameterSourceConfig={setParameterSourceConfig}
         setParameterFilteringParameters={setParameterFilteringParameters}
-        showAddParameterPopover={showAddParameterPopover}
         removeParameter={removeParameter}
         onReplaceAllDashCardVisualizationSettings={
           onReplaceAllDashCardVisualizationSettings
diff --git a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx
index 19526f919514..52bae6065b30 100644
--- a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx
+++ b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx
@@ -3,7 +3,11 @@ import { useCallback, useMemo, useState } from "react";
 import { jt, t } from "ttag";
 
 import { skipToken, useGetFieldQuery, useGetTableQuery } from "metabase/api";
+import { useLearnUrl } from "metabase/common/hooks";
 import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
+import ExternalLink from "metabase/core/components/ExternalLink";
+import { showAddParameterPopover } from "metabase/dashboard/actions";
+import { useDispatch } from "metabase/lib/redux";
 import { Box, Switch } from "metabase/ui";
 import type { FieldId, Parameter, ParameterId } from "metabase-types/api";
 
@@ -29,14 +33,12 @@ export interface ParameterLinkedFiltersProps {
   parameter: Parameter;
   otherParameters: Parameter[];
   onChangeFilteringParameters: (filteringParameters: ParameterId[]) => void;
-  onShowAddParameterPopover: () => void;
 }
 
 export const ParameterLinkedFilters = ({
   parameter,
   otherParameters,
   onChangeFilteringParameters,
-  onShowAddParameterPopover,
 }: ParameterLinkedFiltersProps): JSX.Element => {
   const usableParameters = useMemo(
     () => otherParameters.filter(usableAsLinkedFilter),
@@ -50,7 +52,6 @@ export const ParameterLinkedFilters = ({
         usableParameters={usableParameters}
         parameter={parameter}
         onChangeFilteringParameters={onChangeFilteringParameters}
-        onShowAddParameterPopover={onShowAddParameterPopover}
       />
     </Box>
   );
@@ -60,26 +61,23 @@ function Content({
   usableParameters,
   parameter,
   onChangeFilteringParameters,
-  onShowAddParameterPopover,
 }: {
   usableParameters: Parameter[];
   parameter: Parameter;
   onChangeFilteringParameters: (filteringParameters: ParameterId[]) => void;
-  onShowAddParameterPopover: () => void;
 }) {
   if (usableParameters.length === 0) {
-    return (
-      <NoUsableParameters
-        onShowAddParameterPopover={onShowAddParameterPopover}
-      />
-    );
+    return <NoUsableParameters />;
   }
+
   if (parameter.values_source_type != null) {
     return <ParametersFromOtherSource />;
   }
+
   if (parameter.values_query_type === "none") {
     return <ParameterIsInputBoxType />;
   }
+
   return (
     <UsableParameters
       parameter={parameter}
@@ -89,11 +87,12 @@ function Content({
   );
 }
 
-function NoUsableParameters({
-  onShowAddParameterPopover,
-}: {
-  onShowAddParameterPopover: () => void;
-}): JSX.Element {
+function NoUsableParameters(): JSX.Element {
+  const dispatch = useDispatch();
+  const onShowAddParameterPopover = () => {
+    dispatch(showAddParameterPopover());
+  };
+
   return (
     <div>
       <SectionMessage>
@@ -119,11 +118,24 @@ function ParameterIsInputBoxType(): JSX.Element {
 }
 
 function ParametersFromOtherSource(): JSX.Element {
+  const { url: docsUrl, showMetabaseLinks } = useLearnUrl(
+    "metabase-basics/querying-and-dashboards/sql-in-metabase/field-filters",
+  );
+
   return (
     <div>
       <SectionMessage>
         {t`If the filter has values that are from another question or model, or a custom list, then this filter can't be limited by another dashboard filter.`}
       </SectionMessage>
+      {showMetabaseLinks && (
+        <SectionMessage>
+          {jt`For Native Questions use ${(
+            <ExternalLink key="field-filters" role="link" href={docsUrl}>
+              {t`Field Filters`}
+            </ExternalLink>
+          )} to make Linked Filters available here.`}
+        </SectionMessage>
+      )}
     </div>
   );
 }
diff --git a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.unit.spec.tsx b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.unit.spec.tsx
index c71e20091572..1edb27607dc0 100644
--- a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.unit.spec.tsx
+++ b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.unit.spec.tsx
@@ -1,6 +1,7 @@
 import userEvent from "@testing-library/user-event";
 
 import { renderWithProviders, screen } from "__support__/ui";
+import * as dashboardActions from "metabase/dashboard/actions/parameters";
 import { createMockUiParameter } from "metabase-lib/v1/parameters/mock";
 import type { UiParameter } from "metabase-lib/v1/parameters/types";
 import type { ValuesSourceType } from "metabase-types/api/parameters";
@@ -14,18 +15,16 @@ interface SetupOpts {
 
 const setup = ({ parameter, otherParameters }: SetupOpts) => {
   const onChangeFilteringParameters = jest.fn();
-  const onShowAddParameterPopover = jest.fn();
 
   renderWithProviders(
     <ParameterLinkedFilters
       parameter={parameter}
       otherParameters={otherParameters}
       onChangeFilteringParameters={onChangeFilteringParameters}
-      onShowAddParameterPopover={onShowAddParameterPopover}
     />,
   );
 
-  return { onChangeFilteringParameters, onShowAddParameterPopover };
+  return { onChangeFilteringParameters };
 };
 
 describe("ParameterLinkedFilters", () => {
@@ -102,4 +101,58 @@ describe("ParameterLinkedFilters", () => {
     ).toBeInTheDocument();
     expect(screen.queryByRole("switch")).not.toBeInTheDocument();
   });
+
+  it("should show external link to docs when the parameter has values from another question", () => {
+    setup({
+      parameter: createMockUiParameter({
+        id: "p1",
+        name: "P1",
+        values_source_type: "static-list",
+        values_source_config: { values: ["foo", "bar"] },
+      }),
+      otherParameters: [
+        createMockUiParameter({
+          id: "p2",
+          name: "P2",
+        }),
+      ],
+    });
+
+    expect(
+      screen.getByText(
+        "If the filter has values that are from another question or model, or a custom list, then this filter can't be limited by another dashboard filter.",
+      ),
+    ).toBeInTheDocument();
+    const link = screen.getByRole("link", { name: /Field Filters/i });
+    expect(link).toBeInTheDocument();
+    expect(link).toHaveAttribute(
+      "href",
+      "https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/sql-in-metabase/field-filters",
+    );
+  });
+
+  it("should show suggestion to limit filter's choices", async () => {
+    const showAddParameterPopoverSpy = jest.spyOn(
+      dashboardActions,
+      "showAddParameterPopover",
+    );
+    setup({
+      parameter: createMockUiParameter({
+        id: "p1",
+        name: "P1",
+      }),
+      otherParameters: [],
+    });
+
+    expect(screen.getByText("Limit this filter's choices")).toBeInTheDocument();
+
+    expect(
+      screen.getByText(
+        "If you have another dashboard filter, you can limit the choices that are listed for this filter based on the selection of the other one.",
+      ),
+    ).toBeInTheDocument();
+    await userEvent.click(screen.getByText("add another dashboard filter"));
+
+    expect(showAddParameterPopoverSpy).toHaveBeenCalledTimes(1);
+  });
 });
diff --git a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx
index 5a0b4269ea91..562304e9486b 100644
--- a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx
+++ b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx
@@ -60,7 +60,6 @@ export interface ParameterSidebarProps {
     temporalUnits: TemporalUnit[],
   ) => void;
   onRemoveParameter: (parameterId: ParameterId) => void;
-  onShowAddParameterPopover: () => void;
   onClose: () => void;
 }
 
@@ -78,7 +77,6 @@ export const ParameterSidebar = ({
   onChangeRequired,
   onChangeTemporalUnits,
   onRemoveParameter,
-  onShowAddParameterPopover,
   onClose,
   hasMapping,
 }: ParameterSidebarProps): JSX.Element => {
@@ -248,7 +246,6 @@ export const ParameterSidebar = ({
             parameter={parameter}
             otherParameters={otherParameters}
             onChangeFilteringParameters={handleFilteringParametersChange}
-            onShowAddParameterPopover={onShowAddParameterPopover}
           />
         </Tabs.Panel>
       </Tabs>
diff --git a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.unit.spec.tsx b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.unit.spec.tsx
index 0b4732b32a0e..7940d063cd60 100644
--- a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.unit.spec.tsx
+++ b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.unit.spec.tsx
@@ -57,7 +57,6 @@ const setup = ({
           onChangeFilteringParameters={jest.fn()}
           onRemoveParameter={jest.fn()}
           onChangeTemporalUnits={jest.fn()}
-          onShowAddParameterPopover={jest.fn()}
           onClose={jest.fn()}
           onChangeRequired={jest.fn()}
           hasMapping={hasMapping}


Reviews:
user: login: mngr
state: APPROVED
author_association: NONE
submitted_at: 2025-05-07T13:52:23Z
user: login: ranquild
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T19:27:06Z
user: login: ranquild
body: Small comment about missing translation
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T19:28:15Z
user: login: uladzimirdev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T19:30:52Z

Review comments:
diff_hunk: @@ -119,11 +118,24 @@ function ParameterIsInputBoxType(): JSX.Element {
 }
 
 function ParametersFromOtherSource(): JSX.Element {
+  const { url: docsUrl, showMetabaseLinks } = useLearnUrl(
+    "metabase-basics/querying-and-dashboards/sql-in-metabase/field-filters",
+  );
+
   return (
     <div>
       <SectionMessage>
         {t`If the filter has values that are from another question or model, or a custom list, then this filter can't be limited by another dashboard filter.`}
       </SectionMessage>
+      {showMetabaseLinks && (
+        <SectionMessage>
+          {jt`For Native Questions use ${(
+            <ExternalLink key="field-filters" role="link" href={docsUrl}>
+              Field Filters
path: frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx
user: login: ranquild
body: should use {t``} here
created_at: 2025-05-07T19:27:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,11 +118,24 @@ function ParameterIsInputBoxType(): JSX.Element {
 }
 
 function ParametersFromOtherSource(): JSX.Element {
+  const { url: docsUrl, showMetabaseLinks } = useLearnUrl(
+    "metabase-basics/querying-and-dashboards/sql-in-metabase/field-filters",
+  );
+
   return (
     <div>
       <SectionMessage>
         {t`If the filter has values that are from another question or model, or a custom list, then this filter can't be limited by another dashboard filter.`}
       </SectionMessage>
+      {showMetabaseLinks && (
+        <SectionMessage>
+          {jt`For Native Questions use ${(
+            <ExternalLink key="field-filters" role="link" href={docsUrl}>
+              Field Filters
path: frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx
user: login: uladzimirdev
body: fixed [2628ae8](https://github.com/metabase/metabase/pull/57687/commits/2628ae832d3f8408bf211c685dd16115ef975bd2)
created_at: 2025-05-07T19:30:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-05-07T09:40:26Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/24409_tests-3e7d5702-purple)](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/2-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > setup should ignore snowplow failures and work as normal</code> | Snowplow retry timed out due to null value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment&runConclusion=FAILURE&runId=3084c99c-4573-5f60-8110-622a89819dc5) |
| <code>scenarios > setup should send snowplow events</code> | The 'event' property is undefined when trying to read it. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment&runConclusion=FAILURE&runId=53dcd60e-0ade-5fec-8a75-953183097c09) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57687?repo=metabase/metabase&commitHash=3e7d5702cf7bb9faabb7b732d899a952f24e626c&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T07:15:20Z
body: 👋 Deploying a preview environment for commit 3e7d5702cf7bb9faabb7b732d899a952f24e626c.
✅ Preview:
    https://pr57687.coredev.metabase.com
🗒️ [login instructions](https://www.notion.so/metabase/Ephemeral-PR-Environments-c74a3710c87a460dbf4fc0007a001458)
<!-- Sticky Pull Request Comment -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T07:21:56Z
body: 
  ## e2e tests failed on `3e7d5702cf7bb9faabb7b732d899a952f24e626c-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/14900693721?pr=57687)

  File | Test Name
  ---- | ---------
  `setup.cy.spec.ts` | (_flaky_) scenarios > setup > should send snowplow events
`setup.cy.spec.ts` | (_flaky_) scenarios > setup > should ignore snowplow failures and work as normal

author_association: CONTRIBUTOR