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

justinpark pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 7503ee4e09 feat(sqllab): primary/secondary action extensions (#36644)
7503ee4e09 is described below

commit 7503ee4e09297a95812e26e73c36f6212ec57481
Author: JUST.in DO IT <[email protected]>
AuthorDate: Mon Jan 12 12:06:15 2026 -0800

    feat(sqllab): primary/secondary action extensions (#36644)
---
 .../src/components/AsyncAceEditor/index.tsx        |   2 -
 .../src/components/Icons/AntdEnhanced.tsx          |   2 +
 .../SqlLab/components/AceEditorWrapper/index.tsx   |  26 +-
 .../src/SqlLab/components/AppLayout/index.tsx      |  22 +-
 .../EstimateQueryCostButton.test.tsx               |  28 +-
 .../components/EstimateQueryCostButton/index.tsx   |   9 +-
 .../QueryLimitSelect/QueryLimitSelect.test.tsx     |   3 +-
 .../SqlLab/components/QueryLimitSelect/index.tsx   |  28 +-
 .../RunQueryActionButton.test.tsx                  |   1 -
 .../components/RunQueryActionButton/index.tsx      |  38 +--
 .../SaveDatasetActionButton.test.tsx               |  39 +--
 .../components/SaveDatasetActionButton/index.tsx   |  47 +--
 .../SqlLab/components/SaveQuery/SaveQuery.test.tsx |  23 +-
 .../src/SqlLab/components/SaveQuery/index.tsx      |  22 +-
 .../SqlLab/components/ShareSqlLabQuery/index.tsx   |  10 +-
 .../src/SqlLab/components/SouthPane/index.tsx      |  23 +-
 .../SqlLab/components/SqlEditor/SqlEditor.test.tsx |   6 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      | 249 ++++++--------
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |  90 +----
 .../SqlEditorTopBar/SqlEditorTopBar.test.tsx       | 130 +++++++
 .../SqlLab/components/SqlEditorTopBar/index.tsx    |  62 ++++
 .../SqlEditorTopBar/useDatabaseSelector.test.ts    | 320 ++++++++++++++++++
 .../SqlEditorTopBar/useDatabaseSelector.ts         | 126 +++++++
 .../StatusBar/StatusBar.test.tsx}                  |  29 +-
 .../src/SqlLab/components/StatusBar/index.tsx      |  57 ++++
 .../SqlLab/components/TabbedSqlEditors/index.tsx   |  40 ++-
 superset-frontend/src/SqlLab/constants.ts          |   3 +-
 superset-frontend/src/SqlLab/contributions.ts      |   4 +-
 .../MenuListExtension/MenuListExtension.test.tsx   | 374 +++++++++++++++++++++
 .../src/components/MenuListExtension/index.tsx     | 157 +++++++++
 .../ViewListExtension/ViewListExtension.test.tsx   | 198 +++++++++++
 .../ViewListExtension/index.tsx}                   |  30 +-
 32 files changed, 1803 insertions(+), 395 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/index.tsx
