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 d37f73490d58aa85c07671b26e824b1b91a73e45
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Fri Dec 19 10:45:44 2025 +0100

    Load simulations from url and whatif management page
---
 .../DashboardBuilder/DashboardBuilder.tsx          |  29 +-
 .../components/WhatIfDrawer/WhatIfHeaderMenu.tsx   |  34 +-
 .../dashboard/components/WhatIfDrawer/index.tsx    | 117 ++++++-
 .../dashboard/components/WhatIfDrawer/styles.ts    |   7 +-
 .../components/WhatIfDrawer/useWhatIfFilters.ts    |   2 +
 .../WhatIfSimulationList/EditSimulationModal.tsx   | 383 +++++++++++++++++++++
 .../src/pages/WhatIfSimulationList/index.tsx       | 170 ++++++++-
 superset/initialization/__init__.py                |  11 +
 superset/views/what_if.py                          |  39 +++
 9 files changed, 735 insertions(+), 57 deletions(-)

diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 0e030815a0..6b913d8c67 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -19,6 +19,7 @@
 /* eslint-env browser */
 import cx from 'classnames';
 import { memo, useCallback, useEffect, useMemo, useRef, useState } from 
'react';
+import { useLocation, useHistory } from 'react-router-dom';
 import { addAlpha, JsonObject, t, useElementOnScreen } from 
'@superset-ui/core';
 import { css, styled, useTheme } from '@apache-superset/core/ui';
 import { useDispatch, useSelector } from 'react-redux';
@@ -392,9 +393,31 @@ const DashboardBuilder = () => {
     ({ dashboardState }) => dashboardState.whatIfPanelOpen ?? false,
   );
 
