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

EnxDev pushed a commit to branch test/chatbot-local
in repository https://gitbox.apache.org/repos/asf/superset.git

commit e3efa0ae711979e03fe4998ab4a3d0e602b2109f
Author: Enzo Martellucci <[email protected]>
AuthorDate: Tue May 26 22:51:05 2026 +0200

    feat(extensions): consolidate actions column and live settings sync
    
    - Single Actions column: switch (enable/disable), star (default chatbot,
      chatbot-type only, toggleable), trash (hidden for LOCAL_EXTENSIONS)
    - Add `deletable` field to build_extension_data so the frontend knows
      which extensions can be removed via the UI
    - Add `publisher` field to Extension type for correct delete URL
    - Add notifyExtensionSettingsChanged / subscribeToExtensionSettings
      pub/sub in core/extensions so ChatbotMount re-fetches on any settings
      change without a page reload
    - Wire ChatbotMount to subscribe to settings changes via fetchSettings
      callback
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../src/components/ChatbotMount/index.tsx          |   9 +-
 superset-frontend/src/core/extensions/index.ts     |  13 ++
 .../src/extensions/ExtensionsList.tsx              | 173 ++++++++++++---------
 superset/extensions/utils.py                       |   5 +
 4 files changed, 123 insertions(+), 77 deletions(-)

diff --git a/superset-frontend/src/components/ChatbotMount/index.tsx 
b/superset-frontend/src/components/ChatbotMount/index.tsx
index 68e692c57a7..e85a0d3bf7c 100644
--- a/superset-frontend/src/components/ChatbotMount/index.tsx
+++ b/superset-frontend/src/components/ChatbotMount/index.tsx
@@ -16,12 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
 import { SupersetClient } from '@superset-ui/core';
 import { css, useTheme } from '@apache-superset/core/theme';
 import { ErrorBoundary } from 'src/components/ErrorBoundary';
 import { getActiveChatbot } from 'src/core/chatbot';
 import { subscribeToLocation } from 'src/core/views';
+import { subscribeToExtensionSettings } from 'src/core/extensions';
 import { CHATBOT_LOCATION } from 'src/views/contributions';
 
 const CHATBOT_EDGE_MARGIN = 24;
@@ -34,7 +35,7 @@ const ChatbotMount = () => {
     getActiveChatbot(null, {}),
   );
 
