This is an automated email from the ASF dual-hosted git repository.

kgabryje pushed a commit to branch what-if
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 54518daea0a23fab673769490470fe12745f1b83
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Tue Dec 16 22:39:06 2025 +0100

    banner
---
 .../src/components/Icons/AntdEnhanced.tsx          |   2 +
 .../src/components/Chart/chartAction.js            |  16 ++
 .../src/components/Chart/chartReducer.ts           |  27 ++++
 .../DashboardBuilder/DashboardBuilder.tsx          |  24 ++-
 .../dashboard/components/WhatIfBanner/index.tsx    | 167 +++++++++++++++++++++
 .../dashboard/components/WhatIfDrawer/index.tsx    |  18 ++-
 6 files changed, 244 insertions(+), 10 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
index a079b54c16..8ff6302d3b 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
@@ -66,6 +66,7 @@ import {
   ExclamationCircleOutlined,
   ExclamationCircleFilled,
   ExpandOutlined,
+  ExperimentOutlined,
   EyeOutlined,
   EyeInvisibleOutlined,
   FallOutlined,
@@ -204,6 +205,7 @@ const AntdIcons = {
   ExclamationCircleOutlined,
   ExclamationCircleFilled,
   ExpandOutlined,
+  ExperimentOutlined,
   EyeOutlined,
   EyeInvisibleOutlined,
   FacebookOutlined,
diff --git a/superset-frontend/src/components/Chart/chartAction.js 
b/superset-frontend/src/components/Chart/chartAction.js
index bd9bd94a56..92564b235c 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -603,6 +603,22 @@ export function refreshChart(chartKey, force, dashboardId) 
{
   };
 }
 
+// What-If caching actions
+export const SAVE_ORIGINAL_CHART_DATA = 'SAVE_ORIGINAL_CHART_DATA';
+export function saveOriginalChartData(key) {
+  return { type: SAVE_ORIGINAL_CHART_DATA, key };
+}
+
+export const RESTORE_ORIGINAL_CHART_DATA = 'RESTORE_ORIGINAL_CHART_DATA';
+export function restoreOriginalChartData(key) {
+  return { type: RESTORE_ORIGINAL_CHART_DATA, key };
+}
+
+export const CLEAR_ORIGINAL_CHART_DATA = 'CLEAR_ORIGINAL_CHART_DATA';
+export function clearOriginalChartData(key) {
+  return { type: CLEAR_ORIGINAL_CHART_DATA, key };
+}
+
 export const getDatasourceSamples = async (
   datasourceType,
   datasourceId,
diff --git a/superset-frontend/src/components/Chart/chartReducer.ts 
b/superset-frontend/src/components/Chart/chartReducer.ts
index 081bd41547..b3bc0e7a54 100644
--- a/superset-frontend/src/components/Chart/chartReducer.ts
+++ b/superset-frontend/src/components/Chart/chartReducer.ts
@@ -177,6 +177,33 @@ export default function chartReducer(
         annotationQuery,
       };
     },
+    [actions.SAVE_ORIGINAL_CHART_DATA](state) {
+      // Only save if we don't already have cached data
+      if (state.originalQueriesResponse) {
+        return state;
+      }
+      return {
+        ...state,
+        originalQueriesResponse: state.queriesResponse,
+      };
+    },
+    [actions.RESTORE_ORIGINAL_CHART_DATA](state) {
+      if (!state.originalQueriesResponse) {
+        return state;
+      }
+      return {
+        ...state,
+        queriesResponse: state.originalQueriesResponse,
+        originalQueriesResponse: null,
+        chartStatus: 'success',
+      };
+    },
+    [actions.CLEAR_ORIGINAL_CHART_DATA](state) {
+      return {
+        ...state,
+        originalQueriesResponse: null,
+      };
+    },
   };
 
   /* eslint-disable no-param-reassign */
diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 3961b1e63a..6a02ed3e57 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -57,6 +57,7 @@ import {
 } from 'src/dashboard/util/constants';
 import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
 import WhatIfPanel from 'src/dashboard/components/WhatIfDrawer';