+  // Read simulation ID from URL query parameter
+  const location = useLocation();
+  const history = useHistory();
+  const initialSimulationId = useMemo(() => {
+    const params = new URLSearchParams(location.search);
+    const simParam = params.get('simulation');
+    return simParam ? parseInt(simParam, 10) : null;
+  }, [location.search]);
+
+  // Auto-open What-If panel if simulation ID is in URL
+  useEffect(() => {
+    if (initialSimulationId && !whatIfPanelOpen) {
+      dispatch(toggleWhatIfPanel(true));
+    }
+  }, [initialSimulationId, dispatch, whatIfPanelOpen]);
+
   const handleCloseWhatIfPanel = useCallback(() => {
     dispatch(toggleWhatIfPanel(false));
-  }, [dispatch]);
+    // Clear simulation param from URL when closing
+    const params = new URLSearchParams(location.search);
+    if (params.has('simulation')) {
+      params.delete('simulation');
+      history.replace({ search: params.toString() });
+    }
+  }, [dispatch, location.search, history]);
 
   const handleChangeTab = useCallback(
     ({ pathToTabIndex }: { pathToTabIndex: string[] }) => {
@@ -722,10 +745,12 @@ const DashboardBuilder = () => {
             ) : (
               <Loading />
             )}
-            {!editMode && whatIfPanelOpen && (
+            {!editMode && (
               <WhatIfPanel
+                visible={whatIfPanelOpen}
                 onClose={handleCloseWhatIfPanel}
                 topOffset={barTopOffset}
+                initialSimulationId={initialSimulationId}
               />
             )}
             {editMode && <BuilderComponentPane topOffset={barTopOffset} />}
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx
index 1b0a7d5de7..b4a9a0b2df 100644
--- 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx
+++ 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useState, useEffect, useCallback, Key } from 'react';
+import { useState, useCallback, Key } from 'react';
 import { t } from '@superset-ui/core';
 import { css, useTheme } from '@apache-superset/core/ui';
 import { Menu } from '@superset-ui/core/components/Menu';
@@ -28,7 +28,7 @@ import {
 } from '@superset-ui/core/components';
 import { Icons } from '@superset-ui/core/components/Icons';
 import { Link } from 'react-router-dom';
-import { fetchSimulations, WhatIfSimulation } from './whatIfApi';
+import { WhatIfSimulation } from './whatIfApi';
 
 enum MenuKeys {
   LoadSimulation = 'load-simulation',
@@ -38,14 +38,13 @@ enum MenuKeys {
 }
 
 interface WhatIfHeaderMenuProps {
-  dashboardId: number;
   selectedSimulation: WhatIfSimulation | null;
   onSelectSimulation: (simulation: WhatIfSimulation | null) => void;
   onSaveClick: () => void;
   onSaveAsNewClick: () => void;
   hasModifications: boolean;
-  refreshTrigger?: number;
-  addDangerToast: (msg: string) => void;
+  simulations: WhatIfSimulation[];
+  simulationsLoading: boolean;
 }
 
 const VerticalDotsTrigger = () => {
@@ -65,35 +64,16 @@ const VerticalDotsTrigger = () => {
 };
 
 const WhatIfHeaderMenu = ({
-  dashboardId,
   selectedSimulation,
   onSelectSimulation,
   onSaveClick,
   onSaveAsNewClick,
   hasModifications,
-  refreshTrigger,
-  addDangerToast,
+  simulations,
+  simulationsLoading,
 }: WhatIfHeaderMenuProps) => {
   const theme = useTheme();
   const [isDropdownVisible, setIsDropdownVisible] = useState(false);
-  const [simulations, setSimulations] = useState<WhatIfSimulation[]>([]);
-  const [loading, setLoading] = useState(false);
-
-  const loadSimulations = useCallback(async () => {
-    setLoading(true);
-    try {
-      const result = await fetchSimulations(dashboardId);
-      setSimulations(result);
-    } catch (error) {
-      addDangerToast(t('Failed to load saved simulations'));
-    } finally {
-      setLoading(false);
-    }
-  }, [dashboardId, addDangerToast]);
-
-  useEffect(() => {
-    loadSimulations();
-  }, [loadSimulations, refreshTrigger]);
 
   const handleMenuClick = useCallback(
     ({ key }: { key: Key }) => {
@@ -189,7 +169,7 @@ const WhatIfHeaderMenu = ({
     {
       type: 'submenu' as const,
       key: MenuKeys.LoadSimulation,
-      label: loading ? t('Loading...') : t('Load simulation'),
+      label: simulationsLoading ? t('Loading...') : t('Load simulation'),
       icon: <Icons.FolderOpenOutlined />,
       children: simulationMenuItems,
     },
diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
index ed3cbf9812..13171daeb5 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useSelector } from 'react-redux';
 import { t } from '@superset-ui/core';
 import { css, Alert, useTheme } from '@apache-superset/core/ui';
@@ -34,6 +34,7 @@ import { useToasts } from 
'src/components/MessageToasts/withToasts';
 import { useNumericColumns } from 'src/dashboard/util/useNumericColumns';
 import { RootState, Datasource } from 'src/dashboard/types';
 import WhatIfAIInsights from './WhatIfAIInsights';
+import { useAllChartsLoaded } from './useChartComparison';
 import HarryPotterWandLoader from './HarryPotterWandLoader';
 import FilterButton from './FilterButton';
 import ModificationsDisplay from './ModificationsDisplay';
@@ -41,7 +42,7 @@ import WhatIfHeaderMenu from './WhatIfHeaderMenu';
 import SaveSimulationModal from './SaveSimulationModal';
 import { useWhatIfFilters } from './useWhatIfFilters';
 import { useWhatIfApply } from './useWhatIfApply';
-import { WhatIfSimulation } from './whatIfApi';
+import { WhatIfSimulation, fetchSimulations } from './whatIfApi';
 import {
   SLIDER_MIN,
   SLIDER_MAX,
@@ -71,11 +72,18 @@ import {
 export { WHAT_IF_PANEL_WIDTH };
 
 interface WhatIfPanelProps {
+  visible: boolean;
   onClose: () => void;
   topOffset: number;
+  initialSimulationId?: number | null;
 }
 
-const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => {
+const WhatIfPanel = ({
+  visible,
+  onClose,
+  topOffset,
+  initialSimulationId,
+}: WhatIfPanelProps) => {
   const theme = useTheme();
   const { addSuccessToast, addDangerToast } = useToasts();
 
@@ -93,7 +101,11 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
   const [selectedSimulation, setSelectedSimulation] =
     useState<WhatIfSimulation | null>(null);
   const [saveModalVisible, setSaveModalVisible] = useState(false);
-  const [simulationRefreshTrigger, setSimulationRefreshTrigger] = useState(0);
+  const [simulations, setSimulations] = useState<WhatIfSimulation[]>([]);
+  const [simulationsLoading, setSimulationsLoading] = useState(false);
+
+  // Track if initial simulation from URL has been loaded
+  const initialSimulationLoadedRef = useRef(false);
 
   // Custom hook for filter management
   const {
@@ -101,6 +113,7 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
     filterPopoverVisible,
     currentAdhocFilter,
     setFilterPopoverVisible,
+    setFilters,
     handleOpenFilterPopover,
     handleEditFilter,
     handleFilterChange,
@@ -131,9 +144,19 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
   });
 
   // Get numeric columns and datasources
-  const { numericColumns } = useNumericColumns();
+  const { numericColumns, columnToChartIds } = useNumericColumns();
   const datasources = useSelector((state: RootState) => state.datasources);
 
+  // Get all chart IDs that could be affected by what-if analysis
+  const allDashboardChartIds = useMemo(() => {
+    const chartIds = new Set<number>();
+    columnToChartIds.forEach(ids => ids.forEach(id => chartIds.add(id)));
+    return Array.from(chartIds);
+  }, [columnToChartIds]);
+
+  // Check if all dashboard charts have completed their initial load
+  const initialChartsLoaded = useAllChartsLoaded(allDashboardChartIds);
+
   // Column options for the select dropdown
   const columnOptions = useMemo(
     () =>
@@ -195,6 +218,13 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
         setSliderValue((firstMod.multiplier - 1) * 100);
         setEnableCascadingEffects(simulation.cascadingEffectsEnabled);
 
+        // Load filters from the first modification
+        if (firstMod.filters && firstMod.filters.length > 0) {
+          setFilters(firstMod.filters);
+        } else {
+          clearFilters();
+        }
+
         // Convert to extended modifications with isAISuggested flag
         // First modification is the user's, rest are AI-suggested
         const extendedModifications = simulation.modifications.map(
@@ -217,14 +247,70 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
         clearModifications();
       }
     },
-    [clearFilters, loadModificationsDirectly, clearModifications],
+    [clearFilters, setFilters, loadModificationsDirectly, clearModifications],
   );
 
+  // Load simulations list from API
+  const loadSimulations = useCallback(async () => {
+    if (!dashboardId) return;
+    setSimulationsLoading(true);
+    try {
+      const result = await fetchSimulations(dashboardId);
+      setSimulations(result);
+    } catch (error) {
+      addDangerToast(t('Failed to load saved simulations'));
+    } finally {
+      setSimulationsLoading(false);
+    }
+  }, [dashboardId, addDangerToast]);
+
+  // Fetch simulations when dashboard is ready
+  useEffect(() => {
+    if (dashboardId) {
+      loadSimulations();
+    }
+  }, [dashboardId, loadSimulations]);
+
+  // Load initial simulation from URL parameter
+  // Wait until:
+  // 1. simulations are loaded (from the earlier useEffect)
+  // 2. columnToChartIds is populated (chart metadata is available)
+  // 3. initialChartsLoaded is true (charts have finished their initial 
queries)
+  // This ensures we can properly save original chart data before applying 
what-if modifications
+  useEffect(() => {
+    if (
+      initialSimulationId &&
+      !initialSimulationLoadedRef.current &&
+      simulations.length > 0 &&
+      columnToChartIds.size > 0 &&
+      initialChartsLoaded
+    ) {
+      initialSimulationLoadedRef.current = true;
+      const simulation = simulations.find(s => s.id === initialSimulationId);
+      if (simulation) {
+        handleLoadSimulation(simulation);
+      } else {
+        addDangerToast(t('Simulation not found'));
+      }
+    }
+  }, [
+    initialSimulationId,
+    simulations,
+    addDangerToast,
+    columnToChartIds.size,
+    initialChartsLoaded,
+    handleLoadSimulation,
+  ]);
+
   // Handle saving a simulation
-  const handleSaveSimulation = useCallback((simulation: WhatIfSimulation) => {
-    setSelectedSimulation(simulation);
-    setSimulationRefreshTrigger(prev => prev + 1);
-  }, []);
+  const handleSaveSimulation = useCallback(
+    (simulation: WhatIfSimulation) => {
+      setSelectedSimulation(simulation);
+      // Refresh the simulations list to include the newly saved simulation
+      loadSimulations();
+    },
+    [loadSimulations],
+  );
 
   // Track if we're saving as new (vs updating existing)
   const [isSavingAsNew, setIsSavingAsNew] = useState(false);
@@ -248,7 +334,11 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
     !selectedColumn || sliderValue === SLIDER_DEFAULT || isLoadingSuggestions;
 
   return (
-    <PanelContainer data-test="what-if-panel" topOffset={topOffset}>
+    <PanelContainer
+      data-test="what-if-panel"
+      topOffset={topOffset}
+      visible={visible}
+    >
       <PanelHeader>
         <PanelTitle>
           <Icons.StarFilled
@@ -261,14 +351,13 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
         </PanelTitle>
         <HeaderButtonsContainer>
           <WhatIfHeaderMenu
-            dashboardId={dashboardId}
             selectedSimulation={selectedSimulation}
             onSelectSimulation={handleLoadSimulation}
             onSaveClick={handleOpenSaveModal}
             onSaveAsNewClick={handleOpenSaveAsNewModal}
             hasModifications={appliedModifications.length > 0}
-            refreshTrigger={simulationRefreshTrigger}
-            addDangerToast={addDangerToast}
+            simulations={simulations}
+            simulationsLoading={simulationsLoading}
           />
           <CloseButton onClick={onClose} aria-label={t('Close')}>
             <Icons.CloseOutlined iconSize="m" />
diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
index 4f43c41e30..62b6f55262 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
@@ -21,14 +21,17 @@ import { styled } from '@apache-superset/core/ui';
 import { Button } from '@superset-ui/core/components';
 import { WHAT_IF_PANEL_WIDTH } from './constants';
 
-export const PanelContainer = styled.div<{ topOffset: number }>`
+export const PanelContainer = styled.div<{
+  topOffset: number;
+  visible: boolean;
+}>`
   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};
   border-left: 1px solid ${({ theme }) => theme.colorBorderSecondary};
-  display: flex;
+  display: ${({ visible }) => (visible ? 'flex' : 'none')};
   flex-direction: column;
   overflow: hidden;
   position: sticky;
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfFilters.ts 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfFilters.ts
index 173f4c98ae..1e6c821987 100644
--- 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfFilters.ts
+++ 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfFilters.ts
@@ -30,6 +30,7 @@ export interface UseWhatIfFiltersReturn {
   editingFilterIndex: number | null;
   currentAdhocFilter: AdhocFilter | null;
   setFilterPopoverVisible: (visible: boolean) => void;
+  setFilters: (filters: WhatIfFilter[]) => void;
   handleOpenFilterPopover: () => void;
   handleEditFilter: (index: number) => void;
   handleFilterChange: (adhocFilter: AdhocFilter) => void;
@@ -213,6 +214,7 @@ export function useWhatIfFilters(): UseWhatIfFiltersReturn {
     editingFilterIndex,
     currentAdhocFilter,
     setFilterPopoverVisible,
+    setFilters,
     handleOpenFilterPopover,
     handleEditFilter,
     handleFilterChange,
diff --git 
a/superset-frontend/src/pages/WhatIfSimulationList/EditSimulationModal.tsx 
b/superset-frontend/src/pages/WhatIfSimulationList/EditSimulationModal.tsx
new file mode 100644
index 0000000000..0d4bfc75c6
--- /dev/null
+++ b/superset-frontend/src/pages/WhatIfSimulationList/EditSimulationModal.tsx
@@ -0,0 +1,383 @@
+/**
+ * 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 { useState, useCallback, useMemo } from 'react';
+import { t } from '@superset-ui/core';
+import { css, styled, useTheme } from '@apache-superset/core/ui';
+import { Modal, Tag, Button, Input } from '@superset-ui/core/components';
+import Slider from '@superset-ui/core/components/Slider';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { WhatIfModification, WhatIfFilter } from 'src/dashboard/types';
+import { formatPercentageChange } from 'src/dashboard/util/whatIf';
+import {
+  WhatIfSimulation,
+  updateSimulation,
+} from 'src/dashboard/components/WhatIfDrawer/whatIfApi';
+import {
+  SLIDER_MIN,
+  SLIDER_MAX,
+  SLIDER_MARKS,
+  SLIDER_TOOLTIP_CONFIG,
+} from 'src/dashboard/components/WhatIfDrawer/constants';
+
+interface EditSimulationModalProps {
+  simulation: WhatIfSimulation | null;
+  onHide: () => void;
+  onSaved: () => void;
+  addSuccessToast: (msg: string) => void;
+  addDangerToast: (msg: string) => void;
+}
+
+const ModificationRow = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: ${({ theme }) => theme.sizeUnit * 2}px;
+  padding: ${({ theme }) => theme.sizeUnit * 3}px;
+  background: ${({ theme }) => theme.colorBgLayout};
+  border-radius: ${({ theme }) => theme.borderRadius}px;
+  margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
+`;
+
+const ModificationHeader = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: ${({ theme }) => theme.sizeUnit * 2}px;
+`;
+
+const ColumnName = styled.span`
+  font-weight: ${({ theme }) => theme.fontWeightStrong};
+  font-size: ${({ theme }) => theme.fontSizeLG}px;
+`;
+
+const SliderContainer = styled.div`
+  padding: 0 ${({ theme }) => theme.sizeUnit}px;
+  & .ant-slider-mark {
+    font-size: ${({ theme }) => theme.fontSizeSM}px;
+  }
+`;
+
+const FiltersContainer = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${({ theme }) => theme.sizeUnit}px;
+`;
+
+const FilterLabel = styled.div`
+  font-size: ${({ theme }) => theme.fontSizeSM}px;
+  color: ${({ theme }) => theme.colorTextSecondary};
+  margin-bottom: ${({ theme }) => theme.sizeUnit}px;
+`;
+
+const EmptyState = styled.div`
+  text-align: center;
+  padding: ${({ theme }) => theme.sizeUnit * 6}px;
+  color: ${({ theme }) => theme.colorTextSecondary};
+`;
+
+const AddModificationButton = styled(Button)`
+  margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
+`;
+
+const NewModificationForm = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: ${({ theme }) => theme.sizeUnit * 2}px;
+  padding: ${({ theme }) => theme.sizeUnit * 3}px;
+  background: ${({ theme }) => theme.colorBgLayout};
+  border-radius: ${({ theme }) => theme.borderRadius}px;
+  border: 1px dashed ${({ theme }) => theme.colorBorder};
+  margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
+`;
+
+/**
+ * Format a WhatIfFilter for display
+ */
+function formatFilterLabel(filter: WhatIfFilter): string {
+  const { col, op, val } = filter;
+
+  let valStr: string;
+  if (Array.isArray(val)) {
+    valStr = val.join(', ');
+  } else if (typeof val === 'boolean') {
+    valStr = val ? 'true' : 'false';
+  } else {
+    valStr = String(val);
+  }
+  // Truncate long values
+  if (valStr.length > 20) {
+    valStr = `${valStr.substring(0, 17)}...`;
+  }
+  return `${col} ${op} ${valStr}`;
+}
+
+interface EditableModification extends WhatIfModification {
+  sliderValue: number; // (multiplier - 1) * 100
+}
+
+function EditSimulationModal({
+  simulation,
+  onHide,
+  onSaved,
+  addSuccessToast,
+  addDangerToast,
+}: EditSimulationModalProps) {
+  const theme = useTheme();
+
+  // Convert modifications to editable format with slider values
+  const initialModifications = useMemo(
+    () =>
+      simulation?.modifications.map(mod => ({
+        ...mod,
+        sliderValue: (mod.multiplier - 1) * 100,
+      })) ?? [],
+    [simulation],
+  );
+
+  const [modifications, setModifications] =
+    useState<EditableModification[]>(initialModifications);
+  const [saving, setSaving] = useState(false);
+  const [showNewModification, setShowNewModification] = useState(false);
+  const [newColumnName, setNewColumnName] = useState('');
+  const [newSliderValue, setNewSliderValue] = useState(0);
+
+  // Reset state when simulation changes
+  useMemo(() => {
+    setModifications(initialModifications);
+    setShowNewModification(false);
+    setNewColumnName('');
+    setNewSliderValue(0);
+  }, [initialModifications]);
+
+  const handleSliderChange = useCallback((index: number, value: number) => {
+    setModifications(prev =>
+      prev.map((mod, i) =>
+        i === index
+          ? { ...mod, sliderValue: value, multiplier: 1 + value / 100 }
+          : mod,
+      ),
+    );
+  }, []);
+
+  const handleRemoveModification = useCallback((index: number) => {
+    setModifications(prev => prev.filter((_, i) => i !== index));
+  }, []);
+
+  const handleAddModification = useCallback(() => {
+    if (!newColumnName.trim()) return;
+
+    const newMod: EditableModification = {
+      column: newColumnName.trim(),
+      multiplier: 1 + newSliderValue / 100,
+      sliderValue: newSliderValue,
+    };
+    setModifications(prev => [...prev, newMod]);
+    setShowNewModification(false);
+    setNewColumnName('');
+    setNewSliderValue(0);
+  }, [newColumnName, newSliderValue]);
+
+  const handleSave = useCallback(async () => {
+    if (!simulation) return;
+
+    setSaving(true);
+    try {
+      const updatedModifications: WhatIfModification[] = modifications.map(
+        mod => ({
+          column: mod.column,
+          multiplier: mod.multiplier,
+          filters: mod.filters,
+        }),
+      );
+
+      await updateSimulation(simulation.id, {
+        modifications: updatedModifications,
+      });
+
+      addSuccessToast(t('Simulation updated successfully'));
+      onSaved();
+      onHide();
+    } catch (error) {
+      addDangerToast(t('Failed to update simulation'));
+    } finally {
+      setSaving(false);
+    }
+  }, [
+    simulation,
+    modifications,
+    onSaved,
+    onHide,
+    addSuccessToast,
+    addDangerToast,
+  ]);
+
+  const hasChanges = useMemo(() => {
+    if (!simulation) return false;
+    if (modifications.length !== simulation.modifications.length) return true;
+
+    return modifications.some((mod, i) => {
+      const original = simulation.modifications[i];
+      return mod.multiplier !== original.multiplier;
+    });
+  }, [simulation, modifications]);
+
+  if (!simulation) return null;
+
+  return (
+    <Modal
+      show
+      onHide={onHide}
+      title={t('Edit simulation: %s', simulation.name)}
+      primaryButtonName={t('Save')}
+      onHandledPrimaryAction={handleSave}
+      primaryButtonLoading={saving}
+      disablePrimaryButton={!hasChanges || saving}
+      centered
+    >
+      {modifications.length === 0 && !showNewModification ? (
+        <EmptyState>
+          <Icons.WarningOutlined
+            iconSize="xl"
+            css={css`
+              color: ${theme.colorWarning};
+              margin-bottom: ${theme.sizeUnit * 2}px;
+            `}
+          />
+          <div>{t('No modifications in this simulation')}</div>
+        </EmptyState>
+      ) : (
+        modifications.map((mod, index) => (
+          <ModificationRow key={`${mod.column}-${index}`}>
+            <ModificationHeader>
+              <ColumnName>{mod.column}</ColumnName>
+              <div
+                css={css`
+                  display: flex;
+                  align-items: center;
+                  gap: ${theme.sizeUnit * 2}px;
+                `}
+              >
+                <span
+                  css={css`
+                    font-weight: ${theme.fontWeightStrong};
+                    font-size: ${theme.fontSizeLG}px;
+                    color: ${mod.multiplier >= 1
+                      ? theme.colorSuccess
+                      : theme.colorError};
+                  `}
+                >
+                  {formatPercentageChange(mod.multiplier, 0)}
+                </span>
+                <Button
+                  buttonSize="small"
+                  onClick={() => handleRemoveModification(index)}
+                  buttonStyle="tertiary"
+                  aria-label={t('Remove modification')}
+                >
+                  <Icons.DeleteOutlined iconSize="s" />
+                </Button>
+              </div>
+            </ModificationHeader>
+
+            <SliderContainer>
+              <Slider
+                min={SLIDER_MIN}
+                max={SLIDER_MAX}
+                value={mod.sliderValue}
+                onChange={value => handleSliderChange(index, value)}
+                marks={SLIDER_MARKS}
+                tooltip={SLIDER_TOOLTIP_CONFIG}
+              />
+            </SliderContainer>
+
+            {mod.filters && mod.filters.length > 0 && (
+              <div>
+                <FilterLabel>{t('Filters')}</FilterLabel>
+                <FiltersContainer>
+                  {mod.filters.map((filter, filterIndex) => (
+                    <Tag key={`${filter.col}-${filterIndex}`}>
+                      {formatFilterLabel(filter)}
+                    </Tag>
+                  ))}
+                </FiltersContainer>
+              </div>
+            )}
+          </ModificationRow>
+        ))
+      )}
+
+      {showNewModification && (
+        <NewModificationForm>
+          <Input
+            placeholder={t('Column name')}
+            value={newColumnName}
+            onChange={e => setNewColumnName(e.target.value)}
+          />
+          <SliderContainer>
+            <Slider
+              min={SLIDER_MIN}
+              max={SLIDER_MAX}
+              value={newSliderValue}
+              onChange={setNewSliderValue}
+              marks={SLIDER_MARKS}
+              tooltip={SLIDER_TOOLTIP_CONFIG}
+            />
+          </SliderContainer>
+          <div
+            css={css`
+              display: flex;
+              gap: ${theme.sizeUnit}px;
+            `}
+          >
+            <Button
+              buttonStyle="primary"
+              buttonSize="small"
+              onClick={handleAddModification}
+              disabled={!newColumnName.trim() || newSliderValue === 0}
+            >
+              {t('Add')}
+            </Button>
+            <Button
+              buttonSize="small"
+              onClick={() => {
+                setShowNewModification(false);
+                setNewColumnName('');
+                setNewSliderValue(0);
+              }}
+            >
+              {t('Cancel')}
+            </Button>
+          </div>
+        </NewModificationForm>
+      )}
+
+      {!showNewModification && (
+        <AddModificationButton
+          buttonStyle="tertiary"
+          onClick={() => setShowNewModification(true)}
+        >
+          <Icons.PlusOutlined iconSize="s" />
+          {t('Add modification')}
+        </AddModificationButton>
+      )}
+    </Modal>
+  );
+}
+
+export default EditSimulationModal;
diff --git a/superset-frontend/src/pages/WhatIfSimulationList/index.tsx 
b/superset-frontend/src/pages/WhatIfSimulationList/index.tsx
index 102f92bbd2..5f4e768092 100644
--- a/superset-frontend/src/pages/WhatIfSimulationList/index.tsx
+++ b/superset-frontend/src/pages/WhatIfSimulationList/index.tsx
@@ -18,9 +18,9 @@
  */
 
 import { useMemo, useState, useEffect, useCallback } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useHistory } from 'react-router-dom';
 import { t, SupersetClient } from '@superset-ui/core';
-import { css, styled } from '@apache-superset/core/ui';
+import { css, styled, useTheme } from '@apache-superset/core/ui';
 import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
 
 import {
@@ -28,7 +28,10 @@ import {
   DeleteModal,
   Empty,
   Skeleton,
+  Tag,
+  Tooltip,
 } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
 import {
   ListView,
   ListViewActionsBar,
@@ -37,12 +40,15 @@ import {
 } from 'src/components';
 import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
 import withToasts from 'src/components/MessageToasts/withToasts';
+import { WhatIfFilter, WhatIfModification } from 'src/dashboard/types';
+import { formatPercentageChange } from 'src/dashboard/util/whatIf';
 
 import {
   fetchAllSimulations,
   deleteSimulation,
   WhatIfSimulation,
 } from 'src/dashboard/components/WhatIfDrawer/whatIfApi';
+import EditSimulationModal from './EditSimulationModal';
 
 const PAGE_SIZE = 25;
 
@@ -58,7 +64,7 @@ interface DashboardInfo {
 }
 
 const PageContainer = styled.div`
-  padding: ${({ theme }) => theme.gridUnit * 4}px;
+  padding: ${({ theme }) => theme.sizeUnit * 4}px;
 `;
 
 const EmptyContainer = styled.div`
@@ -66,13 +72,102 @@ const EmptyContainer = styled.div`
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: ${({ theme }) => theme.gridUnit * 16}px;
+  padding: ${({ theme }) => theme.sizeUnit * 16}px;
 `;
 
+const ModificationsContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: ${({ theme }) => theme.sizeUnit}px;
+`;
+
+const ModificationTagsRow = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${({ theme }) => theme.sizeUnit}px;
+`;
+
+const FilterBadge = styled.span`
+  font-size: 10px;
+  color: ${({ theme }) => theme.colorTextSecondary};
+  margin-left: ${({ theme }) => theme.sizeUnit}px;
+`;
+
+/**
+ * Format a WhatIfFilter for display
+ */
+function formatFilterLabel(filter: WhatIfFilter): string {
+  const { col, op, val } = filter;
+
+  let valStr: string;
+  if (Array.isArray(val)) {
+    valStr = val.join(', ');
+  } else if (typeof val === 'boolean') {
+    valStr = val ? 'true' : 'false';
+  } else {
+    valStr = String(val);
+  }
+  // Truncate long values
+  if (valStr.length > 15) {
+    valStr = `${valStr.substring(0, 12)}...`;
+  }
+  return `${col} ${op} ${valStr}`;
+}
+
+/**
+ * Component to render a single modification with its filters
+ */
+function ModificationTag({
+  modification,
+}: {
+  modification: WhatIfModification;
+}) {
+  const theme = useTheme();
+  const hasFilters = modification.filters && modification.filters.length > 0;
+
+  const tagContent = (
+    <Tag
+      css={css`
+        display: inline-flex;
+        align-items: center;
+        gap: ${theme.sizeUnit}px;
+        margin: 0;
+      `}
+    >
+      <span>{modification.column}</span>
+      <span
+        css={css`
+          font-weight: ${theme.fontWeightStrong};
+          color: ${modification.multiplier >= 1
+            ? theme.colorSuccess
+            : theme.colorError};
+        `}
+      >
+        {formatPercentageChange(modification.multiplier, 0)}
+      </span>
+      {hasFilters && (
+        <FilterBadge>
+          <Icons.FilterOutlined iconSize="xs" />
+        </FilterBadge>
+      )}
+    </Tag>
+  );
+
+  if (hasFilters) {
+    const filterTooltip = modification
+      .filters!.map(f => formatFilterLabel(f))
+      .join(', ');
+    return <Tooltip title={filterTooltip}>{tagContent}</Tooltip>;
+  }
+
+  return tagContent;
+}
+
 function WhatIfSimulationList({
   addDangerToast,
   addSuccessToast,
 }: WhatIfSimulationListProps) {
+  const history = useHistory();
   const [simulations, setSimulations] = useState<WhatIfSimulation[]>([]);
   const [dashboards, setDashboards] = useState<Record<number, DashboardInfo>>(
     {},
@@ -80,6 +175,8 @@ function WhatIfSimulationList({
   const [loading, setLoading] = useState(true);
   const [simulationCurrentlyDeleting, setSimulationCurrentlyDeleting] =
     useState<WhatIfSimulation | null>(null);
+  const [simulationCurrentlyEditing, setSimulationCurrentlyEditing] =
+    useState<WhatIfSimulation | null>(null);
 
   const loadSimulations = useCallback(async () => {
     setLoading(true);
@@ -156,7 +253,7 @@ function WhatIfSimulationList({
   );
 
   const menuData: SubMenuProps = {
-    name: t('What-If Simulations'),
+    name: t('What-if simulations'),
   };
 
   const initialSort = [{ id: 'changedOn', desc: true }];
@@ -166,8 +263,22 @@ function WhatIfSimulationList({
       {
         accessor: 'name',
         Header: t('Name'),
-        size: 'xxl',
+        size: 'lg',
         id: 'name',
+        Cell: ({
+          row: {
+            original: { id, name, dashboardId },
+          },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => {
+          const dashboard = dashboards[dashboardId];
+          const dashboardUrl = dashboard?.slug
+            ? `/superset/dashboard/${dashboard.slug}/`
+            : `/superset/dashboard/${dashboardId}/`;
+          const url = `${dashboardUrl}?simulation=${id}`;
+          return <Link to={url}>{name}</Link>;
+        },
       },
       {
         accessor: 'description',
@@ -185,7 +296,7 @@ function WhatIfSimulationList({
       {
         accessor: 'dashboardId',
         Header: t('Dashboard'),
-        size: 'lg',
+        size: 'md',
         id: 'dashboardId',
         Cell: ({
           row: {
@@ -205,7 +316,7 @@ function WhatIfSimulationList({
       {
         accessor: 'modifications',
         Header: t('Modifications'),
-        size: 'md',
+        size: 'xxl',
         id: 'modifications',
         disableSortBy: true,
         Cell: ({
@@ -214,12 +325,28 @@ function WhatIfSimulationList({
           },
         }: {
           row: { original: WhatIfSimulation };
-        }) => modifications.length,
+        }) => {
+          if (modifications.length === 0) {
+            return <span>-</span>;
+          }
+          return (
+            <ModificationsContainer>
+              <ModificationTagsRow>
+                {modifications.map((mod, idx) => (
+                  <ModificationTag
+                    key={`${mod.column}-${idx}`}
+                    modification={mod}
+                  />
+                ))}
+              </ModificationTagsRow>
+            </ModificationsContainer>
+          );
+        },
       },
       {
         accessor: 'changedOn',
         Header: t('Last modified'),
-        size: 'lg',
+        size: 'md',
         id: 'changedOn',
         Cell: ({
           row: {
@@ -239,10 +366,12 @@ function WhatIfSimulationList({
           const dashboardUrl = dashboard?.slug
             ? `/superset/dashboard/${dashboard.slug}/`
             : `/superset/dashboard/${original.dashboardId}/`;
+          const simulationUrl = `${dashboardUrl}?simulation=${original.id}`;
 
           const handleOpen = () => {
-            window.location.href = dashboardUrl;
+            history.push(simulationUrl);
           };
+          const handleEdit = () => setSimulationCurrentlyEditing(original);
           const handleDelete = () => setSimulationCurrentlyDeleting(original);
 
           const actions = [
@@ -253,6 +382,13 @@ function WhatIfSimulationList({
               icon: 'ExportOutlined',
               onClick: handleOpen,
             },
+            {
+              label: 'edit-action',
+              tooltip: t('Edit modifications'),
+              placement: 'bottom',
+              icon: 'EditOutlined',
+              onClick: handleEdit,
+            },
             {
               label: 'delete-action',
               tooltip: t('Delete simulation'),
@@ -268,10 +404,11 @@ function WhatIfSimulationList({
         },
         Header: t('Actions'),
         id: 'actions',
+        size: 'sm',
         disableSortBy: true,
       },
     ],
-    [dashboards],
+    [dashboards, history],
   );
 
   const emptyState = {
@@ -342,6 +479,15 @@ function WhatIfSimulationList({
           title={t('Delete Simulation?')}
         />
       )}
+      {simulationCurrentlyEditing && (
+        <EditSimulationModal
+          simulation={simulationCurrentlyEditing}
+          onHide={() => setSimulationCurrentlyEditing(null)}
+          onSaved={loadSimulations}
+          addSuccessToast={addSuccessToast}
+          addDangerToast={addDangerToast}
+        />
+      )}
       <ConfirmStatusChange
         title={t('Please confirm')}
         description={t(
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index 8d29cd471f..9b84779810 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -223,6 +223,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         from superset.views.user_registrations import UserRegistrationsView
         from superset.views.users.api import CurrentUserRestApi, UserRestApi
         from superset.views.users_list import UsersListView
+        from superset.views.what_if import WhatIfSimulationView
         from superset.what_if.api import WhatIfRestApi
 
         set_app_error_handlers(self.superset_app)
@@ -432,6 +433,16 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_view_no_menu(RoleRestAPI)
         appbuilder.add_view_no_menu(UserInfoView)
 
+        appbuilder.add_view(
+            WhatIfSimulationView,
+            "What-If Simulations",
+            label=_("What-if simulations"),
+            href="/whatif/simulations/",
+            icon="fa-flask",
+            category="Manage",
+            category_label=_("Manage"),
+        )
+
         #
         # Add links
         #
diff --git a/superset/views/what_if.py b/superset/views/what_if.py
new file mode 100644
index 0000000000..ebf92e1362
--- /dev/null
+++ b/superset/views/what_if.py
@@ -0,0 +1,39 @@
+# 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.
+"""View for What-If simulations list page."""
+
+from flask_appbuilder import permission_name
+from flask_appbuilder.api import expose
+from flask_appbuilder.security.decorators import has_access
+
+from superset.superset_typing import FlaskResponse
+
+from .base import BaseSupersetView
+
+
+class WhatIfSimulationView(BaseSupersetView):
+    """View for the What-If simulations list page."""
+
+    route_base = "/whatif"
+    class_permission_name = "Dashboard"
+
+    @expose("/simulations/")
+    @has_access
+    @permission_name("read")
+    def list(self) -> FlaskResponse:
+        """Render the What-If simulations list page."""
+        return super().render_app_template()


Reply via email to