apache / superset

Apache Superset is a Data Visualization and Data Exploration Platform
https://superset.apache.org/
Apache License 2.0
61.98k stars 13.59k forks source link

Dashboard Filter 'useFilterScope' Error #30418

Open Stanhoucke opened 2 hours ago

Stanhoucke commented 2 hours ago

Bug description

The bug is that when interacting with the filters in an embedded dashboard, the filters will be replaced with an Unexpected Error message. The user is forced to refresh the dashboard, losing the state of their filters prior to the error.

image

Steps to reproduce

  1. Load an embedded dashboard
  2. Interact with the filters without clicking Apply Filters
  3. After a certain amount of interactions, the filters will show the above error

I could not reliably find a way or pattern to trigger this error. It may take a minute or two of selecting, deselecting, updating some filter options before seeing the error.

The browser console displays the following TypeError related to the useFilterScope function.

image

Screenshots/recordings

No response

Superset version

4.0.2

Python version

3.10

Node version

16

Browser

Chrome

Additional context

No response

Checklist

dosubot[bot] commented 2 hours ago

Hey there, @Stanhoucke! I'm here to help you with bug fixes, answer your questions, and guide you to become a contributor. Let's tackle this issue together!

I found two similar closed issues that might be relevant to your problem:

  1. Superset - Closed on April 03, 2024.
  2. Upgrading from 2.0.0 to 2.0.1rc2 causes an error when setting owner and roles on dashboard - Closed on February 23, 2024. The suggested solution was to upgrade to version 2.0.1, which fixed the issue.

To resolve the useFilterScope TypeError in an embedded dashboard when interacting with filters in Superset version 4.0.2, ensure that the useFilterScope function is correctly implemented and used. Here is the relevant part of the code:

import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Filter, t } from '@superset-ui/core';
import { Layout, LayoutItem, RootState } from 'src/dashboard/types';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';

const extractTabLabel = (tab?: LayoutItem) =>
  tab?.meta?.text || tab?.meta?.defaultText || '';
const extractChartLabel = (chart?: LayoutItem) =>
  chart?.meta?.sliceNameOverride || chart?.meta?.sliceName || chart?.id || '';

export const useFilterScope = (filter: Filter) => {
  const layout = useSelector<RootState, Layout>(
    state => state.dashboardLayout.present,
  );
  const chartIds = useChartIds();

  return useMemo(() => {
    let topLevelTabs: string[] | undefined;
    const topElementId = layout[DASHBOARD_ROOT_ID].children[0];
    if (topElementId.startsWith('TABS-')) {
      topLevelTabs = layout[topElementId].children;
    }

    if (filter.scope.rootPath.length === 0) {
      return undefined;
    }

    if (
      filter.scope.excluded.length === 0 &&
      (filter.scope.rootPath[0] === DASHBOARD_ROOT_ID ||
        topLevelTabs?.every(topLevelTab =>
          filter.scope.rootPath.includes(topLevelTab),
        ))
    ) {
      return { all: [t('All charts')] };
    }

    if (filter.scope.excluded.length === 0 && topLevelTabs) {
      return {
        tabs: filter.scope.rootPath
          .map(tabId => extractTabLabel(layout[tabId]))
          .filter(Boolean),
      };
    }

    const layoutCharts = Object.values(layout).filter(
      layoutElement => layoutElement.type === CHART_TYPE,
    );

    if (filter.scope.rootPath[0] === DASHBOARD_ROOT_ID) {
      return {
        charts: chartIds
          .filter(chartId => !filter.scope.excluded.includes(chartId))
          .map(chartId => {
            const layoutElement = layoutCharts.find(
              layoutChart => layoutChart.meta.chartId === chartId,
            );
            return extractChartLabel(layoutElement);
          })
          .filter(Boolean),
      };
    }

    if (topLevelTabs) {
      const topLevelTabsInFullScope = [...filter.scope.rootPath];
      const layoutChartElementsInTabsInScope = layoutCharts.filter(element =>
        element.parents.some(parent =>
          topLevelTabsInFullScope.includes(parent),
        ),
      );

      filter.scope.excluded.forEach(chartId => {
        const excludedIndex = topLevelTabsInFullScope.findIndex(tabId =>
          layoutChartElementsInTabsInScope
            .find(chart => chart.meta.chartId === chartId)
            ?.parents.includes(tabId),
        );
        if (excludedIndex > -1) {
          topLevelTabsInFullScope.splice(excludedIndex, 1);
        }
      });

      const chartsInExcludedTabs = chartIds
        .filter(chartId => !filter.scope.excluded.includes(chartId))
        .reduce((acc: LayoutItem[], chartId) => {
          const layoutChartElementInExcludedTab =
            layoutChartElementsInTabsInScope.find(
              element =>
                element.meta.chartId === chartId &&
                element.parents.every(
                  parent => !topLevelTabsInFullScope.includes(parent),
                ),
            );
          if (layoutChartElementInExcludedTab) {
            acc.push(layoutChartElementInExcludedTab);
          }
          return acc;
        }, []);

      return {
        tabs: topLevelTabsInFullScope
          .map(tabId => extractTabLabel(layout[tabId]))
          .filter(Boolean),
        charts: chartsInExcludedTabs.map(extractChartLabel).filter(Boolean),
      };
    }

    return undefined;
  }, [chartIds, filter.scope.excluded, filter.scope.rootPath, layout]);
};

Ensure that the useFilterScope function is correctly used in your component. If you encounter a TypeError, it might be due to incorrect usage or missing dependencies in the context where useFilterScope is called.

Here is an example of how to use useFilterScope correctly:

import React from 'react';
import { useFilterScope } from 'path-to-your-state-file';

const MyDashboardComponent = ({ filter }) => {
  const filterScope = useFilterScope(filter);

  return (
    <div>
      {/* Render your filters based on the scope */}
      {filterScope?.all && <div>All charts are in scope</div>}
      {filterScope?.tabs && filterScope.tabs.map(tab => <div key={tab}>{tab}</div>)}
      {filterScope?.charts && filterScope.charts.map(chart => <div key={chart}>{chart}</div>)}
    </div>
  );
};

Additionally, ensure that the filters passed to useFilterScope are correctly structured and that the necessary state selectors (useSelector, useChartIds, etc.) are functioning as expected. You may also want to check the implementation of the ScopeRow component and related utilities like useTruncation to ensure they are correctly set up and integrated [1][2][3][4][5].

To continue talking to Dosu, mention @dosu.