+import WhatIfBanner from 'src/dashboard/components/WhatIfBanner';
 import { useUiConfig } from 'src/components/UiConfigContext';
 import ResizableSidebar from 'src/components/ResizableSidebar';
 import {
@@ -275,12 +276,15 @@ const StyledDashboardContent = styled.div<{
   editMode: boolean;
   marginLeft: number;
   marginRight: number;
+  hasWhatIfPanel: boolean;
 }>`
-  ${({ theme, editMode, marginLeft, marginRight }) => css`
+  ${({ theme, editMode, marginLeft, marginRight, hasWhatIfPanel }) => css`
     background-color: ${theme.colorBgLayout};
-    display: flex;
-    flex-direction: row;
-    flex-wrap: nowrap;
+    display: grid;
+    grid-template-columns: 1fr ${hasWhatIfPanel ? 'auto' : ''} ${editMode
+      ? 'auto'
+      : ''};
+    grid-template-rows: auto 1fr;
     height: auto;
     flex: 1;
 
@@ -290,13 +294,13 @@ const StyledDashboardContent = styled.div<{
     }
 
     .grid-container {
-      /* without this, the grid will not get smaller upon toggling the builder 
panel on */
-      width: 0;
-      flex: 1;
+      grid-column: 1;
+      grid-row: 2;
       position: relative;
       margin: ${theme.sizeUnit * 4}px;
       margin-left: ${marginLeft}px;
       margin-right: ${marginRight}px;
+      min-width: 0; /* Prevent grid blowout */
 
       ${editMode &&
       `
@@ -312,6 +316,8 @@ const StyledDashboardContent = styled.div<{
     }
 
     .dashboard-builder-sidepane {
+      grid-column: 2;
+      grid-row: 1 / -1; /* Span all rows */
       width: ${BUILDER_SIDEPANEL_WIDTH}px;
       z-index: 1;
     }
@@ -688,7 +694,9 @@ const DashboardBuilder = () => {
             editMode={editMode}
             marginLeft={dashboardContentMarginLeft}
             marginRight={dashboardContentMarginRight}
+            hasWhatIfPanel={!editMode && whatIfPanelOpen}
           >
+            {!editMode && <WhatIfBanner topOffset={barTopOffset} />}
             {showDashboard ? (
               missingInitialFilters.length > 0 ? (
                 <div
@@ -698,6 +706,8 @@ const DashboardBuilder = () => {
                     align-items: center;
                     justify-content: center;
                     flex: 1;
+                    grid-column: 1;
+                    grid-row: 2;
                     & div {
                       width: 500px;
                     }
diff --git a/superset-frontend/src/dashboard/components/WhatIfBanner/index.tsx 
b/superset-frontend/src/dashboard/components/WhatIfBanner/index.tsx
new file mode 100644
index 0000000000..fa01d1f45a
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/WhatIfBanner/index.tsx
@@ -0,0 +1,167 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useCallback, useMemo } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { t } from '@superset-ui/core';
+import { styled, useTheme } from '@apache-superset/core/ui';
+import { Button } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { clearWhatIfModifications } from 
'src/dashboard/actions/dashboardState';
+import { restoreOriginalChartData } from 'src/components/Chart/chartAction';
+import { getNumericColumnsForDashboard } from 'src/dashboard/util/whatIf';
+import { RootState, WhatIfModification } from 'src/dashboard/types';
+
+/**
+ * Banner container positioned at top of dashboard content, next to the 
What-If panel.
+ *
+ * Layout strategy:
+ * - Grid positioning: column 1, row 1 (above dashboard content, next to panel)
+ * - position: sticky with top: topOffset to stick below the dashboard header
+ * - z-index: 10 to stay above chart content while scrolling
+ * - align-self: start prevents the banner from stretching vertically
+ */
+const BannerContainer = styled.div<{ topOffset: number }>`
+  grid-column: 1;
+  grid-row: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: ${({ theme }) => theme.sizeUnit * 3}px;
+  padding: ${({ theme }) => theme.sizeUnit * 2}px
+    ${({ theme }) => theme.sizeUnit * 4}px;
+  margin-bottom: 0;
+  background-color: ${({ theme }) => theme.colorSuccessBg};
+  border-bottom: 1px solid ${({ theme }) => theme.colorSuccessBorder};
+  position: sticky;
+  top: ${({ topOffset }) => topOffset}px;
+  z-index: 10;
+  align-self: start;
+`;
+
+const BannerContent = styled.div`
+  display: flex;
+  align-items: center;
+  gap: ${({ theme }) => theme.sizeUnit * 2}px;
+  color: ${({ theme }) => theme.colorSuccess};
+  font-weight: ${({ theme }) => theme.fontWeightStrong};
+`;
+
+const Separator = styled.span`
+  color: ${({ theme }) => theme.colorSuccess};
+  opacity: 0.5;
+`;
+
+const ExitButton = styled(Button)`
+  && {
+    color: ${({ theme }) => theme.colorSuccess};
+    border-color: ${({ theme }) => theme.colorSuccess};
+    background-color: ${({ theme }) => theme.colorSuccessBg};
+
+    &:hover {
+      color: ${({ theme }) => theme.colorSuccessHover};
+      border-color: ${({ theme }) => theme.colorSuccessHover};
+      background-color: ${({ theme }) => theme.colorSuccessBgHover};
+    }
+  }
+`;
+
+const formatPercentageChange = (multiplier: number): string => {
+  const percentChange = (multiplier - 1) * 100;
+  const sign = percentChange >= 0 ? '+' : '';
+  return `${sign}${Math.round(percentChange)}%`;
+};
+
+interface WhatIfBannerProps {
+  topOffset: number;
+}
+
+const WhatIfBanner = ({ topOffset }: WhatIfBannerProps) => {
+  const theme = useTheme();
+  const dispatch = useDispatch();
+
+  const whatIfModifications = useSelector<RootState, WhatIfModification[]>(
+    state => state.dashboardState.whatIfModifications || [],
+  );
+
+  const charts = useSelector((state: RootState) => state.charts);
+  const datasources = useSelector((state: RootState) => state.datasources);
+
+  const numericColumns = useMemo(
+    () => getNumericColumnsForDashboard(charts, datasources),
+    [charts, datasources],
+  );
+
+  const columnToChartIds = useMemo(() => {
+    const map = new Map<string, number[]>();
+    numericColumns.forEach(col => {
+      map.set(col.columnName, col.usedByChartIds);
+    });
+    return map;
+  }, [numericColumns]);
+
+  const handleExitWhatIf = useCallback(() => {
+    const affectedChartIds = new Set<number>();
+    whatIfModifications.forEach(mod => {
+      const chartIds = columnToChartIds.get(mod.column) || [];
+      chartIds.forEach(id => affectedChartIds.add(id));
+    });
+
+    // Clear what-if modifications
+    dispatch(clearWhatIfModifications());
+
+    // Restore original chart data from cache (instant, no re-query needed)
+    affectedChartIds.forEach(chartId => {
+      dispatch(restoreOriginalChartData(chartId));
+    });
+  }, [dispatch, whatIfModifications, columnToChartIds]);
+
+  if (whatIfModifications.length === 0) {
+    return null;
+  }
+
+  const modification = whatIfModifications[0];
+  const percentageChange = formatPercentageChange(modification.multiplier);
+
+  return (
+    <BannerContainer data-test="what-if-banner" topOffset={topOffset}>
+      <BannerContent>
+        <Icons.ExperimentOutlined iconSize="m" iconColor={theme.colorSuccess} 
/>
+        <span>{t('What-if mode active')}</span>
+        <Separator>|</Separator>
+        <span>
+          {t(
+            'Showing simulated data with %s %s',
+            modification.column,
+            percentageChange,
+          )}
+        </span>
+      </BannerContent>
+      <ExitButton
+        buttonSize="small"
+        onClick={handleExitWhatIf}
+        data-test="exit-what-if-button"
+      >
+        <Icons.CloseOutlined iconSize="s" />
+        {t('Exit what-if mode')}
+      </ExitButton>
+    </BannerContainer>
+  );
+};
+
+export default WhatIfBanner;
diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
index d7eca32cc8..6caade5384 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
@@ -24,7 +24,10 @@ import { Button, Select } from 
'@superset-ui/core/components';
 import Slider from '@superset-ui/core/components/Slider';
 import { Icons } from '@superset-ui/core/components/Icons';
 import { setWhatIfModifications } from 'src/dashboard/actions/dashboardState';
-import { triggerQuery } from 'src/components/Chart/chartAction';
+import {
+  triggerQuery,
+  saveOriginalChartData,
+} from 'src/components/Chart/chartAction';
 import { getNumericColumnsForDashboard } from 'src/dashboard/util/whatIf';
 import { RootState, WhatIfColumn } from 'src/dashboard/types';
 
@@ -35,6 +38,8 @@ const SLIDER_MAX = 50;
 const SLIDER_DEFAULT = 0;
 
 const PanelContainer = styled.div<{ topOffset: number }>`
+  grid-column: 2;
+  grid-row: 1 / -1; /* Span all rows */
   width: ${WHAT_IF_PANEL_WIDTH}px;
   min-width: ${WHAT_IF_PANEL_WIDTH}px;
   background-color: ${({ theme }) => theme.colorBgContainer};
@@ -45,7 +50,7 @@ const PanelContainer = styled.div<{ topOffset: number }>`
   position: sticky;
   top: ${({ topOffset }) => topOffset}px;
   height: calc(100vh - ${({ topOffset }) => topOffset}px);
-  align-self: flex-start;
+  align-self: start;
   z-index: 10;
 `;
 
@@ -162,6 +167,14 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
 
     const multiplier = 1 + sliderValue / 100;
 
+    // Get affected chart IDs
+    const affectedChartIds = columnToChartIds.get(selectedColumn) || [];
+
+    // Save original chart data before applying what-if modifications
+    affectedChartIds.forEach(chartId => {
+      dispatch(saveOriginalChartData(chartId));
+    });
+
     // Set the what-if modifications in Redux state
     dispatch(
       setWhatIfModifications([
@@ -173,7 +186,6 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
     );
 
     // Trigger queries for all charts that use the selected column
-    const affectedChartIds = columnToChartIds.get(selectedColumn) || [];
     affectedChartIds.forEach(chartId => {
       dispatch(triggerQuery(true, chartId));
     });

Reply via email to