index 368cc02c7a..002d65cd07 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/index.tsx
@@ -273,10 +273,8 @@ export function AsyncAceEditor(
               key="ace-tooltip-global"
               styles={css`
                 .ace_editor {
-                  border: 1px solid ${token.colorBorder} !important;
                   background-color: ${token.colorBgContainer} !important;
                 }
-
                 /* Basic editor styles with dark mode support */
                 .ace_editor.ace-github,
                 .ace_editor.ace-tm {
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 6f9222470c..319227687b 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
@@ -114,6 +114,7 @@ import {
   ShareAltOutlined,
   StarOutlined,
   StarFilled,
+  StepForwardOutlined,
   StopOutlined,
   SunOutlined,
   SyncOutlined,
@@ -258,6 +259,7 @@ const AntdIcons = {
   SunOutlined,
   StarOutlined,
   StarFilled,
+  StepForwardOutlined,
   StopOutlined,
   SyncOutlined,
   TagOutlined,
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
index bd01735f75..0a9231e26a 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
@@ -82,6 +82,7 @@ const AceEditorWrapper = ({
 
   const currentSql = queryEditor.sql ?? '';
   const [sql, setSql] = useState(currentSql);
+  const theme = useTheme();
 
   // The editor changeSelection is called multiple times in a row,
   // faster than React reconciliation process, so the selected text
@@ -126,7 +127,8 @@ const AceEditorWrapper = ({
         exec: keyConfig.func,
       });
     });
-
+    const marginSize = theme.sizeUnit * 2;
+    editor.renderer.setScrollMargin(marginSize, marginSize, 0, 0);
     editor.$blockScrolling = Infinity; // eslint-disable-line no-param-reassign
     editor.selection.on('changeSelection', () => {
       const selectedText = editor.getSelectedText();
@@ -178,7 +180,6 @@ const AceEditorWrapper = ({
     },
     !autocomplete,
   );
-  const theme = useTheme();
 
   return (
     <>
@@ -188,6 +189,27 @@ const AceEditorWrapper = ({
             width: 100% !important;
           }
 
+          .ace_content,
+          .SqlEditor .sql-container .ace_gutter {
+            background-color: ${theme.colorBgBase} !important;
+          }
+
+          .ace_gutter::after {
+            content: '';
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            right: ${theme.sizeUnit * 2}px;
+            width: 1px;
+            height: 100%;
+            background-color: ${theme.colorBorder};
+          }
+
+          .ace_gutter,
+          .ace_scroller {
+            background-color: ${theme.colorBgBase} !important;
+          }
+
           .ace_autocomplete {
             // Use !important because Ace Editor applies extra CSS at the last 
second
             // when opening the autocomplete.
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx 
b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
index 8d8f06d34e..bb86e631eb 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
@@ -19,11 +19,10 @@
 import { useSelector } from 'react-redux';
 import { noop } from 'lodash';
 import type { SqlLabRootState } from 'src/SqlLab/types';
-import { styled } from '@apache-superset/core';
+import { css, styled } from '@apache-superset/core';
 import { useComponentDidUpdate } from '@superset-ui/core';
 import { Grid } from '@superset-ui/core/components';
 import ExtensionsManager from 'src/extensions/ExtensionsManager';
-import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
 import { Splitter } from 'src/components/Splitter';
 import useEffectEvent from 'src/hooks/useEffectEvent';
 import useStoredSidebarWidth from 
'src/components/ResizableSidebar/useStoredSidebarWidth';
@@ -31,11 +30,15 @@ import {
   SQL_EDITOR_LEFTBAR_WIDTH,
   SQL_EDITOR_RIGHTBAR_WIDTH,
 } from 'src/SqlLab/constants';
+import { ViewContribution } from 'src/SqlLab/contributions';
+import ViewListExtension from 'src/components/ViewListExtension';
 
 import SqlEditorLeftBar from '../SqlEditorLeftBar';
-import { ViewContribution } from 'src/SqlLab/contributions';
+import StatusBar from '../StatusBar';
 
 const StyledContainer = styled.div`
+  display: flex;
+  flex-direction: column;
   height: 100%;
 
   & .ant-splitter-panel:not(.sqllab-body):not(.queryPane) {
@@ -93,11 +96,17 @@ const AppLayout: React.FC = ({ children }) => {
     ExtensionsManager.getInstance().getViewContributions(
       ViewContribution.RightSidebar,
     ) || [];
-  const { getView } = useExtensionsContext();
 
   return (
     <StyledContainer>
-      <Splitter lazy onResizeEnd={onSidebarChange} onResize={noop}>
+      <Splitter
+        css={css`
+          flex: 1;
+        `}
+        lazy
+        onResizeEnd={onSidebarChange}
+        onResize={noop}
+      >
         <Splitter.Panel
           collapsible={{
             start: true,
@@ -126,11 +135,12 @@ const AppLayout: React.FC = ({ children }) => {
             min={SQL_EDITOR_RIGHTBAR_WIDTH}
           >
             <ContentWrapper>
-              {contributions.map(contribution => getView(contribution.id))}
+              <ViewListExtension viewId={ViewContribution.RightSidebar} />
             </ContentWrapper>
           </Splitter.Panel>
         )}
       </Splitter>
+      <StatusBar />
     </StyledContainer>
   );
 };
diff --git 
a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
 
b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
index 684aef7b06..cda93761ff 100644
--- 
a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
@@ -56,22 +56,24 @@ const setup = (props: 
Partial<EstimateQueryCostButtonProps>, store?: Store) =>
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('EstimateQueryCostButton', () => {
   test('renders EstimateQueryCostButton', async () => {
-    const { queryByText } = setup({}, mockStore(initialState));
+    const { queryByLabelText } = setup({}, mockStore(initialState));
 
-    expect(queryByText('Estimate cost')).toBeInTheDocument();
+    expect(queryByLabelText('Estimate cost')).toBeInTheDocument();
   });
 
   test('renders label for selected query', async () => {
-    const { queryByText } = setup(
+    const { queryByLabelText } = setup(
       { queryEditorId: extraQueryEditor1.id },
       mockStore(initialState),
     );
 
-    expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
+    expect(
+      queryByLabelText('Estimate selected query cost'),
+    ).toBeInTheDocument();
   });
 
   test('renders label for selected query from unsaved', async () => {
-    const { queryByText } = setup(
+    const { queryByLabelText } = setup(
       {},
       mockStore({
         ...initialState,
@@ -85,11 +87,13 @@ describe('EstimateQueryCostButton', () => {
       }),
     );
 
-    expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
+    expect(
+      queryByLabelText('Estimate selected query cost'),
+    ).toBeInTheDocument();
   });
 
   test('renders estimation error result', async () => {
-    const { queryByText, getByText } = setup(
+    const { queryByLabelText, queryByText, getByLabelText } = setup(
       {},
       mockStore({
         ...initialState,
@@ -104,14 +108,14 @@ describe('EstimateQueryCostButton', () => {
       }),
     );
 
-    expect(queryByText('Estimate cost')).toBeInTheDocument();
-    fireEvent.click(getByText('Estimate cost'));
+    expect(queryByLabelText('Estimate cost')).toBeInTheDocument();
+    fireEvent.click(getByLabelText('Estimate cost'));
 
     expect(queryByText('Estimate error')).toBeInTheDocument();
   });
 
   test('renders estimation success result', async () => {
-    const { queryByText, getByText, findByTitle } = setup(
+    const { queryByLabelText, getByLabelText, findByTitle } = setup(
       {},
       mockStore({
         ...initialState,
@@ -127,8 +131,8 @@ describe('EstimateQueryCostButton', () => {
       }),
     );
 
-    expect(queryByText('Estimate cost')).toBeInTheDocument();
-    fireEvent.click(getByText('Estimate cost'));
+    expect(queryByLabelText('Estimate cost')).toBeInTheDocument();
+    fireEvent.click(getByLabelText('Estimate cost'));
     const totalCostTitle = await findByTitle('Total cost');
     expect(totalCostTitle).toBeInTheDocument();
   });
diff --git 
a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx 
b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
index 227ff354ea..621020c410 100644
--- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
@@ -27,6 +27,7 @@ import {
   ModalTrigger,
   TableView,
   EmptyWrapperType,
+  Icons,
 } from '@superset-ui/core/components';
 import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
 import { SqlLabRootState, QueryCostEstimate } from 'src/SqlLab/types';
@@ -111,14 +112,16 @@ const EstimateQueryCostButton = ({
         modalBody={renderModalBody()}
         triggerNode={
           <Button
+            color="primary"
+            variant="text"
             style={{ height: 32, padding: '4px 15px' }}
             onClick={onClickHandler}
             key="query-estimate-btn"
             tooltip={tooltip}
             disabled={disabled}
-          >
-            {btnText}
-          </Button>
+            icon={<Icons.MonitorOutlined iconSize="m" />}
+            aria-label={btnText}
+          />
         }
       />
     </span>
diff --git 
a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
 
b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
index 0544f49299..73b9025958 100644
--- 
a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
@@ -30,6 +30,7 @@ import { initialState, defaultQueryEditor } from 
'src/SqlLab/fixtures';
 import QueryLimitSelect, {
   QueryLimitSelectProps,
   convertToNumWithSpaces,
+  convertToShortNum,
 } from 'src/SqlLab/components/QueryLimitSelect';
 
 const middlewares = [thunk];
@@ -102,7 +103,7 @@ describe('QueryLimitSelect', () => {
         },
       }),
     );
-    expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
+    expect(getByText(convertToShortNum(queryLimit))).toBeInTheDocument();
   });
 
   test('renders dropdown select', async () => {
diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
index 59e9ba93bf..9b63b494df 100644
--- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
@@ -34,6 +34,19 @@ export function convertToNumWithSpaces(num: number) {
   return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
 }
 
+export function convertToShortNum(num: number) {
+  if (num < 1000) {
+    return num;
+  }
+  if (num < 1_000_000) {
+    return `${num / 1000}K`;
+  }
+  if (num < 1_000_000_000) {
+    return `${num / 1000_000}M`;
+  }
+  return num;
+}
+
 function renderQueryLimit(
   maxRow: number,
   setQueryLimit: (limit: number) => void,
@@ -74,12 +87,15 @@ const QueryLimitSelect = ({
       popupRender={() => renderQueryLimit(maxRow, setQueryLimit)}
       trigger={['click']}
     >
-      <Button size="small" showMarginRight={false} buttonStyle="link">
-        <span>{t('LIMIT')}:</span>
-        <span className="limitDropdown">
-          {convertToNumWithSpaces(queryLimit)}
-        </span>
-        <Icons.CaretDownOutlined iconSize="m" />
+      <Button
+        size="small"
+        color="primary"
+        variant="text"
+        showMarginRight={false}
+      >
+        <span>{t('Limit')}</span>
+        <span className="limitDropdown">{convertToShortNum(queryLimit)}</span>
+        <Icons.DownOutlined iconSize="m" />
       </Button>
     </Dropdown>
   );
diff --git 
a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
 
b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
index 2ac34b4bb7..61b5613792 100644
--- 
a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
@@ -38,7 +38,6 @@ jest.mock('@superset-ui/core/components/Select/AsyncSelect', 
() => () => (
 
 const defaultProps = {
   queryEditorId: defaultQueryEditor.id,
-  allowAsync: false,
   dbId: 1,
   queryState: 'ready',
   runQuery: () => {},
diff --git 
a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx 
b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
index 5c2e8fa6f5..9bc54bb268 100644
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
@@ -33,10 +33,10 @@ import {
 import useLogAction from 'src/logger/useLogAction';
 
 export interface RunQueryActionButtonProps {
+  compactMode?: boolean;
   queryEditorId: string;
-  allowAsync: boolean;
   queryState?: string;
-  runQuery: (c?: boolean) => void;
+  runQuery: () => void;
   stopQuery: () => void;
   overlayCreateAsMenu: ReactElement | null;
 }
@@ -47,13 +47,14 @@ const buildTextAndIcon = (
   theme: SupersetTheme,
 ): { text: string; icon?: IconType } => {
   let text = t('Run');
-  let icon: IconType | undefined;
+  let icon: IconType | undefined = <Icons.CaretRightOutlined />;
   if (selectedText) {
     text = t('Run selection');
+    icon = <Icons.StepForwardOutlined />;
   }
   if (shouldShowStopButton) {
     text = t('Stop');
-    icon = <Icons.Square iconSize="xs" iconColor={theme.colorIcon} />;
+    icon = <Icons.Square iconColor={theme.colorIcon} />;
   }
   return {
     text,
@@ -62,32 +63,27 @@ const buildTextAndIcon = (
 };
 
 const onClick = (
-  shouldShowStopButton: boolean,
-  allowAsync: boolean,
-  runQuery: (c?: boolean) => void = () => undefined,
+  isStopAction: boolean,
+  runQuery: () => void = () => undefined,
   stopQuery = () => {},
   logAction: (name: string, payload: Record<string, any>) => void,
 ): void => {
-  const eventName = shouldShowStopButton
+  const eventName = isStopAction
     ? LOG_ACTIONS_SQLLAB_STOP_QUERY
     : LOG_ACTIONS_SQLLAB_RUN_QUERY;
 
   logAction(eventName, { shortcut: false });
-  if (shouldShowStopButton) return stopQuery();
-  if (allowAsync) {
-    return runQuery(true);
-  }
-  return runQuery(false);
+  if (isStopAction) return stopQuery();
+  runQuery();
 };
 
 const StyledButton = styled.span`
   button {
     line-height: 13px;
-    // this is to over ride a previous transition built into the component
-    transition: background-color 0ms;
-    &:last-of-type {
-      margin-right: ${({ theme }) => theme.sizeUnit * 2}px;
-    }
+    min-width: auto !important;
+    padding: 0 ${({ theme }) => theme.sizeUnit * 3}px 0
+      ${({ theme }) => theme.sizeUnit * 2}px;
+
     span[name='caret-down'] {
       display: flex;
       margin-left: ${({ theme }) => theme.sizeUnit * 1}px;
@@ -96,7 +92,6 @@ const StyledButton = styled.span`
 `;
 
 const RunQueryActionButton = ({
-  allowAsync = false,
   queryEditorId,
   queryState,
   overlayCreateAsMenu,
@@ -142,7 +137,7 @@ const RunQueryActionButton = ({
       <ButtonComponent
         data-test="run-query-action"
         onClick={() =>
-          onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery, 
logAction)
+          onClick(shouldShowStopBtn, runQuery, stopQuery, logAction)
         }
         disabled={isDisabled}
         tooltip={
@@ -162,6 +157,8 @@ const RunQueryActionButton = ({
                   }
                 />
               ),
+              type: 'primary',
+              danger: shouldShowStopBtn,
               trigger: 'click',
             }
           : {
@@ -169,6 +166,7 @@ const RunQueryActionButton = ({
               icon,
             })}
       >
+        {overlayCreateAsMenu && <>{icon}</>}
         {text}
       </ButtonComponent>
     </StyledButton>
diff --git 
a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx
 
b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx
index cd78007a6e..faaa5cb67f 100644
--- 
a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx
@@ -16,50 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { render, screen, userEvent } from 'spec/helpers/testing-library';
-import { Menu } from '@superset-ui/core/components/Menu';
+import { render, screen } from 'spec/helpers/testing-library';
 import SaveDatasetActionButton from 
'src/SqlLab/components/SaveDatasetActionButton';
 
-const overlayMenu = (
-  <Menu items={[{ label: 'Save dataset', key: 'save-dataset' }]} />
-);
-
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('SaveDatasetActionButton', () => {
   test('renders a split save button', async () => {
+    const onSaveAsExplore = jest.fn();
     render(
       <SaveDatasetActionButton
         setShowSave={() => true}
-        overlayMenu={overlayMenu}
+        onSaveAsExplore={onSaveAsExplore}
       />,
     );
 
-    const saveBtn = screen.getByRole('button', { name: /save/i });
-    const caretBtn = screen.getByRole('button', { name: /down/i });
+    const saveBtn = screen.getByRole('button', { name: 'Save' });
+    const saveDatasetBtn = screen.getByRole('button', {
+      name: /save dataset/i,
+    });
 
     expect(
-      await screen.findByRole('button', { name: /save/i }),
+      await screen.findByRole('button', { name: 'Save' }),
     ).toBeInTheDocument();
     expect(saveBtn).toBeVisible();
-    expect(caretBtn).toBeVisible();
-  });
-
-  test('renders a "save dataset" dropdown menu item when user clicks caret 
button', async () => {
-    render(
-      <SaveDatasetActionButton
-        setShowSave={() => true}
-        overlayMenu={overlayMenu}
-      />,
-    );
-
-    const caretBtn = screen.getByRole('button', { name: /down/i });
-    expect(
-      await screen.findByRole('button', { name: /down/i }),
-    ).toBeInTheDocument();
-    userEvent.click(caretBtn);
-
-    const saveDatasetMenuItem = screen.getByText(/save dataset/i);
-
-    expect(saveDatasetMenuItem).toBeInTheDocument();
+    expect(saveDatasetBtn).toBeVisible();
   });
 });
diff --git 
a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
index e1891b77fc..a68edb7efa 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
@@ -17,37 +17,38 @@
  * under the License.
  */
 import { t } from '@apache-superset/core';
-import { useTheme } from '@apache-superset/core/ui';
 import { Icons } from '@superset-ui/core/components/Icons';
-import { Button, DropdownButton } from '@superset-ui/core/components';
+import { Button } from '@superset-ui/core/components';
 
 interface SaveDatasetActionButtonProps {
   setShowSave: (arg0: boolean) => void;
-  overlayMenu: JSX.Element | null;
+  onSaveAsExplore?: () => void;
 }
 
 const SaveDatasetActionButton = ({
   setShowSave,
-  overlayMenu,
-}: SaveDatasetActionButtonProps) => {
-  const theme = useTheme();
-
-  return !overlayMenu ? (
-    <Button onClick={() => setShowSave(true)} buttonStyle="primary">
-      {t('Save')}
-    </Button>
-  ) : (
-    <DropdownButton
+  onSaveAsExplore,
+}: SaveDatasetActionButtonProps) => (
+  <>
+    <Button
+      color="primary"
+      variant="text"
       onClick={() => setShowSave(true)}
-      popupRender={() => overlayMenu}
-      icon={
-        <Icons.DownOutlined iconSize="xs" iconColor={theme.colorPrimaryText} />
-      }
-      trigger={['click']}
-    >
-      {t('Save')}
-    </DropdownButton>
-  );
-};
+      icon={<Icons.SaveOutlined />}
+      tooltip={t('Save query')}
+      aria-label={t('Save')}
+    />
+    {onSaveAsExplore && (
+      <Button
+        color="primary"
+        variant="text"
+        onClick={() => onSaveAsExplore?.()}
+        icon={<Icons.TableOutlined />}
+        tooltip={t('Save or Overwrite Dataset')}
+        aria-label={t('Save dataset')}
+      />
+    )}
+  </>
+);
 
 export default SaveDatasetActionButton;
diff --git 
a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx 
b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
index 7d41400332..eb13d6acf5 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
@@ -179,11 +179,13 @@ describe('SavedQuery', () => {
     });
 
     await waitFor(() => {
-      const saveBtn = screen.getByRole('button', { name: /save/i });
-      const caretBtn = screen.getByRole('button', { name: /down/i });
+      const saveBtn = screen.getByRole('button', { name: 'Save' });
+      const saveDataSetBtn = screen.getByRole('button', {
+        name: /save dataset/i,
+      });
 
       expect(saveBtn).toBeVisible();
-      expect(caretBtn).toBeVisible();
+      expect(saveDataSetBtn).toBeVisible();
     });
   });
 
@@ -193,12 +195,7 @@ describe('SavedQuery', () => {
       store: mockStore(mockState),
     });
 
-    const caretBtn = await screen.findByRole('button', {
-      name: /down/i,
-    });
-    userEvent.click(caretBtn);
-
-    const saveDatasetMenuItem = await screen.findByText(/save dataset/i);
+    const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
     userEvent.click(saveDatasetMenuItem);
 
     const saveDatasetHeader = screen.getByText(/save or overwrite dataset/i);
@@ -211,13 +208,7 @@ describe('SavedQuery', () => {
       useRedux: true,
       store: mockStore(mockState),
     });
-
-    const caretBtn = await screen.findByRole('button', {
-      name: /down/i,
-    });
-    userEvent.click(caretBtn);
-
-    const saveDatasetMenuItem = await screen.findByText(/save dataset/i);
+    const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
     userEvent.click(saveDatasetMenuItem);
 
     const closeBtn = screen.getByRole('button', { name: /close/i });
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index 6aad652ead..96e025ac48 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -30,7 +30,6 @@ import {
   Col,
   Icons,
 } from '@superset-ui/core/components';
-import { Menu } from '@superset-ui/core/components/Menu';
 import SaveDatasetActionButton from 
'src/SqlLab/components/SaveDatasetActionButton';
 import {
   SaveDatasetModal,
@@ -66,7 +65,6 @@ const Styles = styled.span`
   span[role='img']:not([aria-label='down']) {
     display: flex;
     margin: 0;
-    color: ${({ theme }) => theme.colorIcon};
     svg {
       vertical-align: -${({ theme }) => theme.sizeUnit * 1.25}px;
       margin: 0;
@@ -116,20 +114,10 @@ const SaveQuery = ({
   const shouldShowSaveButton =
     database?.allows_virtual_table_explore !== undefined;
 
-  const overlayMenu = (
-    <Menu
-      items={[
-        {
-          label: t('Save dataset'),
-          key: 'save-dataset',
-          onClick: () => {
-            logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {});
-            setShowSaveDatasetModal(true);
-          },
-        },
-      ]}
-    />
-  );
+  const onSaveAsExplore = () => {
+    logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {});
+    setShowSaveDatasetModal(true);
+  };
 
   const queryPayload = () => ({
     name: label,
@@ -209,7 +197,7 @@ const SaveQuery = ({
       {shouldShowSaveButton && (
         <SaveDatasetActionButton
           setShowSave={setShowSave}
-          overlayMenu={canExploreDatabase ? overlayMenu : null}
+          onSaveAsExplore={canExploreDatabase ? onSaveAsExplore : undefined}
         />
       )}
       <SaveDatasetModal
diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx 
b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
index 28e93f662b..7b84b5325a 100644
--- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
@@ -71,18 +71,16 @@ const ShareSqlLabQuery = ({
     const tooltip = t('Copy query link to your clipboard');
     return (
       <Button
-        buttonSize="small"
-        buttonStyle="secondary"
+        color="primary"
+        variant="text"
         tooltip={tooltip}
         css={css`
           span > :first-of-type {
             margin-right: 0;
           }
         `}
-      >
-        <Icons.LinkOutlined iconSize="m" />
-        {t('Copy link')}
-      </Button>
+        icon={<Icons.LinkOutlined iconSize="m" />}
+      />
     );
   };
 
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx 
b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 529cdaec77..1f4697db6b 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -25,9 +25,11 @@ import { css, styled, useTheme } from 
'@apache-superset/core/ui';
 
 import { removeTables, setActiveSouthPaneTab } from 
'src/SqlLab/actions/sqlLab';
 
-import { Label } from '@superset-ui/core/components';
+import { Flex, Label } from '@superset-ui/core/components';
 import { Icons } from '@superset-ui/core/components/Icons';
 import { SqlLabRootState } from 'src/SqlLab/types';
+import { ViewContribution } from 'src/SqlLab/contributions';
+import MenuListExtension from 'src/components/MenuListExtension';
 import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
 import ExtensionsManager from 'src/extensions/ExtensionsManager';
 import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
@@ -41,7 +43,6 @@ import {
 } from '../../constants';
 import Results from './Results';
 import TablePreview from '../TablePreview';
-import { ViewContribution } from 'src/SqlLab/contributions';
 
 /*
     editorQueries are queries executed by users passed from SqlEditor component
@@ -73,6 +74,10 @@ const StyledPane = styled.div`
       overflow-y: auto;
     }
   }
+  .ant-tabs-extra-content {
+    margin: 0 ${({ theme }) => theme.sizeUnit * 4}px
+      ${({ theme }) => theme.sizeUnit * 2}px;
+  }
   .ant-tabs-tabpane {
     .scrollable {
       overflow-y: auto;
@@ -101,7 +106,7 @@ const SouthPane = ({
   const dispatch = useDispatch();
   const contributions =
     ExtensionsManager.getInstance().getViewContributions(
-      ViewContribution.SouthPanels,
+      ViewContribution.Panels,
     ) || [];
   const { getView } = useExtensionsContext();
   const { offline, tables } = useSelector(
@@ -219,6 +224,18 @@ const SouthPane = ({
   return (
     <StyledPane data-test="south-pane" className="SouthPane" 
ref={southPaneRef}>
       <Tabs
+        tabBarExtraContent={{
+          right: (
+            <Flex
+              css={css`
+                padding: 8px;
+              `}
+            >
+              <MenuListExtension viewId={ViewContribution.Panels} primary />
+              <MenuListExtension viewId={ViewContribution.Panels} secondary />
+            </Flex>
+          ),
+        }}
         type="editable-card"
         activeKey={pinnedTableKeys[activeSouthPaneTab] || activeSouthPaneTab}
         className="SouthPaneTabs"
diff --git 
a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx 
b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
index b0b7cd03e2..b15bbdadc4 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
@@ -321,7 +321,7 @@ describe('SqlEditor', () => {
     const defaultQueryLimit = 101;
     const updatedProps = { ...mockedProps, defaultQueryLimit };
     const { findByText } = setup(updatedProps, store);
-    fireEvent.click(await findByText('LIMIT:'));
+    fireEvent.click(await findByText('Limit'));
     expect(await findByText('10 000')).toBeInTheDocument();
   });
 
@@ -382,8 +382,8 @@ describe('SqlEditor', () => {
           },
         },
       });
-      const { findByText } = setup(mockedProps, store);
-      const button = await findByText('Estimate cost');
+      const { findByLabelText } = setup(mockedProps, store);
+      const button = await findByLabelText('Estimate cost');
       expect(button).toBeInTheDocument();
 
       // click button
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx 
b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 792cc0a9ad..8426a3f981 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -51,17 +51,15 @@ import { debounce, isEmpty } from 'lodash';
 import Mousetrap from 'mousetrap';
 import {
   Button,
-  Dropdown,
+  Divider,
   EmptyState,
   Input,
   Modal,
-  Timer,
 } from '@superset-ui/core/components';
 import { Splitter } from 'src/components/Splitter';
 import { Skeleton } from '@superset-ui/core/components/Skeleton';
 import { Switch } from '@superset-ui/core/components/Switch';
 import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
-import { Icons } from '@superset-ui/core/components/Icons';
 import { detectOS } from 'src/utils/common';
 import {
   addNewQueryEditor,
@@ -85,7 +83,6 @@ import {
   switchQueryEditor,
 } from 'src/SqlLab/actions/sqlLab';
 import {
-  STATE_TYPE_MAP,
   SQL_EDITOR_GUTTER_HEIGHT,
   INITIAL_NORTH_PERCENT,
   SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
@@ -107,8 +104,6 @@ import {
   LOG_ACTIONS_SQLLAB_STOP_QUERY,
   Logger,
 } from 'src/logger/LogUtils';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
-import { commands } from 'src/core';
 import { CopyToClipboard } from 'src/components';
 import TemplateParamsEditor from '../TemplateParamsEditor';
 import SouthPane from '../SouthPane';
@@ -123,6 +118,7 @@ import KeyboardShortcutButton, {
   KEY_MAP,
   KeyboardShortcut,
 } from '../KeyboardShortcutButton';
+import SqlEditorTopBar from '../SqlEditorTopBar';
 
 const bootstrapData = getBootstrapData();
 const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
@@ -166,34 +162,56 @@ const StyledSqlEditor = styled.div`
     height: 100%;
 
     .queryPane {
-      padding: ${theme.sizeUnit * 2}px 0px;
+      padding: 0;
       + .ant-splitter-bar .ant-splitter-bar-dragger {
         &::before {
-          background: transparent;
+          height: 1px;
+          background-color: ${theme.colorBorder};
+          transform: translateX(-50%) !important;
         }
         &::after {
           height: ${SQL_EDITOR_GUTTER_HEIGHT}px;
           background: transparent;
           border-top: 1px solid ${theme.colorBorder};
           border-bottom: 1px solid ${theme.colorBorder};
+          transform: translate(-50%, -2px);
         }
       }
     }
 
     .north-pane {
+      padding: ${theme.sizeUnit * 2}px 0 0 0;
       height: 100%;
       margin: 0 ${theme.sizeUnit * 4}px;
     }
 
-    .SouthPane .ant-tabs-tabpane {
-      margin: 0 ${theme.sizeUnit * 4}px;
-      & .ant-tabs {
-        margin: 0 ${theme.sizeUnit * -4}px;
+    .SouthPane {
+      & .ant-tabs-tabpane {
+        margin: 0 ${theme.sizeUnit * 4}px;
+        & .ant-tabs {
+          margin: 0 ${theme.sizeUnit * -4}px;
+        }
+      }
+      & .ant-tabs-tab {
+        box-shadow: none !important;
+        background: transparent !important;
+        border-color: transparent !important;
+        margin-top: ${theme.sizeUnit * 2}px !important;
+        &.ant-tabs-tab-active {
+          border-bottom-color: ${theme.colorPrimary} !important;
+          & .ant-tabs-tab-btn {
+            font-weight: ${theme.fontWeightStrong};
+            color: ${theme.colorTextBase} !important;
+            text-shadow: none !important;
+          }
+        }
       }
     }
 
     .sql-container {
       flex: 1 1 auto;
+      margin: 0 ${theme.sizeUnit * -4}px;
+      box-shadow: 0 0 0 1px ${theme.colorBorder};
     }
   `}
 `;
@@ -615,30 +633,13 @@ const SqlEditor: FC<Props> = ({
     setCtas(event.target.value);
   };
 
-  const renderDropdown = () => {
+  const getSecondaryMenuItems = () => {
     const qe = queryEditor;
     const successful = latestQuery?.state === 'success';
     const scheduleToolTip = successful
       ? t('Schedule the query periodically')
       : t('You must run the query successfully first');
 
-    const contributions =
-      ExtensionsManager.getInstance().getMenuContributions('sqllab.editor');
-
-    const secondaryContributions = (contributions?.secondary || []).map(
-      contribution => {
-        const command = ExtensionsManager.getInstance().getCommandContribution(
-          contribution.command,
-        )!;
-        return {
-          key: command.command,
-          label: command.title,
-          title: command.description,
-          onClick: () => commands.executeCommand(command.command),
-        };
-      },
-    );
-
     const menuItems: MenuItemType[] = [
       {
         key: 'render-html',
@@ -710,10 +711,9 @@ const SqlEditor: FC<Props> = ({
           </KeyboardShortcutButton>
         ),
       },
-      ...secondaryContributions,
     ].filter(Boolean) as MenuItemType[];
 
-    return <Menu css={{ width: theme.sizeUnit * 50 }} items={menuItems} />;
+    return menuItems;
   };
 
   const onSaveQuery = async (query: QueryPayload, clientId: string) => {
@@ -721,34 +721,8 @@ const SqlEditor: FC<Props> = ({
     dispatch(addSavedQueryToTabState(queryEditor, savedQuery));
   };
 
-  const renderEditorBottomBar = (hideActions: boolean) => {
+  const renderEditorPrimaryAction = () => {
     const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } = database || {};
-
-    const contributions =
-      ExtensionsManager.getInstance().getMenuContributions('sqllab.editor');
-
-    const primaryContributions = (contributions?.primary || []).map(
-      contribution => {
-        const command = ExtensionsManager.getInstance().getCommandContribution(
-          contribution.command,
-        )!;
-        // @ts-ignore
-        const Icon = Icons[command?.icon as IconNameType];
-
-        return (
-          <Button
-            key={contribution.view}
-            onClick={() => commands.executeCommand(command.command)}
-            tooltip={command?.description}
-            icon={<Icon iconSize="m" iconColor={theme.colorPrimary} />}
-            buttonSize="small"
-          >
-            {command?.title}
-          </Button>
-        );
-      },
-    );
-
     const showMenu = allowCTAS || allowCVAS;
     const menuItems: MenuItemType[] = [
       allowCTAS && {
@@ -778,93 +752,62 @@ const SqlEditor: FC<Props> = ({
     const runMenuBtn = <Menu items={menuItems} />;
 
     return (
-      <StyledToolbar className="sql-toolbar" id="js-sql-toolbar">
-        {hideActions ? (
-          <Alert
-            type="warning"
-            message={t(
-              'The database that was used to generate this query could not be 
found',
-            )}
-            description={t(
-              'Choose one of the available databases on the left panel.',
-            )}
-            closable={false}
+      <>
+        <RunQueryActionButton
+          queryEditorId={queryEditor.id}
+          queryState={latestQuery?.state}
+          runQuery={runQuery}
+          stopQuery={stopQuery}
+          overlayCreateAsMenu={showMenu ? runMenuBtn : null}
+        />
+        <span>
+          <QueryLimitSelect
+            queryEditorId={queryEditor.id}
+            maxRow={maxRow}
+            defaultQueryLimit={defaultQueryLimit}
           />
-        ) : (
-          <>
-            <div className="leftItems">
-              <span>
-                <RunQueryActionButton
-                  allowAsync={database?.allow_run_async === true}
-                  queryEditorId={queryEditor.id}
-                  queryState={latestQuery?.state}
-                  runQuery={runQuery}
-                  stopQuery={stopQuery}
-                  overlayCreateAsMenu={showMenu ? runMenuBtn : null}
-                />
-              </span>
-              {isFeatureEnabled(FeatureFlag.EstimateQueryCost) &&
-                database?.allows_cost_estimate && (
-                  <span>
-                    <EstimateQueryCostButton
-                      getEstimate={getQueryCostEstimate}
-                      queryEditorId={queryEditor.id}
-                      tooltip={t('Estimate the cost before running a query')}
-                    />
-                  </span>
-                )}
-              <span>
-                <QueryLimitSelect
-                  queryEditorId={queryEditor.id}
-                  maxRow={maxRow}
-                  defaultQueryLimit={defaultQueryLimit}
-                />
-              </span>
-              {latestQuery && (
-                <Timer
-                  startTime={latestQuery.startDttm}
-                  endTime={latestQuery.endDttm}
-                  status={STATE_TYPE_MAP[latestQuery.state]}
-                  isRunning={latestQuery.state === 'running'}
-                />
-              )}
-            </div>
-            <div className="rightItems">
-              <span>
-                <SaveQuery
-                  queryEditorId={queryEditor.id}
-                  columns={latestQuery?.results?.columns || []}
-                  onSave={onSaveQuery}
-                  onUpdate={(query, remoteId) =>
-                    dispatch(updateSavedQuery(query, remoteId))
-                  }
-                  saveQueryWarning={saveQueryWarning}
-                  database={database}
-                />
-              </span>
-              <span>
-                <ShareSqlLabQuery queryEditorId={queryEditor.id} />
-              </span>
-              <div>{primaryContributions}</div>
-              <Dropdown
-                popupRender={() => renderDropdown()}
-                trigger={['click']}
-              >
-                <Button
-                  buttonSize="xsmall"
-                  showMarginRight={false}
-                  buttonStyle="link"
-                >
-                  <Icons.EllipsisOutlined />
-                </Button>
-              </Dropdown>
-            </div>
-          </>
-        )}
-      </StyledToolbar>
+        </span>
+        <Divider type="vertical" />
+        {isFeatureEnabled(FeatureFlag.EstimateQueryCost) &&
+          database?.allows_cost_estimate && (
+            <span>
+              <EstimateQueryCostButton
+                getEstimate={getQueryCostEstimate}
+                queryEditorId={queryEditor.id}
+                tooltip={t('Estimate the cost before running a query')}
+              />
+            </span>
+          )}
+        <SaveQuery
+          queryEditorId={queryEditor.id}
+          columns={latestQuery?.results?.columns || []}
+          onSave={onSaveQuery}
+          onUpdate={(query, remoteId) =>
+            dispatch(updateSavedQuery(query, remoteId))
+          }
+          saveQueryWarning={saveQueryWarning}
+          database={database}
+        />
+        <ShareSqlLabQuery queryEditorId={queryEditor.id} />
+      </>
     );
   };
 
+  const renderEmptyAlert = () => (
+    <StyledToolbar className="sql-toolbar" id="js-sql-toolbar">
+      <Alert
+        type="warning"
+        message={t(
+          'The database that was used to generate this query could not be 
found',
+        )}
+        description={t(
+          'Choose one of the available databases on the left panel.',
+        )}
+        closable={false}
+      />
+    </StyledToolbar>
+  );
+
   const handleCursorPositionChange = (newPosition: CursorPosition) => {
     dispatch(queryEditorSetCursorPosition(queryEditor, newPosition));
   };
@@ -950,13 +893,13 @@ const SqlEditor: FC<Props> = ({
         className="queryPane"
       >
         <div className="north-pane">
-          {SqlFormExtension && (
-            <SqlFormExtension
+          {showEmptyState ? (
+            renderEmptyAlert()
+          ) : (
+            <SqlEditorTopBar
               queryEditorId={queryEditor.id}
-              setQueryEditorAndSaveSqlWithDebounce={
-                setQueryEditorAndSaveSqlWithDebounce
-              }
-              startQuery={startQuery}
+              defaultPrimaryActions={renderEditorPrimaryAction()}
+              defaultSecondaryActions={getSecondaryMenuItems()}
             />
           )}
           {queryEditor.isDataset && renderDatasetWarning()}
@@ -977,7 +920,15 @@ const SqlEditor: FC<Props> = ({
               }
             </AutoSizer>
           </div>
-          {renderEditorBottomBar(showEmptyState)}
+          {SqlFormExtension && (
+            <SqlFormExtension
+              queryEditorId={queryEditor.id}
+              setQueryEditorAndSaveSqlWithDebounce={
+                setQueryEditorAndSaveSqlWithDebounce
+              }
+              startQuery={startQuery}
+            />
+          )}
         </div>
       </Splitter.Panel>
       <Splitter.Panel className="queryPane">
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx 
b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 7d9a025864..5010298da0 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -16,36 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useEffect, useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 
 import { SqlLabRootState, Table } from 'src/SqlLab/types';
 import {
-  queryEditorSetDb,
   addTable,
   removeTables,
   collapseTable,
   expandTable,
-  queryEditorSetCatalog,
-  queryEditorSetSchema,
-  setDatabases,
-  addDangerToast,
   resetState,
-  type Database,
 } from 'src/SqlLab/actions/sqlLab';
 import { Button, EmptyState, Icons } from '@superset-ui/core/components';
-import { type DatabaseObject } from 'src/components';
 import { t } from '@apache-superset/core';
 import { styled, css } from '@apache-superset/core/ui';
 import { TableSelectorMultiple } from 'src/components/TableSelector';
 import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
-import {
-  getItem,
-  LocalStorageKeys,
-  setItem,
-} from 'src/utils/localStorageHelpers';
 import { noop } from 'lodash';
 import TableElement from '../TableElement';
+import useDatabaseSelector from '../SqlEditorTopBar/useDatabaseSelector';
 
 export interface SqlEditorLeftBarProps {
   queryEditorId: string;
@@ -70,10 +59,8 @@ const LeftBarStyles = styled.div`
 `;
 
 const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
-  const databases = useSelector<
-    SqlLabRootState,
-    SqlLabRootState['sqlLab']['databases']
-  >(({ sqlLab }) => sqlLab.databases);
+  const { db: userSelectedDb, ...dbSelectorProps } =
+    useDatabaseSelector(queryEditorId);
   const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
     ({ sqlLab }) =>
       sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
@@ -86,16 +73,8 @@ const SqlEditorLeftBar = ({ queryEditorId }: 
SqlEditorLeftBarProps) => {
     'schema',
     'tabViewId',
   ]);
-  const database = useMemo(
-    () => (queryEditor.dbId ? databases[queryEditor.dbId] : undefined),
-    [databases, queryEditor.dbId],
-  );
-
   const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
-  const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
-    null,
-  );
-  const { dbId, catalog, schema } = queryEditor;
+  const { dbId, schema } = queryEditor;
   const tables = useMemo(
     () =>
       allSelectedTables.filter(
@@ -106,29 +85,10 @@ const SqlEditorLeftBar = ({ queryEditorId }: 
SqlEditorLeftBarProps) => {
 
   noop(_emptyResultsWithSearch); // This is to avoid unused variable warning, 
can be removed if not needed
 
-  useEffect(() => {
-    const bool = new URLSearchParams(window.location.search).get('db');
-    const userSelected = getItem(
-      LocalStorageKeys.Database,
-      null,
-    ) as DatabaseObject | null;
-
-    if (bool && userSelected) {
-      setUserSelected(userSelected);
-      setItem(LocalStorageKeys.Database, null);
-    } else if (database) {
-      setUserSelected(database);
-    }
-  }, [database]);
-
   const onEmptyResults = useCallback((searchText?: string) => {
     setEmptyResultsWithSearch(!!searchText);
   }, []);
 
-  const onDbChange = ({ id: dbId }: { id: number }) => {
-    dispatch(queryEditorSetDb(queryEditor, dbId));
-  };
-
   const selectedTableNames = useMemo(
     () => tables?.map(table => table.name) || [],
     [tables],
@@ -176,38 +136,6 @@ const SqlEditorLeftBar = ({ queryEditorId }: 
SqlEditorLeftBarProps) => {
 
   const shouldShowReset = window.location.search === '?reset=1';
 
-  const handleCatalogChange = useCallback(
-    (catalog: string | null) => {
-      if (queryEditor) {
-        dispatch(queryEditorSetCatalog(queryEditor, catalog));
-      }
-    },
-    [dispatch, queryEditor],
-  );
-
-  const handleSchemaChange = useCallback(
-    (schema: string) => {
-      if (queryEditor) {
-        dispatch(queryEditorSetSchema(queryEditor, schema));
-      }
-    },
-    [dispatch, queryEditor],
-  );
-
-  const handleDbList = useCallback(
-    (result: DatabaseObject[]) => {
-      dispatch(setDatabases(result as unknown as Database[]));
-    },
-    [dispatch],
-  );
-
-  const handleError = useCallback(
-    (message: string) => {
-      dispatch(addDangerToast(message));
-    },
-    [dispatch],
-  );
-
   const handleResetState = useCallback(() => {
     dispatch(resetState());
   }, [dispatch]);
@@ -215,16 +143,10 @@ const SqlEditorLeftBar = ({ queryEditorId }: 
SqlEditorLeftBarProps) => {
   return (
     <LeftBarStyles data-test="sql-editor-left-bar">
       <TableSelectorMultiple
+        {...dbSelectorProps}
         onEmptyResults={onEmptyResults}
         emptyState={<EmptyState />}
         database={userSelectedDb}
-        getDbList={handleDbList}
-        handleError={handleError}
-        onDbChange={onDbChange}
-        onCatalogChange={handleCatalogChange}
-        catalog={catalog}
-        onSchemaChange={handleSchemaChange}
-        schema={schema}
         onTableSelectChange={onTablesChange}
         tableValue={selectedTableNames}
         sqlLabMode
diff --git 
a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/SqlEditorTopBar.test.tsx
 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/SqlEditorTopBar.test.tsx
new file mode 100644
index 0000000000..d5a6871d52
--- /dev/null
+++ 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/SqlEditorTopBar.test.tsx
@@ -0,0 +1,130 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import { MenuItemType } from '@superset-ui/core/components/Menu';
+import SqlEditorTopBar, {
+  SqlEditorTopBarProps,
+} from 'src/SqlLab/components/SqlEditorTopBar';
+
+jest.mock('src/components/MenuListExtension', () => ({
+  __esModule: true,
+  default: ({
+    children,
+    viewId,
+    primary,
+    secondary,
+    defaultItems,
+  }: {
+    children?: React.ReactNode;
+    viewId: string;
+    primary?: boolean;
+    secondary?: boolean;
+    defaultItems?: MenuItemType[];
+  }) => (
+    <div
+      data-test="mock-menu-extension"
+      data-view-id={viewId}
+      data-primary={primary}
+      data-secondary={secondary}
+      data-default-items-count={defaultItems?.length ?? 0}
+    >
+      {children}
+    </div>
+  ),
+}));
+
+const defaultProps: SqlEditorTopBarProps = {
+  queryEditorId: 'test-query-editor-id',
+  defaultPrimaryActions: <button type="button">Primary Action</button>,
+  defaultSecondaryActions: [
+    { key: 'action1', label: 'Action 1' },
+    { key: 'action2', label: 'Action 2' },
+  ],
+};
+
+const setup = (props?: Partial<SqlEditorTopBarProps>) =>
+  render(<SqlEditorTopBar {...defaultProps} {...props} />);
+
+test('renders SqlEditorTopBar component', () => {
+  setup();
+  const menuExtensions = screen.getAllByTestId('mock-menu-extension');
+  expect(menuExtensions).toHaveLength(2);
+});
+
+test('renders primary MenuListExtension with correct props', () => {
+  setup();
+  const menuExtensions = screen.getAllByTestId('mock-menu-extension');
+  const primaryExtension = menuExtensions[0];
+
+  expect(primaryExtension).toHaveAttribute('data-view-id', 'sqllab.editor');
+  expect(primaryExtension).toHaveAttribute('data-primary', 'true');
+});
+
+test('renders secondary MenuListExtension with correct props', () => {
+  setup();
+  const menuExtensions = screen.getAllByTestId('mock-menu-extension');
+  const secondaryExtension = menuExtensions[1];
+
+  expect(secondaryExtension).toHaveAttribute('data-view-id', 'sqllab.editor');
+  expect(secondaryExtension).toHaveAttribute('data-secondary', 'true');
+  expect(secondaryExtension).toHaveAttribute('data-default-items-count', '2');
+});
+
+test('renders defaultPrimaryActions as children of primary MenuListExtension', 
() => {
+  setup();
+  expect(
+    screen.getByRole('button', { name: 'Primary Action' }),
+  ).toBeInTheDocument();
+});
+
+test('renders with custom primary actions', () => {
+  const customPrimaryActions = (
+    <>
+      <button type="button">Custom Action 1</button>
+      <button type="button">Custom Action 2</button>
+    </>
+  );
+
+  setup({ defaultPrimaryActions: customPrimaryActions });
+
+  expect(
+    screen.getByRole('button', { name: 'Custom Action 1' }),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'Custom Action 2' }),
+  ).toBeInTheDocument();
+});
+
+test('renders with empty secondary actions', () => {
+  setup({ defaultSecondaryActions: [] });
+
+  const menuExtensions = screen.getAllByTestId('mock-menu-extension');
+  const secondaryExtension = menuExtensions[1];
+
+  expect(secondaryExtension).toHaveAttribute('data-default-items-count', '0');
+});
+
+test('passes correct viewId (ViewContribution.Editor) to MenuListExtension', 
() => {
+  setup();
+  const menuExtensions = screen.getAllByTestId('mock-menu-extension');
+
+  menuExtensions.forEach(extension => {
+    expect(extension).toHaveAttribute('data-view-id', 'sqllab.editor');
+  });
+});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx
new file mode 100644
index 0000000000..f8b7ead860
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx
@@ -0,0 +1,62 @@
+/**
+ * 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 { Divider, Flex } from '@superset-ui/core/components';
+import { styled } from '@apache-superset/core/ui';
+import { ViewContribution } from 'src/SqlLab/contributions';
+import MenuListExtension, {
+  type MenuListExtensionProps,
+} from 'src/components/MenuListExtension';
+
+const StyledFlex = styled(Flex)`
+  margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
+
+  & .ant-divider {
+    margin: ${({ theme }) => theme.sizeUnit * 2}px 0;
+    height: ${({ theme }) => theme.sizeUnit * 6}px;
+  }
+`;
+export interface SqlEditorTopBarProps {
+  queryEditorId: string;
+  defaultPrimaryActions: React.ReactNode;
+  defaultSecondaryActions: MenuListExtensionProps['defaultItems'];
+}
+
+const SqlEditorTopBar = ({
+  defaultPrimaryActions,
+  defaultSecondaryActions,
+}: SqlEditorTopBarProps) => (
+  <StyledFlex justify="space-between" gap="small" id="js-sql-toolbar">
+    <Flex flex={1} gap="small" align="center">
+      <Flex gap="small" align="center">
+        <MenuListExtension viewId={ViewContribution.Editor} primary 
compactMode>
+          {defaultPrimaryActions}
+        </MenuListExtension>
+      </Flex>
+      <Divider type="vertical" />
+      <MenuListExtension
+        viewId={ViewContribution.Editor}
+        secondary
+        defaultItems={defaultSecondaryActions}
+      />
+      <Divider type="vertical" />
+    </Flex>
+  </StyledFlex>
+);
+
+export default SqlEditorTopBar;
diff --git 
a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.test.ts
 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.test.ts
new file mode 100644
index 0000000000..820f820be3
--- /dev/null
+++ 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.test.ts
@@ -0,0 +1,320 @@
+/**
+ * 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 configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { renderHook, act } from '@testing-library/react-hooks';
+import { createWrapper } from 'spec/helpers/testing-library';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import * as localStorageHelpers from 'src/utils/localStorageHelpers';
+
+import useDatabaseSelector from './useDatabaseSelector';
+
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+
+const mockDatabase = {
+  id: 1,
+  database_name: 'main',
+  backend: 'mysql',
+};
+
+const mockDatabases = {
+  [mockDatabase.id]: mockDatabase,
+};
+
+const createInitialState = (overrides = {}) => ({
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    databases: mockDatabases,
+    ...overrides,
+  },
+});
+
+beforeEach(() => {
+  jest.spyOn(localStorageHelpers, 'getItem').mockReturnValue(null);
+  jest.spyOn(localStorageHelpers, 'setItem').mockImplementation(() => {});
+});
+
+afterEach(() => {
+  jest.clearAllMocks();
+  jest.restoreAllMocks();
+});
+
+test('returns initial values from query editor', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  expect(result.current.catalog).toBe(defaultQueryEditor.catalog);
+  expect(result.current.schema).toBe(defaultQueryEditor.schema);
+  expect(typeof result.current.onDbChange).toBe('function');
+  expect(typeof result.current.onCatalogChange).toBe('function');
+  expect(typeof result.current.onSchemaChange).toBe('function');
+  expect(typeof result.current.getDbList).toBe('function');
+  expect(typeof result.current.handleError).toBe('function');
+});
+
+test('returns database when dbId exists in store', () => {
+  const store = mockStore(
+    createInitialState({
+      unsavedQueryEditor: {
+        id: defaultQueryEditor.id,
+        dbId: mockDatabase.id,
+      },
+    }),
+  );
+
+  const { result, rerender } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  // Trigger effect by rerendering
+  rerender();
+
+  expect(result.current.db).toEqual(mockDatabase);
+});
+
+test('dispatches QUERY_EDITOR_SETDB action on onDbChange', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  act(() => {
+    result.current.onDbChange({ id: 2 });
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'QUERY_EDITOR_SETDB',
+      dbId: 2,
+    }),
+  );
+});
+
+test('dispatches queryEditorSetCatalog action on onCatalogChange', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  act(() => {
+    result.current.onCatalogChange('new_catalog');
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'QUERY_EDITOR_SET_CATALOG',
+    }),
+  );
+});
+
+test('dispatches queryEditorSetSchema action on onSchemaChange', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  act(() => {
+    result.current.onSchemaChange('new_schema');
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'QUERY_EDITOR_SET_SCHEMA',
+    }),
+  );
+});
+
+test('dispatches setDatabases action on getDbList', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  const newDatabase = {
+    id: 3,
+    database_name: 'test_db',
+    backend: 'postgresql',
+  };
+
+  act(() => {
+    result.current.getDbList(newDatabase as any);
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'SET_DATABASES',
+    }),
+  );
+});
+
+test('dispatches addDangerToast action on handleError', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  act(() => {
+    result.current.handleError('Test error message');
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'ADD_TOAST',
+      payload: expect.objectContaining({
+        toastType: 'DANGER_TOAST',
+        text: 'Test error message',
+      }),
+    }),
+  );
+});
+
+test('reads database from localStorage when URL has db param', () => {
+  const localStorageDb = {
+    id: 5,
+    database_name: 'local_storage_db',
+    backend: 'sqlite',
+  };
+
+  jest.spyOn(localStorageHelpers, 'getItem').mockReturnValue(localStorageDb);
+
+  const originalLocation = window.location;
+  Object.defineProperty(window, 'location', {
+    value: { search: '?db=true' },
+    writable: true,
+  });
+
+  const store = mockStore(createInitialState());
+  const { result, rerender } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  rerender();
+
+  expect(result.current.db).toEqual(localStorageDb);
+  expect(localStorageHelpers.setItem).toHaveBeenCalledWith(
+    localStorageHelpers.LocalStorageKeys.Database,
+    null,
+  );
+
+  Object.defineProperty(window, 'location', {
+    value: originalLocation,
+    writable: true,
+  });
+});
+
+test('returns null db when dbId does not exist in databases', () => {
+  const store = mockStore(
+    createInitialState({
+      databases: {},
+    }),
+  );
+
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  expect(result.current.db).toBeNull();
+});
+
+test('handles null catalog change', () => {
+  const store = mockStore(createInitialState());
+  const { result } = renderHook(
+    () => useDatabaseSelector(defaultQueryEditor.id),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  act(() => {
+    result.current.onCatalogChange(null);
+  });
+
+  const actions = store.getActions();
+  expect(actions).toContainEqual(
+    expect.objectContaining({
+      type: 'QUERY_EDITOR_SET_CATALOG',
+    }),
+  );
+});
diff --git 
a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts
 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts
new file mode 100644
index 0000000000..984f7b2a89
--- /dev/null
+++ 
b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts
@@ -0,0 +1,126 @@
+/**
+ * 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 { useEffect, useCallback, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { SqlLabRootState } from 'src/SqlLab/types';
+import {
+  queryEditorSetDb,
+  queryEditorSetCatalog,
+  queryEditorSetSchema,
+  setDatabases,
+  addDangerToast,
+  type Database,
+} from 'src/SqlLab/actions/sqlLab';
+import { type DatabaseObject } from 'src/components';
+import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
+import {
+  getItem,
+  LocalStorageKeys,
+  setItem,
+} from 'src/utils/localStorageHelpers';
+
+export default function useDatabaseSelector(queryEditorId: string) {
+  const databases = useSelector<
+    SqlLabRootState,
+    SqlLabRootState['sqlLab']['databases']
+  >(({ sqlLab }) => sqlLab.databases);
+  const dispatch = useDispatch();
+  const queryEditor = useQueryEditor(queryEditorId, [
+    'dbId',
+    'catalog',
+    'schema',
+    'tabViewId',
+  ]);
+  const database = useMemo(
+    () => (queryEditor.dbId ? databases[queryEditor.dbId] : undefined),
+    [databases, queryEditor.dbId],
+  );
+  const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
+    null,
+  );
+  const { catalog, schema } = queryEditor;
+
+  const onDbChange = useCallback(
+    ({ id: dbId }: { id: number }) => {
+      if (queryEditor) {
+        dispatch(queryEditorSetDb(queryEditor, dbId));
+      }
+    },
+    [dispatch, queryEditor],
+  );
+
+  const handleCatalogChange = useCallback(
+    (catalog: string | null) => {
+      if (queryEditor) {
+        dispatch(queryEditorSetCatalog(queryEditor, catalog));
+      }
+    },
+    [dispatch, queryEditor],
+  );
+
+  const handleSchemaChange = useCallback(
+    (schema: string) => {
+      if (queryEditor) {
+        dispatch(queryEditorSetSchema(queryEditor, schema));
+      }
+    },
+    [dispatch, queryEditor],
+  );
+
+  const handleDbList = useCallback(
+    (result: DatabaseObject[]) => {
+      dispatch(setDatabases(result as unknown as Database[]));
+    },
+    [dispatch],
+  );
+
+  const handleError = useCallback(
+    (message: string) => {
+      dispatch(addDangerToast(message));
+    },
+    [dispatch],
+  );
+
+  useEffect(() => {
+    const bool = new URLSearchParams(window.location.search).get('db');
+    const userSelected = getItem(
+      LocalStorageKeys.Database,
+      null,
+    ) as DatabaseObject | null;
+
+    if (bool && userSelected) {
+      setUserSelected(userSelected);
+      setItem(LocalStorageKeys.Database, null);
+    } else if (database) {
+      setUserSelected(database);
+    }
+  }, [database]);
+
+  return {
+    db: userSelectedDb,
+    catalog,
+    schema,
+    getDbList: handleDbList,
+    handleError,
+    onDbChange,
+    onCatalogChange: handleCatalogChange,
+    onSchemaChange: handleSchemaChange,
+  };
+}
diff --git a/superset-frontend/src/SqlLab/contributions.ts 
b/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
similarity index 52%
copy from superset-frontend/src/SqlLab/contributions.ts
copy to superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
index 70f00f4d07..3cf0ea3fb3 100644
--- a/superset-frontend/src/SqlLab/contributions.ts
+++ b/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
@@ -16,7 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export enum ViewContribution {
-  RightSidebar = 'sqllab.rightSidebar',
-  SouthPanels = 'sqllab.panels',
-}
+import { render, screen } from 'spec/helpers/testing-library';
+import StatusBar from 'src/SqlLab/components/StatusBar';
+
+jest.mock('src/extensions/ExtensionsManager', () => {
+  const getInstance = jest.fn().mockReturnValue({
+    getViewContributions: jest
+      .fn()
+      .mockReturnValue([{ id: 'test-status-bar' }]),
+  });
+  return { getInstance };
+});
+
+jest.mock('src/components/ViewListExtension', () => ({
+  __esModule: true,
+  default: ({ viewId }: { viewId: string }) => (
+    <div data-test="mock-view-extension" data-view-id={viewId}>
+      ViewListExtension
+    </div>
+  ),
+}));
+
+test('renders StatusBar component', () => {
+  render(<StatusBar />);
+  expect(screen.getByTestId('mock-view-extension')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/SqlLab/components/StatusBar/index.tsx 
b/superset-frontend/src/SqlLab/components/StatusBar/index.tsx
new file mode 100644
index 0000000000..97ce1e7fbd
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/StatusBar/index.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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 { styled } from '@apache-superset/core';
+import { Flex } from '@superset-ui/core/components';
+import ViewListExtension from 'src/components/ViewListExtension';
+import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { SQL_EDITOR_STATUSBAR_HEIGHT } from 'src/SqlLab/constants';
+import { ViewContribution } from 'src/SqlLab/contributions';
+
+const Container = styled(Flex)`
+  flex-direction: row-reverse;
+  height: ${SQL_EDITOR_STATUSBAR_HEIGHT}px;
+  background-color: ${({ theme }) => theme.colorPrimary};
+  color: ${({ theme }) => theme.colorWhite};
+  padding: 0 ${({ theme }) => theme.sizeUnit * 4}px;
+
+  & .ant-tag {
+    color: ${({ theme }) => theme.colorWhite};
+    background-color: transparent;
+    border: 0;
+  }
+`;
+
+const StatusBar = () => {
+  const statusBarContributions =
+    ExtensionsManager.getInstance().getViewContributions(
+      ViewContribution.StatusBar,
+    ) || [];
+
+  return (
+    <>
+      {statusBarContributions.length > 0 && (
+        <Container align="center" justify="space-between">
+          <ViewListExtension viewId={ViewContribution.StatusBar} />
+        </Container>
+      )}
+    </>
+  );
+};
+
+export default StatusBar;
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx 
b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
index 9a78a116bb..83c870e123 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
@@ -42,6 +42,42 @@ const StyledEditableTabs = styled(EditableTabs)`
   height: 100%;
   display: flex;
   flex-direction: column;
+  & .ant-tabs-nav::before {
+    border-color: ${({ theme }) => theme.colorBorder} !important;
+  }
+  & .ant-tabs-nav-add {
+    border-color: ${({ theme }) => theme.colorBorder} !important;
+    height: 34px;
+  }
+  & .ant-tabs-nav-list {
+    align-items: end;
+    padding-top: 1px;
+    column-gap: ${({ theme }) => theme.sizeUnit}px;
+  }
+  & .ant-tabs-tab-active {
+    border-left-color: ${({ theme }) => theme.colorPrimaryActive} !important;
+    border-top-color: ${({ theme }) => theme.colorPrimaryActive} !important;
+    border-right-color: ${({ theme }) => theme.colorPrimaryActive} !important;
+    box-shadow: 0 0 2px ${({ theme }) => theme.colorPrimaryActive} !important;
+    border-top: 2px;
+  }
+  & .ant-tabs-tab {
+    border-radius: 2px 2px 0px 0px !important;
+    padding: ${({ theme }) => theme.sizeUnit}px
+      ${({ theme }) => theme.sizeUnit * 2}px !important;
+    & + .ant-tabs-nav-add {
+      margin-right: ${({ theme }) => theme.sizeUnit * 4}px;
+    }
+    &:not(.ant-tabs-tab-active) {
+      border-color: ${({ theme }) => theme.colorBorder} !important;
+      box-shadow: inset 0 0 1px ${({ theme }) => theme.colorBorder} !important;
+    }
+  }
+  & .ant-tabs-nav-add {
+    border-radius: 2px 2px 0px 0px !important;
+    min-height: auto !important;
+    align-self: flex-end;
+  }
 `;
 
 const StyledTab = styled.span`
@@ -198,14 +234,14 @@ class TabbedSqlEditors extends 
PureComponent<TabbedSqlEditorsProps> {
         addIcon={
           <Tooltip
             id="add-tab"
-            placement="bottom"
+            placement="left"
             title={
               userOS === 'Windows'
                 ? t('New tab (Ctrl + q)')
                 : t('New tab (Ctrl + t)')
             }
           >
-            <Icons.PlusCircleOutlined
+            <Icons.PlusOutlined
               iconSize="l"
               css={css`
                 vertical-align: middle;
diff --git a/superset-frontend/src/SqlLab/constants.ts 
b/superset-frontend/src/SqlLab/constants.ts
index 3b656c3a46..2158af3c45 100644
--- a/superset-frontend/src/SqlLab/constants.ts
+++ b/superset-frontend/src/SqlLab/constants.ts
@@ -66,9 +66,10 @@ export const TIME_OPTIONS = [
 ];
 
 // SqlEditor layout constants
-export const SQL_EDITOR_GUTTER_HEIGHT = 4;
+export const SQL_EDITOR_GUTTER_HEIGHT = 5;
 export const SQL_EDITOR_LEFTBAR_WIDTH = 400;
 export const SQL_EDITOR_RIGHTBAR_WIDTH = 400;
+export const SQL_EDITOR_STATUSBAR_HEIGHT = 30;
 export const INITIAL_NORTH_PERCENT = 30;
 export const SET_QUERY_EDITOR_SQL_DEBOUNCE_MS = 2000;
 export const VALIDATION_DEBOUNCE_MS = 600;
diff --git a/superset-frontend/src/SqlLab/contributions.ts 
b/superset-frontend/src/SqlLab/contributions.ts
index 70f00f4d07..b9549bed28 100644
--- a/superset-frontend/src/SqlLab/contributions.ts
+++ b/superset-frontend/src/SqlLab/contributions.ts
@@ -18,5 +18,7 @@
  */
 export enum ViewContribution {
   RightSidebar = 'sqllab.rightSidebar',
-  SouthPanels = 'sqllab.panels',
+  Panels = 'sqllab.panels',
+  Editor = 'sqllab.editor',
+  StatusBar = 'sqllab.statusBar',
 }
diff --git 
a/superset-frontend/src/components/MenuListExtension/MenuListExtension.test.tsx 
b/superset-frontend/src/components/MenuListExtension/MenuListExtension.test.tsx
new file mode 100644
index 0000000000..853c58c8aa
--- /dev/null
+++ 
b/superset-frontend/src/components/MenuListExtension/MenuListExtension.test.tsx
@@ -0,0 +1,374 @@
+/**
+ * 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import type { contributions, core } from '@apache-superset/core';
+import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { commands } from 'src/core';
+import MenuListExtension from '.';
+
+jest.mock('src/core', () => ({
+  commands: {
+    executeCommand: jest.fn(),
+  },
+}));
+
+function createMockCommand(
+  command: string,
+  overrides: Partial<contributions.CommandContribution> = {},
+): contributions.CommandContribution {
+  return {
+    command,
+    icon: 'PlusOutlined',
+    title: `${command} Title`,
+    description: `${command} description`,
+    ...overrides,
+  };
+}
+
+function createMockMenuItem(
+  view: string,
+  command: string,
+): contributions.MenuItem {
+  return {
+    view,
+    command,
+  };
+}
+
+function createMockMenu(
+  overrides: Partial<contributions.MenuContribution> = {},
+): contributions.MenuContribution {
+  return {
+    context: [],
+    primary: [],
+    secondary: [],
+    ...overrides,
+  };
+}
+
+function createMockExtension(
+  options: Partial<core.Extension> & {
+    commands?: contributions.CommandContribution[];
+    menus?: Record<string, contributions.MenuContribution>;
+  } = {},
+): core.Extension {
+  const {
+    id = 'test-extension',
+    name = 'Test Extension',
+    commands: cmds = [],
+    menus = {},
+  } = options;
+
+  return {
+    id,
+    name,
+    description: 'A test extension',
+    version: '1.0.0',
+    dependencies: [],
+    remoteEntry: '',
+    exposedModules: [],
+    extensionDependencies: [],
+    contributions: {
+      commands: cmds,
+      menus,
+      views: {},
+    },
+    activate: jest.fn(),
+    deactivate: jest.fn(),
+  };
+}
+
+function setupActivatedExtension(
+  manager: ExtensionsManager,
+  extension: core.Extension,
+) {
+  const context = { disposables: [] };
+  (manager as any).contextIndex.set(extension.id, context);
+  (manager as any).extensionContributions.set(extension.id, {
+    commands: extension.contributions.commands,
+    menus: extension.contributions.menus,
+    views: extension.contributions.views,
+  });
+}
+
+async function createActivatedExtension(
+  manager: ExtensionsManager,
+  extensionOptions: Parameters<typeof createMockExtension>[0] = {},
+): Promise<core.Extension> {
+  const mockExtension = createMockExtension(extensionOptions);
+  await manager.initializeExtension(mockExtension);
+  setupActivatedExtension(manager, mockExtension);
+  return mockExtension;
+}
+
+const TEST_VIEW_ID = 'test.menu';
+
+beforeEach(() => {
+  (ExtensionsManager as any).instance = undefined;
+  jest.clearAllMocks();
+});
+
+afterEach(() => {
+  (ExtensionsManager as any).instance = undefined;
+});
+
+test('renders children when primary mode with no extensions', () => {
+  render(
+    <MenuListExtension viewId={TEST_VIEW_ID} primary>
+      <button type="button">Child Button</button>
+    </MenuListExtension>,
+  );
+
+  expect(
+    screen.getByRole('button', { name: 'Child Button' }),
+  ).toBeInTheDocument();
+});
+
+test('renders primary actions from extension contributions', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.action')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        primary: [createMockMenuItem('test-view', 'test.action')],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} primary />);
+
+  expect(screen.getByText('test.action Title')).toBeInTheDocument();
+});
+
+test('renders primary actions with children', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.action')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        primary: [createMockMenuItem('test-view', 'test.action')],
+      }),
+    },
+  });
+
+  render(
+    <MenuListExtension viewId={TEST_VIEW_ID} primary>
+      <button type="button">Child Button</button>
+    </MenuListExtension>,
+  );
+
+  expect(screen.getByText('test.action Title')).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'Child Button' }),
+  ).toBeInTheDocument();
+});
+
+test('hides title in compact mode for primary actions', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.action')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        primary: [createMockMenuItem('test-view', 'test.action')],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} primary compactMode />);
+
+  expect(screen.queryByText('test.action Title')).not.toBeInTheDocument();
+  expect(screen.getByRole('button')).toBeInTheDocument();
+});
+
+test('executes command when primary action button is clicked', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.action')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        primary: [createMockMenuItem('test-view', 'test.action')],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} primary />);
+
+  const button = screen.getByRole('button', { name: 'test.action Title' });
+  await userEvent.click(button);
+
+  expect(commands.executeCommand).toHaveBeenCalledWith('test.action');
+});
+
+test('returns null when secondary mode with no actions and no defaultItems', 
() => {
+  const { container } = render(
+    <MenuListExtension viewId={TEST_VIEW_ID} secondary />,
+  );
+
+  expect(container).toBeEmptyDOMElement();
+});
+
+test('renders dropdown button when secondary mode with defaultItems', () => {
+  render(
+    <MenuListExtension
+      viewId={TEST_VIEW_ID}
+      secondary
+      defaultItems={[{ key: 'item1', label: 'Item 1' }]}
+    />,
+  );
+
+  expect(screen.getByRole('button')).toBeInTheDocument();
+});
+
+test('renders dropdown menu with defaultItems when clicked', async () => {
+  render(
+    <MenuListExtension
+      viewId={TEST_VIEW_ID}
+      secondary
+      defaultItems={[
+        { key: 'item1', label: 'Item 1' },
+        { key: 'item2', label: 'Item 2' },
+      ]}
+    />,
+  );
+
+  const dropdownButton = screen.getByRole('button');
+  await userEvent.click(dropdownButton);
+
+  await waitFor(() => {
+    expect(screen.getByText('Item 1')).toBeInTheDocument();
+    expect(screen.getByText('Item 2')).toBeInTheDocument();
+  });
+});
+
+test('renders secondary actions from extension contributions', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.secondary')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        secondary: [createMockMenuItem('test-view', 'test.secondary')],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} secondary />);
+
+  const dropdownButton = screen.getByRole('button');
+  await userEvent.click(dropdownButton);
+
+  await waitFor(() => {
+    expect(screen.getByText('test.secondary Title')).toBeInTheDocument();
+  });
+});
+
+test('merges extension secondary actions with defaultItems', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.secondary')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        secondary: [createMockMenuItem('test-view', 'test.secondary')],
+      }),
+    },
+  });
+
+  render(
+    <MenuListExtension
+      viewId={TEST_VIEW_ID}
+      secondary
+      defaultItems={[{ key: 'default-item', label: 'Default Item' }]}
+    />,
+  );
+
+  const dropdownButton = screen.getByRole('button');
+  await userEvent.click(dropdownButton);
+
+  await waitFor(() => {
+    expect(screen.getByText('test.secondary Title')).toBeInTheDocument();
+    expect(screen.getByText('Default Item')).toBeInTheDocument();
+  });
+});
+
+test('executes command when secondary menu item is clicked', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [createMockCommand('test.secondary')],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        secondary: [createMockMenuItem('test-view', 'test.secondary')],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} secondary />);
+
+  const dropdownButton = screen.getByRole('button');
+  await userEvent.click(dropdownButton);
+
+  await waitFor(() => {
+    expect(screen.getByText('test.secondary Title')).toBeInTheDocument();
+  });
+
+  const menuItem = screen.getByText('test.secondary Title');
+  await userEvent.click(menuItem);
+
+  expect(commands.executeCommand).toHaveBeenCalledWith('test.secondary');
+});
+
+test('renders multiple primary actions from multiple contributions', async () 
=> {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    commands: [
+      createMockCommand('test.action1'),
+      createMockCommand('test.action2'),
+    ],
+    menus: {
+      [TEST_VIEW_ID]: createMockMenu({
+        primary: [
+          createMockMenuItem('test-view1', 'test.action1'),
+          createMockMenuItem('test-view2', 'test.action2'),
+        ],
+      }),
+    },
+  });
+
+  render(<MenuListExtension viewId={TEST_VIEW_ID} primary />);
+
+  expect(await screen.findByText('test.action1 Title')).toBeInTheDocument();
+  expect(screen.getByText('test.action2 Title')).toBeInTheDocument();
+});
+
+test('handles viewId with no matching contributions', () => {
+  render(
+    <MenuListExtension viewId="nonexistent.menu" primary>
+      <button type="button">Fallback</button>
+    </MenuListExtension>,
+  );
+
+  expect(screen.getByRole('button', { name: 'Fallback' })).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/MenuListExtension/index.tsx 
b/superset-frontend/src/components/MenuListExtension/index.tsx
new file mode 100644
index 0000000000..98608f6a29
--- /dev/null
+++ b/superset-frontend/src/components/MenuListExtension/index.tsx
@@ -0,0 +1,157 @@
+/**
+ * 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 { useMemo } from 'react';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { Button, Dropdown } from '@superset-ui/core/components';
+import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { commands } from 'src/core';
+import ExtensionsManager from 'src/extensions/ExtensionsManager';
+
+export type MenuListExtensionProps = {
+  viewId: string;
+} & (
+  | {
+      primary: boolean;
+      secondary?: never;
+      children?: React.ReactNode;
+      defaultItems?: never;
+      compactMode?: boolean;
+    }
+  | {
+      primary?: never;
+      secondary: boolean;
+      children?: never;
+      defaultItems?: MenuItemType[];
+      compactMode?: never;
+    }
+);
+
+const MenuListExtension = ({
+  viewId,
+  primary,
+  secondary,
+  defaultItems,
+  children,
+  compactMode,
+}: MenuListExtensionProps) => {
+  const theme = useTheme();
+  const contributions =
+    ExtensionsManager.getInstance().getMenuContributions(viewId);
+
+  const actions = primary ? contributions?.primary : contributions?.secondary;
+  const primaryActions = useMemo(
+    () =>
+      primary
+        ? (actions || []).map(contribution => {
+            const command =
+              ExtensionsManager.getInstance().getCommandContribution(
+                contribution.command,
+              )!;
+            if (!command?.icon) {
+              return null;
+            }
+            const Icon =
+              (Icons as Record<string, typeof Icons.FileOutlined>)[
+                command.icon
+              ] ?? Icons.FileOutlined;
+
+            return (
+              <Button
+                key={contribution.view}
+                onClick={() => commands.executeCommand(command?.command)}
+                tooltip={command?.description ?? command?.title}
+                icon={<Icon iconSize="m" />}
+                buttonSize="small"
+                aria-label={command?.title}
+                {...(compactMode && { variant: 'text', color: 'primary' })}
+              >
+                {!compactMode ? command?.title : undefined}
+              </Button>
+            );
+          })
+        : [],
+    [actions, primary, compactMode],
+  );
+  const secondaryActions = useMemo(
+    () =>
+      secondary
+        ? (actions || [])
+            .map(contribution => {
+              const command =
+                ExtensionsManager.getInstance().getCommandContribution(
+                  contribution.command,
+                )!;
+              if (!command) {
+                return null;
+              }
+              return {
+                key: command.command,
+                label: command.title,
+                title: command.description,
+                onClick: () => commands.executeCommand(command.command),
+              } as MenuItemType;
+            })
+            .concat(...(defaultItems || []))
+            .filter(Boolean)
+        : [],
+    [actions, secondary, defaultItems],
+  );
+
+  if (secondary && secondaryActions.length === 0) {
+    return null;
+  }
+
+  if (secondary) {
+    return (
+      <Dropdown
+        popupRender={() => (
+          <Menu
+            css={css`
+              & .ant-dropdown-menu-title-content > div {
+                gap: ${theme.sizeUnit * 4}px;
+              }
+            `}
+            items={secondaryActions}
+          />
+        )}
+        trigger={['click']}
+      >
+        <Button
+          showMarginRight={false}
+          color="primary"
+          variant="text"
+          css={css`
+            padding: 8px;
+          `}
+        >
+          <Icons.MoreOutlined />
+        </Button>
+      </Dropdown>
+    );
+  }
+  return (
+    <>
+      {primaryActions}
+      {children}
+    </>
+  );
+};
+
+export default MenuListExtension;
diff --git 
a/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx 
b/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx
new file mode 100644
index 0000000000..906c1d9151
--- /dev/null
+++ 
b/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx
@@ -0,0 +1,198 @@
+/**
+ * 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 { ReactElement } from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import type { contributions, core } from '@apache-superset/core';
+import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
+import ViewListExtension from '.';
+
+function createMockView(
+  id: string,
+  overrides: Partial<contributions.ViewContribution> = {},
+): contributions.ViewContribution {
+  return {
+    id,
+    name: `${id} View`,
+    ...overrides,
+  };
+}
+
+function createMockExtension(
+  options: Partial<core.Extension> & {
+    views?: Record<string, contributions.ViewContribution[]>;
+  } = {},
+): core.Extension {
+  const {
+    id = 'test-extension',
+    name = 'Test Extension',
+    views = {},
+  } = options;
+
+  return {
+    id,
+    name,
+    description: 'A test extension',
+    version: '1.0.0',
+    dependencies: [],
+    remoteEntry: '',
+    exposedModules: [],
+    extensionDependencies: [],
+    contributions: {
+      commands: [],
+      menus: {},
+      views,
+    },
+    activate: jest.fn(),
+    deactivate: jest.fn(),
+  };
+}
+
+function setupActivatedExtension(
+  manager: ExtensionsManager,
+  extension: core.Extension,
+) {
+  const context = { disposables: [] };
+  (manager as any).contextIndex.set(extension.id, context);
+  (manager as any).extensionContributions.set(extension.id, {
+    commands: extension.contributions.commands,
+    menus: extension.contributions.menus,
+    views: extension.contributions.views,
+  });
+}
+
+async function createActivatedExtension(
+  manager: ExtensionsManager,
+  extensionOptions: Parameters<typeof createMockExtension>[0] = {},
+): Promise<core.Extension> {
+  const mockExtension = createMockExtension(extensionOptions);
+  await manager.initializeExtension(mockExtension);
+  setupActivatedExtension(manager, mockExtension);
+  return mockExtension;
+}
+
+const TEST_VIEW_ID = 'test.view';
+
+const renderWithExtensionsProvider = (ui: ReactElement) => {
+  return render(ui, { wrapper: ExtensionsProvider as any });
+};
+
+beforeEach(() => {
+  (ExtensionsManager as any).instance = undefined;
+});
+
+afterEach(() => {
+  (ExtensionsManager as any).instance = undefined;
+});
+
+test('renders nothing when no view contributions exist', () => {
+  const { container } = renderWithExtensionsProvider(
+    <ViewListExtension viewId={TEST_VIEW_ID} />,
+  );
+
+  expect(container.firstChild?.childNodes.length ?? 0).toBe(0);
+});
+
+test('renders placeholder for unregistered view provider', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    views: {
+      [TEST_VIEW_ID]: [createMockView('test-view-1')],
+    },
+  });
+
+  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
+
+  expect(screen.getByText(/test-view-1/)).toBeInTheDocument();
+});
+
+test('renders multiple view placeholders for multiple contributions', async () 
=> {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    views: {
+      [TEST_VIEW_ID]: [
+        createMockView('test-view-1'),
+        createMockView('test-view-2'),
+      ],
+    },
+  });
+
+  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
+
+  expect(screen.getByText(/test-view-1/)).toBeInTheDocument();
+  expect(screen.getByText(/test-view-2/)).toBeInTheDocument();
+});
+
+test('renders nothing for viewId with no matching contributions', () => {
+  const { container } = renderWithExtensionsProvider(
+    <ViewListExtension viewId="nonexistent.view" />,
+  );
+
+  expect(container.firstChild?.childNodes.length ?? 0).toBe(0);
+});
+
+test('handles multiple extensions with views for same viewId', async () => {
+  const manager = ExtensionsManager.getInstance();
+
+  await createActivatedExtension(manager, {
+    id: 'extension-1',
+    views: {
+      [TEST_VIEW_ID]: [createMockView('ext1-view')],
+    },
+  });
+
+  await createActivatedExtension(manager, {
+    id: 'extension-2',
+    views: {
+      [TEST_VIEW_ID]: [createMockView('ext2-view')],
+    },
+  });
+
+  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
+
+  expect(screen.getByText(/ext1-view/)).toBeInTheDocument();
+  expect(screen.getByText(/ext2-view/)).toBeInTheDocument();
+});
+
+test('renders views for different viewIds independently', async () => {
+  const manager = ExtensionsManager.getInstance();
+  const VIEW_ID_A = 'view.a';
+  const VIEW_ID_B = 'view.b';
+
+  await createActivatedExtension(manager, {
+    views: {
+      [VIEW_ID_A]: [createMockView('view-a-component')],
+      [VIEW_ID_B]: [createMockView('view-b-component')],
+    },
+  });
+
+  const { rerender } = renderWithExtensionsProvider(
+    <ViewListExtension viewId={VIEW_ID_A} />,
+  );
+
+  expect(screen.getByText(/view-a-component/)).toBeInTheDocument();
+  expect(screen.queryByText(/view-b-component/)).not.toBeInTheDocument();
+
+  rerender(<ViewListExtension viewId={VIEW_ID_B} />);
+
+  expect(screen.getByText(/view-b-component/)).toBeInTheDocument();
+  expect(screen.queryByText(/view-a-component/)).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/SqlLab/contributions.ts 
b/superset-frontend/src/components/ViewListExtension/index.tsx
similarity index 51%
copy from superset-frontend/src/SqlLab/contributions.ts
copy to superset-frontend/src/components/ViewListExtension/index.tsx
index 70f00f4d07..a7f5209356 100644
--- a/superset-frontend/src/SqlLab/contributions.ts
+++ b/superset-frontend/src/components/ViewListExtension/index.tsx
@@ -16,7 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export enum ViewContribution {
-  RightSidebar = 'sqllab.rightSidebar',
-  SouthPanels = 'sqllab.panels',
+import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
+
+export interface ViewListExtensionProps {
+  viewId: string;
 }
+
+const ViewListExtension = ({ viewId }: ViewListExtensionProps) => {
+  const maybeContributions =
+    ExtensionsManager.getInstance().getViewContributions(viewId);
+  const contributions = Array.isArray(maybeContributions)
+    ? maybeContributions
+    : [];
+  const { getView } = useExtensionsContext();
+
+  return (
+    <>
+      {contributions
+        .filter(
+          contribution =>
+            contribution && typeof contribution.id !== 'undefined',
+        )
+        .map(contribution => getView(contribution.id))}
+    </>
+  );
+};
+
+export default ViewListExtension;

Reply via email to