-  useEffect(() => {
+  const fetchSettings = useCallback(() => {
     let cancelled = false;
     SupersetClient.get({ endpoint: '/api/v1/extensions/settings' })
       .then(({ json }) => {
@@ -53,6 +54,10 @@ const ChatbotMount = () => {
     };
   }, []);
 
+  useEffect(() => fetchSettings(), [fetchSettings]);
+
+  useEffect(() => subscribeToExtensionSettings(fetchSettings), 
[fetchSettings]);
+
   useEffect(
     () =>
       subscribeToLocation(CHATBOT_LOCATION, () =>
diff --git a/superset-frontend/src/core/extensions/index.ts 
b/superset-frontend/src/core/extensions/index.ts
index ae49a135aed..72cfd450fce 100644
--- a/superset-frontend/src/core/extensions/index.ts
+++ b/superset-frontend/src/core/extensions/index.ts
@@ -29,3 +29,16 @@ export const extensions: typeof extensionsApi = {
   getExtension,
   getAllExtensions,
 };
+
+const settingsListeners = new Set<() => void>();
+
+export const notifyExtensionSettingsChanged = (): void => {
+  settingsListeners.forEach(fn => fn());
+};
+
+export const subscribeToExtensionSettings = (
+  listener: () => void,
+): (() => void) => {
+  settingsListeners.add(listener);
+  return () => settingsListeners.delete(listener);
+};
diff --git a/superset-frontend/src/extensions/ExtensionsList.tsx 
b/superset-frontend/src/extensions/ExtensionsList.tsx
index e31b14a87f2..5daceb60dc2 100644
--- a/superset-frontend/src/extensions/ExtensionsList.tsx
+++ b/superset-frontend/src/extensions/ExtensionsList.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import { t } from '@apache-superset/core/translation';
-import { css } from '@apache-superset/core/theme';
 import {
   FunctionComponent,
   useCallback,
@@ -27,7 +26,7 @@ import {
   useState,
 } from 'react';
 import { SupersetClient } from '@superset-ui/core';
-import { ConfirmStatusChange, Select, Tooltip } from 
'@superset-ui/core/components';
+import { ConfirmStatusChange, Tooltip } from '@superset-ui/core/components';
 import { Switch } from '@superset-ui/core/components/Switch';
 import { Icons } from '@superset-ui/core/components/Icons';
 import { useListViewResource } from 'src/views/CRUD/hooks';
@@ -37,6 +36,7 @@ import SubMenu, { SubMenuProps } from 
'src/features/home/SubMenu';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import { CHATBOT_LOCATION } from 'src/views/contributions';
 import { getRegisteredViewIds, subscribeToLocation } from 'src/core/views';
+import { notifyExtensionSettingsChanged } from 'src/core/extensions';
 
 const PAGE_SIZE = 25;
 
@@ -45,6 +45,7 @@ type Extension = {
   name: string;
   publisher: string;
   enabled: boolean;
+  deletable: boolean;
 };
 
 type ExtensionSettings = {
@@ -103,6 +104,7 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
       })
         .then(({ json }) => {
           setSettings(json.result);
+          notifyExtensionSettingsChanged();
           addSuccessToast(t('Settings saved.'));
         })
         .catch(() => addDangerToast(t('Failed to save extension settings.')));
@@ -112,17 +114,28 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
 
   const toggleEnabled = useCallback(
     (extensionId: string, enabled: boolean) => {
-      saveSettings({ enabled: { ...settings.enabled, [extensionId]: enabled } 
});
+      saveSettings({
+        enabled: { ...settings.enabled, [extensionId]: enabled },
+      });
+    },
+    [settings, saveSettings],
+  );
+
+  const setDefaultChatbot = useCallback(
+    (extensionId: string) => {
+      const next =
+        settings.active_chatbot_id === extensionId ? null : extensionId;
+      saveSettings({ active_chatbot_id: next });
     },
     [settings, saveSettings],
   );
 
-  const chatbotExtensions = useMemo(() => {
-    const chatbotIds = new Set(getRegisteredViewIds(CHATBOT_LOCATION));
-    return resourceCollection.filter(ext => chatbotIds.has(ext.id));
+  const chatbotIds = useMemo(
+    () => new Set(getRegisteredViewIds(CHATBOT_LOCATION)),
     // chatbotRegistryVersion is intentionally in deps to re-evaluate when 
views register
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [resourceCollection, chatbotRegistryVersion]);
+    [chatbotRegistryVersion],
+  );
 
   const handleUploadClick = () => {
     fileInputRef.current?.click();
@@ -186,7 +199,6 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
       {
         Header: t('Name'),
         accessor: 'name',
-        size: 'lg',
         id: 'name',
         Cell: ({
           row: {
@@ -194,66 +206,96 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
           },
         }: any) => name,
       },
-      {
-        Header: t('Enabled'),
-        accessor: 'enabled',
-        size: 'sm',
-        id: 'enabled',
-        Cell: ({
-          row: {
-            original: { id },
-          },
-        }: any) => (
-          <Switch
-            data-test="toggle-enabled"
-            checked={settings.enabled[id] ?? true}
-            onClick={(checked: boolean) => toggleEnabled(id, checked)}
-            size="small"
-          />
-        ),
-      },
       {
         Header: t('Actions'),
         id: 'actions',
         disableSortBy: true,
-        Cell: ({ row: { original } }: any) => (
-          <ConfirmStatusChange
-            title={t('Please confirm')}
-            description={
-              <>
-                {t('Are you sure you want to delete')} <b>{original.name}</b>?
-              </>
-            }
-            onConfirm={() => handleDelete(original)}
-          >
-            {(confirmDelete: () => void) => (
+        Cell: ({ row: { original } }: any) => {
+          const { id, deletable } = original;
+          const isChatbot = chatbotIds.has(id);
+          const isDefault = settings.active_chatbot_id === id;
+          return (
+            <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
               <Tooltip
-                id="delete-extension-tooltip"
-                title={t('Delete')}
+                id="toggle-enabled-tooltip"
+                title={t('Enable / Disable')}
                 placement="bottom"
               >
-                <span
-                  role="button"
-                  tabIndex={0}
-                  className="action-button"
-                  onClick={confirmDelete}
-                >
-                  <Icons.DeleteOutlined iconSize="l" />
-                </span>
+                <Switch
+                  data-test="toggle-enabled"
+                  checked={settings.enabled[id] ?? true}
+                  onClick={(checked: boolean) => toggleEnabled(id, checked)}
+                  size="small"
+                />
               </Tooltip>
-            )}
-          </ConfirmStatusChange>
-        ),
+              {isChatbot && (
+                <Tooltip
+                  id="set-default-chatbot-tooltip"
+                  title={
+                    isDefault
+                      ? t('Remove default')
+                      : t('Set as default chatbot')
+                  }
+                  placement="bottom"
+                >
+                  <span
+                    role="button"
+                    tabIndex={0}
+                    className="action-button"
+                    onClick={() => setDefaultChatbot(id)}
+                  >
+                    {isDefault ? (
+                      <Icons.StarFilled iconSize="l" />
+                    ) : (
+                      <Icons.StarOutlined iconSize="l" />
+                    )}
+                  </span>
+                </Tooltip>
+              )}
+              {deletable && (
+                <ConfirmStatusChange
+                  title={t('Please confirm')}
+                  description={
+                    <>
+                      {t('Are you sure you want to delete')}{' '}
+                      <b>{original.name}</b>?
+                    </>
+                  }
+                  onConfirm={() => handleDelete(original)}
+                >
+                  {(confirmDelete: () => void) => (
+                    <Tooltip
+                      id="delete-extension-tooltip"
+                      title={t('Delete')}
+                      placement="bottom"
+                    >
+                      <span
+                        role="button"
+                        tabIndex={0}
+                        className="action-button"
+                        onClick={confirmDelete}
+                      >
+                        <Icons.DeleteOutlined iconSize="l" />
+                      </span>
+                    </Tooltip>
+                  )}
+                </ConfirmStatusChange>
+              )}
+            </span>
+          );
+        },
       },
     ],
-    [loading, settings, toggleEnabled],
+    [
+      loading,
+      settings,
+      chatbotIds,
+      toggleEnabled,
+      setDefaultChatbot,
+      handleDelete,
+    ],
   );
 
-  const chatbotOptions = chatbotExtensions.map(ext => ({
-    label: ext.name,
-    value: ext.id,
-  }));
-
   const menuData: SubMenuProps = {
     activeChild: 'Extensions',
     name: t('Extensions'),
@@ -285,25 +327,6 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
         onChange={handleFileChange}
       />
       <SubMenu {...menuData} />
-      {chatbotOptions.length > 1 && (
-        <div style={{ padding: '16px 24px' }}>
-          <label htmlFor="chatbot-select" style={{ marginRight: 8 }}>
-            {t('Default chatbot')}
-          </label>
-          <Select
-            allowClear
-            options={chatbotOptions}
-            value={settings.active_chatbot_id ?? undefined}
-            onChange={value =>
-              saveSettings({ active_chatbot_id: (value as string) ?? null })
-            }
-            placeholder={t('First registered (automatic)')}
-            css={css`
-              width: 280px;
-            `}
-          />
-        </div>
-      )}
       <ListView<Extension>
         columns={columns}
         count={resourceCount}
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 878fbf0bb75..4a51ed1d288 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -232,6 +232,10 @@ def get_loaded_extension(
 
 def build_extension_data(extension: LoadedExtension) -> dict[str, Any]:
     manifest = extension.manifest
+    local_paths = {
+        str((Path(p) / "dist").resolve())
+        for p in current_app.config.get("LOCAL_EXTENSIONS", [])
+    }
     extension_data: dict[str, Any] = {
         "id": manifest.id,
         "publisher": manifest.publisher,
@@ -239,6 +243,7 @@ def build_extension_data(extension: LoadedExtension) -> 
dict[str, Any]:
         "version": extension.version,
         "description": manifest.description or "",
         "dependencies": manifest.dependencies,
+        "deletable": extension.source_base_path not in local_paths,
     }
     if manifest.frontend:
         frontend = manifest.frontend

Reply via email to