You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by en...@apache.org on 2025/12/18 12:57:01 UTC

(superset) branch enxdev/feat/datasource-editor created (now 11c6d7151f)

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

enzomartellucci pushed a change to branch enxdev/feat/datasource-editor
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 11c6d7151f feat(datasource-connector): add schema editor for AI-analyzed tables and columns

This branch includes the following new commits:

     new 11c6d7151f feat(datasource-connector): add schema editor for AI-analyzed tables and columns

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: feat(datasource-connector): add schema editor for AI-analyzed tables and columns

Posted by en...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

enzomartellucci pushed a commit to branch enxdev/feat/datasource-editor
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 11c6d7151f988c6f3483cc330ad1fd079521216d
Author: Enzo Martellucci <en...@gmail.com>
AuthorDate: Thu Dec 18 13:56:24 2025 +0100

    feat(datasource-connector): add schema editor for AI-analyzed tables and columns
    
    Add a new Datasource Editor panel that allows users to review and edit
    AI-generated descriptions for database tables and columns. This editor
    appears after the schema analysis completes in the datasource connector
    wizard flow.
    
    Frontend changes:
    - Add DatasourceEditorPanel with two-column layout (tree view + detail panel)
    - Add SchemaTreeView component using antd Tree for schema navigation
    - Add SchemaDetailPanel for viewing/editing table and column details
    - Add EditableDescription reusable component for inline editing
    - Add useSchemaReport hook for fetching schema reports
    - Add useSchemaEditorMutations hook for updating descriptions
    - Add centralized config.ts for USE_MOCK_DATA flag
    - Update types.ts with AnalyzedTable, AnalyzedColumn, SchemaSelection types
    - Update wizard index.tsx to integrate the editor panel
    - Export AIInfoBanner from superset-ui-core components
    
    Backend changes:
    - Add PUT /api/v1/datasource_analyzer/table/<id> endpoint
    - Add PUT /api/v1/datasource_analyzer/column/<id> endpoint
    - Add POST /api/v1/datasource_analyzer/generate endpoint
    - Add is_primary_key and is_foreign_key columns to AnalyzedColumn model
    - Update analyze.py to populate PK/FK flags
    - Remove redirect logic from datasource_connector.py view
---
 .../superset-ui-core/src/components/index.ts       |   2 +
 .../components/ConnectorLayout.tsx                 |  29 ++-
 .../components/DatasourceEditorPanel.tsx           | 222 ++++++++++++++++++
 .../components/EditableDescription.tsx             | 150 ++++++++++++
 .../components/ReviewSchemaPanel.tsx               |   6 +-
 .../components/SchemaDetailPanel.tsx               | 244 ++++++++++++++++++++
 .../components/SchemaTreeView.tsx                  | 230 ++++++++++++++++++
 .../DatasourceConnector/{types.ts => config.ts}    |  30 +--
 .../hooks/useSchemaEditorMutations.ts              | 162 +++++++++++++
 .../DatasourceConnector/hooks/useSchemaReport.ts   | 223 ++++++++++++++++++
 .../src/pages/DatasourceConnector/index.tsx        |  50 +++-
 .../src/pages/DatasourceConnector/types.ts         |  58 ++++-
 superset/commands/database_analyzer/analyze.py     |  38 +--
 superset/databases/analyzer_api.py                 | 256 ++++++++++++++++++++-
 superset/models/database_analyzer.py               |   2 +
 superset/views/datasource_connector.py             |  26 ---
 16 files changed, 1646 insertions(+), 82 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts
index 758efa5947..19cc01a8a8 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts
@@ -149,6 +149,8 @@ export { Progress, type ProgressProps } from './Progress';
 
 export { Skeleton, type SkeletonProps } from './Skeleton';
 
+export { Spin } from './Spin';
+
 export { Switch, type SwitchProps } from './Switch';
 
 export { TreeSelect, type TreeSelectProps } from './TreeSelect';
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/ConnectorLayout.tsx b/superset-frontend/src/pages/DatasourceConnector/components/ConnectorLayout.tsx
index 59b963d984..7509c09633 100644
--- a/superset-frontend/src/pages/DatasourceConnector/components/ConnectorLayout.tsx
+++ b/superset-frontend/src/pages/DatasourceConnector/components/ConnectorLayout.tsx
@@ -19,7 +19,12 @@
 import { ReactNode } from 'react';
 import { t } from '@superset-ui/core';
 import { styled, css } from '@apache-superset/core/ui';
-import { AIInfoBanner, Flex, Icons, Typography } from '@superset-ui/core/components';
+import {
+  AIInfoBanner,
+  Flex,
+  Icons,
+  Typography,
+} from '@superset-ui/core/components';
 import { ConnectorStep } from '../types';
 
 interface ConnectorLayoutProps {
@@ -134,11 +139,29 @@ const stepsConfig: StepConfig[] = [
   },
 ];
 
+// Map internal steps to visual step index
+// EDIT_SCHEMA is visually part of "Review Schema" step
+function getVisualStepIndex(step: ConnectorStep): number {
+  switch (step) {
+    case ConnectorStep.CONNECT_DATA_SOURCE:
+      return 0;
+    case ConnectorStep.REVIEW_SCHEMA:
+    case ConnectorStep.EDIT_SCHEMA:
+      return 1;
+    case ConnectorStep.GENERATE_DASHBOARD:
+      return 2;
+    default:
+      return 0;
+  }
+}
+
 export default function ConnectorLayout({
   currentStep,
   children,
   templateName,
 }: ConnectorLayoutProps) {
+  const visualStep = getVisualStepIndex(currentStep);
+
   return (
     <PageContainer>
       <PageHeader>
@@ -149,8 +172,8 @@ export default function ConnectorLayout({
       <StepsContainer>
         <Flex align="center" gap={0}>
           {stepsConfig.map((step, index) => {
-            const isActive = index === currentStep;
-            const isCompleted = index < currentStep;
+            const isActive = index === visualStep;
+            const isCompleted = index < visualStep;
 
             return (
               <Flex key={step.title} align="center" gap={8}>
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/DatasourceEditorPanel.tsx b/superset-frontend/src/pages/DatasourceConnector/components/DatasourceEditorPanel.tsx
new file mode 100644
index 0000000000..f23239c32b
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/components/DatasourceEditorPanel.tsx
@@ -0,0 +1,222 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { t } from '@superset-ui/core';
+import { styled } from '@apache-superset/core/ui';
+import {
+  AIInfoBanner,
+  Button,
+  Flex,
+  Icons,
+  Spin,
+  Typography,
+} from '@superset-ui/core/components';
+import useSchemaReport from '../hooks/useSchemaReport';
+import useSchemaEditorMutations from '../hooks/useSchemaEditorMutations';
+import SchemaTreeView from './SchemaTreeView';
+import SchemaDetailPanel from './SchemaDetailPanel';
+import type { SchemaSelection } from '../types';
+
+interface DatasourceEditorPanelProps {
+  reportId: number;
+  dashboardId: number | null;
+  onBack: () => void;
+  onConfirm: (runId: string) => void;
+}
+
+const EditorContainer = styled.div`
+  ${({ theme }) => `
+    width: 100%;
+    max-width: 1200px;
+    display: flex;
+    flex-direction: column;
+    gap: ${theme.marginLG}px;
+  `}
+`;
+
+const ContentGrid = styled.div`
+  ${({ theme }) => `
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: ${theme.marginLG}px;
+
+    @media (max-width: 900px) {
+      grid-template-columns: 1fr;
+    }
+  `}
+`;
+
+const FooterActions = styled(Flex)`
+  ${({ theme }) => `
+    padding-top: ${theme.paddingMD}px;
+    border-top: 1px solid ${theme.colorBorder};
+  `}
+`;
+
+const LoadingContainer = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingXL}px;
+    min-height: 400px;
+  `}
+`;
+
+const ErrorContainer = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingXL}px;
+    background-color: ${theme.colorErrorBg};
+    border: 1px solid ${theme.colorError};
+    border-radius: ${theme.borderRadius}px;
+  `}
+`;
+
+const AIBannerWrapper = styled.div`
+  ${({ theme }) => `
+    width: 100%;
+    margin-bottom: ${theme.marginMD}px;
+  `}
+`;
+
+export default function DatasourceEditorPanel({
+  reportId,
+  dashboardId,
+  onBack,
+  onConfirm,
+}: DatasourceEditorPanelProps) {
+  const { report, loading, error, refetch } = useSchemaReport(reportId);
+  const {
+    updateTableDescription,
+    updateColumnDescription,
+    generateDashboard,
+    mutationState,
+  } = useSchemaEditorMutations();
+
+  const [selection, setSelection] = useState<SchemaSelection>(null);
+  const [isGenerating, setIsGenerating] = useState(false);
+
+  const handleSelectionChange = useCallback((newSelection: SchemaSelection) => {
+    setSelection(newSelection);
+  }, []);
+
+  const handleUpdateTableDescription = useCallback(
+    async (tableId: number, description: string | null): Promise<boolean> => {
+      const success = await updateTableDescription(tableId, description);
+      if (success) {
+        refetch();
+      }
+      return success;
+    },
+    [updateTableDescription, refetch],
+  );
+
+  const handleUpdateColumnDescription = useCallback(
+    async (columnId: number, description: string | null): Promise<boolean> => {
+      const success = await updateColumnDescription(columnId, description);
+      if (success) {
+        refetch();
+      }
+      return success;
+    },
+    [updateColumnDescription, refetch],
+  );
+
+  const handleConfirmAndGenerate = useCallback(async () => {
+    if (!dashboardId) {
+      return;
+    }
+
+    setIsGenerating(true);
+    const runId = await generateDashboard(reportId, dashboardId);
+    setIsGenerating(false);
+
+    if (runId) {
+      onConfirm(runId);
+    }
+  }, [reportId, dashboardId, generateDashboard, onConfirm]);
+
+  if (loading) {
+    return (
+      <EditorContainer>
+        <LoadingContainer vertical align="center" justify="center">
+          <Spin size="large" />
+          <Typography.Text type="secondary" css={{ marginTop: 16 }}>
+            {t('Loading schema report...')}
+          </Typography.Text>
+        </LoadingContainer>
+      </EditorContainer>
+    );
+  }
+
+  if (error || !report) {
+    return (
+      <EditorContainer>
+        <ErrorContainer vertical align="center" gap={16}>
+          <Icons.ExclamationCircleOutlined iconSize="xl" iconColor="error" />
+          <Typography.Text type="danger">
+            {error || t('Failed to load schema report')}
+          </Typography.Text>
+          <Button onClick={refetch}>{t('Retry')}</Button>
+        </ErrorContainer>
+        <FooterActions justify="flex-start">
+          <Button onClick={onBack}>{t('Back')}</Button>
+        </FooterActions>
+      </EditorContainer>
+    );
+  }
+
+  return (
+    <EditorContainer>
+      <AIBannerWrapper>
+        <AIInfoBanner
+          text={t(
+            'Review and edit AI-generated descriptions for your tables and columns. These descriptions help improve data understanding and dashboard generation accuracy.',
+          )}
+          data-test="schema-editor-ai-hint"
+        />
+      </AIBannerWrapper>
+      <ContentGrid>
+        <SchemaTreeView
+          tables={report.tables}
+          selection={selection}
+          onSelectionChange={handleSelectionChange}
+          schemaName={report.schema_name}
+        />
+        <SchemaDetailPanel
+          selection={selection}
+          onUpdateTableDescription={handleUpdateTableDescription}
+          onUpdateColumnDescription={handleUpdateColumnDescription}
+          isUpdating={mutationState.loading}
+        />
+      </ContentGrid>
+
+      <FooterActions justify="space-between" align="center">
+        <Button onClick={onBack} disabled={isGenerating}>
+          {t('Back')}
+        </Button>
+        <Button
+          buttonStyle="primary"
+          onClick={handleConfirmAndGenerate}
+          loading={isGenerating}
+          disabled={!dashboardId}
+        >
+          {t('Confirm Schema & Generate Dashboard')}
+        </Button>
+      </FooterActions>
+    </EditorContainer>
+  );
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/EditableDescription.tsx b/superset-frontend/src/pages/DatasourceConnector/components/EditableDescription.tsx
new file mode 100644
index 0000000000..da1f24fe9c
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/components/EditableDescription.tsx
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback, useEffect } from 'react';
+import { t } from '@superset-ui/core';
+import { styled } from '@apache-superset/core/ui';
+import {
+  Button,
+  Flex,
+  Icons,
+  Input,
+  Typography,
+} from '@superset-ui/core/components';
+
+const { TextArea } = Input;
+
+interface EditableDescriptionProps {
+  description: string | null;
+  placeholder?: string;
+  onSave: (description: string | null) => Promise<boolean>;
+  isUpdating: boolean;
+}
+
+const SectionTitle = styled(Flex)`
+  ${({ theme }) => `
+    margin-bottom: ${theme.marginSM}px;
+  `}
+`;
+
+const DescriptionBox = styled.div`
+  ${({ theme }) => `
+    background-color: ${theme.colorBgLayout};
+    border: 1px solid ${theme.colorBorderSecondary};
+    border-radius: ${theme.borderRadiusSM}px;
+    padding: ${theme.paddingSM}px;
+    min-height: 80px;
+  `}
+`;
+
+const EditActions = styled(Flex)`
+  ${({ theme }) => `
+    margin-top: ${theme.marginSM}px;
+  `}
+`;
+
+export default function EditableDescription({
+  description,
+  placeholder,
+  onSave,
+  isUpdating,
+}: EditableDescriptionProps) {
+  const [isEditing, setIsEditing] = useState(false);
+  const [editedDescription, setEditedDescription] = useState('');
+
+  // Reset editing state when description changes externally
+  useEffect(() => {
+    if (!isEditing) {
+      setEditedDescription(description || '');
+    }
+  }, [description, isEditing]);
+
+  const handleStartEdit = useCallback(() => {
+    setEditedDescription(description || '');
+    setIsEditing(true);
+  }, [description]);
+
+  const handleCancelEdit = useCallback(() => {
+    setIsEditing(false);
+    setEditedDescription(description || '');
+  }, [description]);
+
+  const handleSaveDescription = useCallback(async () => {
+    const success = await onSave(editedDescription || null);
+    if (success) {
+      setIsEditing(false);
+    }
+  }, [editedDescription, onSave]);
+
+  return (
+    <>
+      <SectionTitle align="center" justify="space-between">
+        <Typography.Text strong>{t('AI-Generated Description')}</Typography.Text>
+        {!isEditing && (
+          <Button
+            buttonSize="small"
+            buttonStyle="link"
+            onClick={handleStartEdit}
+            icon={<Icons.EditOutlined />}
+          >
+            {t('Edit')}
+          </Button>
+        )}
+      </SectionTitle>
+
+      {isEditing ? (
+        <>
+          <TextArea
+            value={editedDescription}
+            onChange={e => setEditedDescription(e.target.value)}
+            rows={4}
+            placeholder={placeholder || t('Enter a description...')}
+          />
+          <EditActions gap={8} justify="flex-end">
+            <Button
+              buttonSize="small"
+              buttonStyle="tertiary"
+              onClick={handleCancelEdit}
+              disabled={isUpdating}
+            >
+              {t('Cancel')}
+            </Button>
+            <Button
+              buttonSize="small"
+              buttonStyle="primary"
+              onClick={handleSaveDescription}
+              loading={isUpdating}
+            >
+              {t('Save')}
+            </Button>
+          </EditActions>
+        </>
+      ) : (
+        <DescriptionBox>
+          <Typography.Text>
+            {description || (
+              <Typography.Text type="secondary" italic>
+                {t('No description available')}
+              </Typography.Text>
+            )}
+          </Typography.Text>
+        </DescriptionBox>
+      )}
+    </>
+  );
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/ReviewSchemaPanel.tsx b/superset-frontend/src/pages/DatasourceConnector/components/ReviewSchemaPanel.tsx
index 421f2a2d56..3423e7cc4e 100644
--- a/superset-frontend/src/pages/DatasourceConnector/components/ReviewSchemaPanel.tsx
+++ b/superset-frontend/src/pages/DatasourceConnector/components/ReviewSchemaPanel.tsx
@@ -24,7 +24,7 @@ import { Flex, Icons, Typography } from '@superset-ui/core/components';
 interface ReviewSchemaPanelProps {
   databaseName: string | null;
   schemaName: string | null;
-  onAnalysisComplete: () => void;
+  onAnalysisComplete: (reportId: number) => void;
 }
 
 enum AnalysisStep {
@@ -241,8 +241,10 @@ export default function ReviewSchemaPanel({
             advanceStep(nextStep);
           } else {
             // Analysis complete - trigger callback after a short delay
+            // TODO: In real implementation, get the reportId from the analysis API
+            // For now, use a placeholder reportId of 1
             completionTimeoutId = setTimeout(() => {
-              onAnalysisComplete();
+              onAnalysisComplete(1);
             }, 1000);
           }
         }, stepDurations[step]);
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/SchemaDetailPanel.tsx b/superset-frontend/src/pages/DatasourceConnector/components/SchemaDetailPanel.tsx
new file mode 100644
index 0000000000..f41c948169
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/components/SchemaDetailPanel.tsx
@@ -0,0 +1,244 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useCallback } from 'react';
+import { t } from '@superset-ui/core';
+import { styled, useTheme } from '@apache-superset/core/ui';
+import { Flex, Icons, Tag, Typography } from '@superset-ui/core/components';
+import type { SchemaSelection } from '../types';
+import EditableDescription from './EditableDescription';
+
+interface SchemaDetailPanelProps {
+  selection: SchemaSelection;
+  onUpdateTableDescription: (
+    tableId: number,
+    description: string | null,
+  ) => Promise<boolean>;
+  onUpdateColumnDescription: (
+    columnId: number,
+    description: string | null,
+  ) => Promise<boolean>;
+  isUpdating: boolean;
+}
+
+const PanelContainer = styled.div`
+  ${({ theme }) => `
+    width: 100%;
+    background-color: ${theme.colorBgContainer};
+    border: 1px solid ${theme.colorBorder};
+    border-radius: ${theme.borderRadius}px;
+    overflow: hidden;
+  `}
+`;
+
+const HeaderContainer = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingSM}px ${theme.paddingMD}px;
+    border-bottom: 1px solid ${theme.colorBorder};
+    background-color: ${theme.colorBgLayout};
+  `}
+`;
+
+const ContentSection = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.paddingMD}px;
+  `}
+`;
+
+const ItemHeader = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingMD}px;
+    border-bottom: 1px solid ${theme.colorBorderSecondary};
+  `}
+`;
+
+const ConfidenceIndicator = styled(Flex)`
+  ${({ theme }) => `
+    margin-top: ${theme.marginMD}px;
+    padding: ${theme.paddingSM}px;
+    background-color: ${theme.colorSuccessBg};
+    border-radius: ${theme.borderRadiusSM}px;
+  `}
+`;
+
+const EmptyState = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingXL}px;
+    color: ${theme.colorTextSecondary};
+  `}
+`;
+
+const TypeLabel = styled(Typography.Text)`
+  ${({ theme }) => `
+    color: ${theme.colorPrimary};
+    font-weight: ${theme.fontWeightStrong};
+  `}
+`;
+
+export default function SchemaDetailPanel({
+  selection,
+  onUpdateTableDescription,
+  onUpdateColumnDescription,
+  isUpdating,
+}: SchemaDetailPanelProps) {
+  const theme = useTheme();
+
+  const handleSaveTableDescription = useCallback(
+    async (description: string | null): Promise<boolean> => {
+      if (selection?.type !== 'table') return false;
+      return onUpdateTableDescription(selection.table.id, description);
+    },
+    [selection, onUpdateTableDescription],
+  );
+
+  const handleSaveColumnDescription = useCallback(
+    async (description: string | null): Promise<boolean> => {
+      if (selection?.type !== 'column') return false;
+      return onUpdateColumnDescription(selection.column.id, description);
+    },
+    [selection, onUpdateColumnDescription],
+  );
+
+  // Empty state
+  if (!selection) {
+    return (
+      <PanelContainer>
+        <HeaderContainer align="center" gap={8}>
+          <Icons.InfoCircleOutlined
+            iconSize="s"
+            iconColor={theme.colorPrimary}
+          />
+          <Typography.Text strong>
+            {t('AI Schema Understanding')}
+          </Typography.Text>
+        </HeaderContainer>
+        <EmptyState vertical align="center" justify="center">
+          <Icons.TableOutlined
+            iconSize="xl"
+            iconColor={theme.colorTextSecondary}
+          />
+          <Typography.Text type="secondary" css={{ marginTop: theme.marginMD }}>
+            {t('Select a table or column from the schema tree to view details')}
+          </Typography.Text>
+        </EmptyState>
+      </PanelContainer>
+    );
+  }
+
+  // Column detail view
+  if (selection.type === 'column') {
+    const { column } = selection;
+
+    return (
+      <PanelContainer>
+        <HeaderContainer align="center" gap={8}>
+          <Icons.InfoCircleOutlined
+            iconSize="s"
+            iconColor={theme.colorPrimary}
+          />
+          <Typography.Text strong>
+            {t('AI Schema Understanding')}
+          </Typography.Text>
+        </HeaderContainer>
+
+        <ItemHeader vertical gap={8}>
+          <Flex align="center" gap={8}>
+            <Typography.Title level={5} css={{ margin: 0 }}>
+              {column.name}
+            </Typography.Title>
+            {column.is_primary_key && (
+              <Tag color="warning">{t('PRIMARY KEY')}</Tag>
+            )}
+            {column.is_foreign_key && (
+              <Tag color="processing">{t('FOREIGN KEY')}</Tag>
+            )}
+          </Flex>
+          <Flex align="center" gap={4}>
+            <Typography.Text type="secondary">{t('Type:')}</Typography.Text>
+            <TypeLabel>{column.type}</TypeLabel>
+          </Flex>
+        </ItemHeader>
+
+        <ContentSection>
+          <EditableDescription
+            key={`column-${column.id}`}
+            description={column.description}
+            placeholder={t('Enter a description for this column...')}
+            onSave={handleSaveColumnDescription}
+            isUpdating={isUpdating}
+          />
+
+          <ConfidenceIndicator align="center" gap={8}>
+            <Icons.CheckCircleOutlined
+              iconSize="s"
+              iconColor={theme.colorSuccess}
+            />
+            <Typography.Text css={{ color: theme.colorSuccess }}>
+              {t('AI Confidence: High')}
+            </Typography.Text>
+          </ConfidenceIndicator>
+        </ContentSection>
+      </PanelContainer>
+    );
+  }
+
+  // Table detail view
+  const { table } = selection;
+
+  return (
+    <PanelContainer>
+      <HeaderContainer align="center" gap={8}>
+        <Icons.InfoCircleOutlined iconSize="s" iconColor={theme.colorPrimary} />
+        <Typography.Text strong>{t('AI Schema Understanding')}</Typography.Text>
+      </HeaderContainer>
+
+      <ItemHeader align="center" gap={12}>
+        <Icons.CheckSquareOutlined
+          iconSize="m"
+          iconColor={theme.colorSuccess}
+        />
+        <Flex vertical gap={2}>
+          <Typography.Title level={5} css={{ margin: 0 }}>
+            {table.name}
+          </Typography.Title>
+          <Tag color="default">{table.type}</Tag>
+        </Flex>
+      </ItemHeader>
+
+      <ContentSection>
+        <EditableDescription
+          key={`table-${table.id}`}
+          description={table.description}
+          placeholder={t('Enter a description for this table...')}
+          onSave={handleSaveTableDescription}
+          isUpdating={isUpdating}
+        />
+
+        <ConfidenceIndicator align="center" gap={8}>
+          <Icons.CheckCircleOutlined
+            iconSize="s"
+            iconColor={theme.colorSuccess}
+          />
+          <Typography.Text css={{ color: theme.colorSuccess }}>
+            {t('AI Confidence: High')}
+          </Typography.Text>
+        </ConfidenceIndicator>
+      </ContentSection>
+    </PanelContainer>
+  );
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/components/SchemaTreeView.tsx b/superset-frontend/src/pages/DatasourceConnector/components/SchemaTreeView.tsx
new file mode 100644
index 0000000000..7e0dc3cf39
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/components/SchemaTreeView.tsx
@@ -0,0 +1,230 @@
+/**
+ * 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 { t } from '@superset-ui/core';
+import { styled, useTheme } from '@apache-superset/core/ui';
+import { Tree } from 'antd';
+import type { DataNode as TreeDataNode } from 'antd/es/tree';
+import { Flex, Icons, Typography } from '@superset-ui/core/components';
+import type { AnalyzedTable, SchemaSelection } from '../types';
+
+interface SchemaTreeViewProps {
+  tables: AnalyzedTable[];
+  selection: SchemaSelection;
+  onSelectionChange: (selection: SchemaSelection) => void;
+  schemaName: string | null;
+}
+
+const TreeContainer = styled.div`
+  ${({ theme }) => `
+    width: 100%;
+    background-color: ${theme.colorBgContainer};
+    border: 1px solid ${theme.colorBorder};
+    border-radius: ${theme.borderRadius}px;
+    overflow: hidden;
+
+    .ant-tree {
+      background: transparent;
+      padding: ${theme.paddingSM}px;
+    }
+
+    .ant-tree-treenode {
+      padding: ${theme.paddingXXS}px 0;
+    }
+
+    .ant-tree-node-content-wrapper {
+      display: flex;
+      align-items: center;
+      width: 100%;
+    }
+
+    .ant-tree-title {
+      display: flex;
+      align-items: center;
+      gap: ${theme.marginXS}px;
+      width: 100%;
+    }
+  `}
+`;
+
+const HeaderContainer = styled(Flex)`
+  ${({ theme }) => `
+    padding: ${theme.paddingSM}px ${theme.paddingMD}px;
+    border-bottom: 1px solid ${theme.colorBorder};
+    background-color: ${theme.colorBgLayout};
+  `}
+`;
+
+const TableIcon = styled.span`
+  ${({ theme }) => `
+    display: flex;
+    align-items: center;
+    color: ${theme.colorPrimary};
+  `}
+`;
+
+const KeyIcon = styled.span`
+  ${({ theme }) => `
+    display: flex;
+    align-items: center;
+    color: ${theme.colorError};
+  `}
+`;
+
+const ColumnIcon = styled.span`
+  ${({ theme }) => `
+    display: flex;
+    align-items: center;
+    color: ${theme.colorTextSecondary};
+  `}
+`;
+
+const ColumnType = styled(Typography.Text)`
+  ${({ theme }) => `
+    margin-left: auto;
+    padding-left: ${theme.paddingMD}px;
+    font-family: monospace;
+    font-size: ${theme.fontSizeSM}px;
+  `}
+`;
+
+const TreeNodeTitle = styled(Flex)`
+  width: 100%;
+`;
+
+export default function SchemaTreeView({
+  tables,
+  selection,
+  onSelectionChange,
+  schemaName,
+}: SchemaTreeViewProps) {
+  const theme = useTheme();
+
+  const treeData: TreeDataNode[] = useMemo(
+    () =>
+      tables.map(table => ({
+        key: `table-${table.id}`,
+        title: (
+          <TreeNodeTitle align="center" gap={4}>
+            <TableIcon>
+              <Icons.TableOutlined iconSize="s" />
+            </TableIcon>
+            <Typography.Text strong>{table.name}</Typography.Text>
+          </TreeNodeTitle>
+        ),
+        children: table.columns.map(column => ({
+          key: `column-${table.id}-${column.id}`,
+          title: (
+            <TreeNodeTitle align="center" justify="space-between">
+              <Flex align="center" gap={4}>
+                {column.is_primary_key || column.is_foreign_key ? (
+                  <KeyIcon>
+                    <Icons.KeyOutlined iconSize="s" />
+                  </KeyIcon>
+                ) : (
+                  <ColumnIcon>
+                    <Icons.FieldNumberOutlined iconSize="s" />
+                  </ColumnIcon>
+                )}
+                <Typography.Text>{column.name}</Typography.Text>
+              </Flex>
+              <ColumnType type="secondary">({column.type})</ColumnType>
+            </TreeNodeTitle>
+          ),
+          isLeaf: true,
+        })),
+      })),
+    [tables],
+  );
+
+  const handleSelect = (
+    _selectedKeys: React.Key[],
+    info: { node: TreeDataNode },
+  ) => {
+    const key = String(info.node.key);
+
+    if (key.startsWith('table-')) {
+      const tableId = parseInt(key.replace('table-', ''), 10);
+      const table = tables.find(t => t.id === tableId);
+      if (table) {
+        onSelectionChange({ type: 'table', table });
+      }
+    } else if (key.startsWith('column-')) {
+      // Format: column-{tableId}-{columnId}
+      const parts = key.split('-');
+      const tableId = parseInt(parts[1], 10);
+      const columnId = parseInt(parts[2], 10);
+      const table = tables.find(t => t.id === tableId);
+      const column = table?.columns.find(c => c.id === columnId);
+      if (table && column) {
+        onSelectionChange({ type: 'column', column, table });
+      }
+    }
+  };
+
+  // Compute selected key from selection
+  const selectedKeys = useMemo(() => {
+    if (!selection) return [];
+    if (selection.type === 'table') {
+      return [`table-${selection.table.id}`];
+    }
+    return [`column-${selection.table.id}-${selection.column.id}`];
+  }, [selection]);
+
+  const defaultExpandedKeys = tables.map(table => `table-${table.id}`);
+
+  return (
+    <TreeContainer>
+      <HeaderContainer align="center" gap={8}>
+        <Icons.DatabaseOutlined iconSize="s" iconColor={theme.colorPrimary} />
+        <Typography.Text strong>{t('Database Schema')}</Typography.Text>
+      </HeaderContainer>
+      {schemaName && (
+        <Flex
+          align="center"
+          gap={4}
+          css={{
+            padding: `${theme.paddingXS}px ${theme.paddingMD}px`,
+            borderBottom: `1px solid ${theme.colorBorderSecondary}`,
+          }}
+        >
+          <Typography.Text type="secondary">
+            {t('Connected to:')}
+          </Typography.Text>
+          <Typography.Text
+            css={{
+              color: theme.colorPrimary,
+              fontWeight: theme.fontWeightStrong,
+            }}
+          >
+            {schemaName}
+          </Typography.Text>
+        </Flex>
+      )}
+      <Tree
+        treeData={treeData}
+        selectedKeys={selectedKeys}
+        defaultExpandedKeys={defaultExpandedKeys}
+        onSelect={handleSelect}
+        showIcon={false}
+        blockNode
+      />
+    </TreeContainer>
+  );
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/types.ts b/superset-frontend/src/pages/DatasourceConnector/config.ts
similarity index 61%
copy from superset-frontend/src/pages/DatasourceConnector/types.ts
copy to superset-frontend/src/pages/DatasourceConnector/config.ts
index e354da0643..ac38ad4ae9 100644
--- a/superset-frontend/src/pages/DatasourceConnector/types.ts
+++ b/superset-frontend/src/pages/DatasourceConnector/config.ts
@@ -17,28 +17,8 @@
  * under the License.
  */
 
-export interface DatasourceConnectorState {
-  databaseId: number | null;
-  databaseName: string | null;
-  catalogName: string | null;
-  schemaName: string | null;
-  isSubmitting: boolean;
-}
-
-export interface DatasourceAnalyzerPostPayload {
-  database_id: number;
-  schema_name: string;
-  catalog_name?: string | null;
-}
-
-export interface DatasourceAnalyzerResponse {
-  result: {
-    run_id: string;
-  };
-}
-
-export enum ConnectorStep {
-  CONNECT_DATA_SOURCE = 0,
-  REVIEW_SCHEMA = 1,
-  GENERATE_DASHBOARD = 2,
-}
+/**
+ * Set to true to use mock data for UI testing without backend.
+ * Set to false to use real API endpoints.
+ */
+export const USE_MOCK_DATA = true;
diff --git a/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaEditorMutations.ts b/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaEditorMutations.ts
new file mode 100644
index 0000000000..61e34e8bfc
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaEditorMutations.ts
@@ -0,0 +1,162 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useCallback, useState } from 'react';
+import { SupersetClient, logging } from '@superset-ui/core';
+import type { GenerateDashboardResponse } from '../types';
+import { USE_MOCK_DATA } from '../config';
+
+interface MutationState {
+  loading: boolean;
+  error: string | null;
+}
+
+interface UseSchemaEditorMutationsReturn {
+  updateTableDescription: (
+    tableId: number,
+    description: string | null,
+  ) => Promise<boolean>;
+  updateColumnDescription: (
+    columnId: number,
+    description: string | null,
+  ) => Promise<boolean>;
+  generateDashboard: (
+    reportId: number,
+    dashboardId: number,
+  ) => Promise<string | null>;
+  mutationState: MutationState;
+}
+
+export default function useSchemaEditorMutations(): UseSchemaEditorMutationsReturn {
+  const [mutationState, setMutationState] = useState<MutationState>({
+    loading: false,
+    error: null,
+  });
+
+  const updateTableDescription = useCallback(
+    async (tableId: number, description: string | null): Promise<boolean> => {
+      setMutationState({ loading: true, error: null });
+
+      // Use mock for testing
+      if (USE_MOCK_DATA) {
+        await new Promise(resolve => setTimeout(resolve, 300));
+        logging.info(
+          `Mock: Updated table ${tableId} description to:`,
+          description,
+        );
+        setMutationState({ loading: false, error: null });
+        return true;
+      }
+
+      try {
+        await SupersetClient.put({
+          endpoint: `/api/v1/datasource_analyzer/table/${tableId}`,
+          jsonPayload: { description },
+        });
+        setMutationState({ loading: false, error: null });
+        return true;
+      } catch (err) {
+        logging.error('Error updating table description:', err);
+        const errorMessage =
+          err instanceof Error
+            ? err.message
+            : 'Failed to update table description';
+        setMutationState({ loading: false, error: errorMessage });
+        return false;
+      }
+    },
+    [],
+  );
+
+  const updateColumnDescription = useCallback(
+    async (columnId: number, description: string | null): Promise<boolean> => {
+      setMutationState({ loading: true, error: null });
+
+      // Use mock for testing
+      if (USE_MOCK_DATA) {
+        await new Promise(resolve => setTimeout(resolve, 300));
+        logging.info(
+          `Mock: Updated column ${columnId} description to:`,
+          description,
+        );
+        setMutationState({ loading: false, error: null });
+        return true;
+      }
+
+      try {
+        await SupersetClient.put({
+          endpoint: `/api/v1/datasource_analyzer/column/${columnId}`,
+          jsonPayload: { description },
+        });
+        setMutationState({ loading: false, error: null });
+        return true;
+      } catch (err) {
+        logging.error('Error updating column description:', err);
+        const errorMessage =
+          err instanceof Error
+            ? err.message
+            : 'Failed to update column description';
+        setMutationState({ loading: false, error: errorMessage });
+        return false;
+      }
+    },
+    [],
+  );
+
+  const generateDashboard = useCallback(
+    async (reportId: number, dashboardId: number): Promise<string | null> => {
+      setMutationState({ loading: true, error: null });
+
+      // Use mock for testing
+      if (USE_MOCK_DATA) {
+        await new Promise(resolve => setTimeout(resolve, 500));
+        const mockRunId = `mock-run-${Date.now()}`;
+        logging.info(`Mock: Generated dashboard with run_id: ${mockRunId}`);
+        setMutationState({ loading: false, error: null });
+        return mockRunId;
+      }
+
+      try {
+        const response = await SupersetClient.post({
+          endpoint: '/api/v1/datasource_analyzer/generate',
+          jsonPayload: {
+            report_id: reportId,
+            dashboard_id: dashboardId,
+          },
+        });
+        const data = response.json as GenerateDashboardResponse;
+        setMutationState({ loading: false, error: null });
+        return data.result.run_id;
+      } catch (err) {
+        logging.error('Error generating dashboard:', err);
+        const errorMessage =
+          err instanceof Error ? err.message : 'Failed to generate dashboard';
+        setMutationState({ loading: false, error: errorMessage });
+        return null;
+      }
+    },
+    [],
+  );
+
+  return {
+    updateTableDescription,
+    updateColumnDescription,
+    generateDashboard,
+    mutationState,
+  };
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaReport.ts b/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaReport.ts
new file mode 100644
index 0000000000..049ac2e351
--- /dev/null
+++ b/superset-frontend/src/pages/DatasourceConnector/hooks/useSchemaReport.ts
@@ -0,0 +1,223 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useCallback } from 'react';
+import { SupersetClient, logging } from '@superset-ui/core';
+import type { SchemaReportResponse, DatabaseSchemaReport } from '../types';
+import { USE_MOCK_DATA } from '../config';
+
+// Mock data for testing UI without backend
+const MOCK_REPORT: DatabaseSchemaReport = {
+  id: 1,
+  database_id: 1,
+  schema_name: 'postgres-prod',
+  status: 'completed',
+  created_at: new Date().toISOString(),
+  tables: [
+    {
+      id: 1,
+      name: 'orders',
+      type: 'table',
+      description:
+        'This table stores all customer orders including order details, timestamps, and status information.',
+      columns: [
+        {
+          id: 1,
+          name: 'order_id',
+          type: 'INTEGER',
+          position: 1,
+          description: 'Unique identifier for each order',
+          is_primary_key: true,
+        },
+        {
+          id: 2,
+          name: 'customer_id',
+          type: 'INTEGER',
+          position: 2,
+          description: 'Reference to the customer who placed the order',
+          is_foreign_key: true,
+        },
+        {
+          id: 3,
+          name: 'order_date',
+          type: 'TIMESTAMP',
+          position: 3,
+          description: null,
+        },
+        {
+          id: 4,
+          name: 'total_amount',
+          type: 'DECIMAL',
+          position: 4,
+          description: null,
+        },
+        {
+          id: 5,
+          name: 'status',
+          type: 'VARCHAR',
+          position: 5,
+          description: null,
+        },
+      ],
+    },
+    {
+      id: 2,
+      name: 'customers',
+      type: 'table',
+      description:
+        'Customer master data including contact information and account details.',
+      columns: [
+        {
+          id: 6,
+          name: 'customer_id',
+          type: 'INTEGER',
+          position: 1,
+          description: 'Unique identifier for each customer',
+          is_primary_key: true,
+        },
+        {
+          id: 7,
+          name: 'email',
+          type: 'VARCHAR',
+          position: 2,
+          description: null,
+        },
+        {
+          id: 8,
+          name: 'created_at',
+          type: 'TIMESTAMP',
+          position: 3,
+          description: null,
+        },
+        {
+          id: 9,
+          name: 'country',
+          type: 'VARCHAR',
+          position: 4,
+          description: null,
+        },
+      ],
+    },
+    {
+      id: 3,
+      name: 'products',
+      type: 'table',
+      description: 'Product catalog containing all available items for sale.',
+      columns: [
+        {
+          id: 10,
+          name: 'product_id',
+          type: 'INTEGER',
+          position: 1,
+          description: 'Unique identifier for each product',
+          is_primary_key: true,
+        },
+        {
+          id: 11,
+          name: 'name',
+          type: 'VARCHAR',
+          position: 2,
+          description: null,
+        },
+        {
+          id: 12,
+          name: 'price',
+          type: 'DECIMAL',
+          position: 3,
+          description: null,
+        },
+        {
+          id: 13,
+          name: 'category',
+          type: 'VARCHAR',
+          position: 4,
+          description: null,
+        },
+      ],
+    },
+  ],
+};
+
+interface UseSchemaReportReturn {
+  report: DatabaseSchemaReport | null;
+  loading: boolean;
+  error: string | null;
+  refetch: () => void;
+}
+
+export default function useSchemaReport(
+  reportId: number | null,
+): UseSchemaReportReturn {
+  const [report, setReport] = useState<DatabaseSchemaReport | null>(null);
+  const [loading, setLoading] = useState<boolean>(false);
+  const [error, setError] = useState<string | null>(null);
+
+  const fetchReport = useCallback(async () => {
+    if (!reportId) {
+      setReport(null);
+      return;
+    }
+
+    // Use mock data for testing
+    if (USE_MOCK_DATA) {
+      setLoading(true);
+      // Simulate network delay
+      await new Promise(resolve => setTimeout(resolve, 500));
+      setReport(MOCK_REPORT);
+      setLoading(false);
+      return;
+    }
+
+    setLoading(true);
+    setError(null);
+
+    try {
+      const response = await SupersetClient.get({
+        endpoint: `/api/v1/datasource_analyzer/report/${reportId}`,
+      });
+
+      const data = response.json as SchemaReportResponse;
+      setReport({
+        id: data.id,
+        database_id: data.database_id,
+        schema_name: data.schema_name,
+        status: data.status,
+        created_at: data.created_at,
+        tables: data.tables,
+      });
+    } catch (err) {
+      logging.error('Error fetching schema report:', err);
+      setError(
+        err instanceof Error ? err.message : 'Failed to fetch schema report',
+      );
+    } finally {
+      setLoading(false);
+    }
+  }, [reportId]);
+
+  useEffect(() => {
+    fetchReport();
+  }, [fetchReport]);
+
+  return {
+    report,
+    loading,
+    error,
+    refetch: fetchReport,
+  };
+}
diff --git a/superset-frontend/src/pages/DatasourceConnector/index.tsx b/superset-frontend/src/pages/DatasourceConnector/index.tsx
index dad949690f..638e7615e8 100644
--- a/superset-frontend/src/pages/DatasourceConnector/index.tsx
+++ b/superset-frontend/src/pages/DatasourceConnector/index.tsx
@@ -25,6 +25,7 @@ import type { DatabaseObject } from 'src/components/DatabaseSelector/types';
 import DatabaseModal from 'src/features/databases/DatabaseModal';
 import ConnectorLayout from './components/ConnectorLayout';
 import DataSourcePanel from './components/DataSourcePanel';
+import DatasourceEditorPanel from './components/DatasourceEditorPanel';
 import ReviewSchemaPanel from './components/ReviewSchemaPanel';
 import useDatabaseListRefresh from './hooks/useDatabaseListRefresh';
 import { ConnectorStep, DatasourceConnectorState } from './types';
@@ -57,6 +58,7 @@ export default function DatasourceConnector() {
   );
   const [templateInfo, setTemplateInfo] =
     useState<TemplateDashboardInfo | null>(null);
+  const [databaseReportId, setDatabaseReportId] = useState<number | null>(null);
 
   // Get dashboard_id from query params
   const dashboardId = useMemo(() => {
@@ -154,7 +156,7 @@ export default function DatasourceConnector() {
   );
 
   const handleCancel = useCallback(() => {
-    history.push('/dashboard/templates/');
+    history.goBack();
   }, [history]);
 
   const handleContinueToReview = useCallback(async () => {
@@ -176,13 +178,30 @@ export default function DatasourceConnector() {
   }, [state, addDangerToast, addSuccessToast]);
 
   //const handleBackToConnect = useCallback(() => {
-  //   setCurrentStep(ConnectorStep.CONNECT_DATA_SOURCE);
+  //  setCurrentStep(ConnectorStep.CONNECT_DATA_SOURCE);
   //}, []);
 
-  const handleContinueToGenerate = useCallback(() => {
-    addSuccessToast(t('Moving to Generate Dashboard step'));
-    setCurrentStep(ConnectorStep.GENERATE_DASHBOARD);
-  }, [addSuccessToast]);
+  const handleAnalysisComplete = useCallback(
+    (reportId: number) => {
+      setDatabaseReportId(reportId);
+      addSuccessToast(t('Analysis complete! Review and edit your schema.'));
+      setCurrentStep(ConnectorStep.EDIT_SCHEMA);
+    },
+    [addSuccessToast],
+  );
+
+  const handleBackToReview = useCallback(() => {
+    setCurrentStep(ConnectorStep.REVIEW_SCHEMA);
+  }, []);
+
+  const handleConfirmAndGenerate = useCallback(
+    (runId: string) => {
+      addSuccessToast(t('Dashboard generation started'));
+      // Navigate to the loading screen with the run_id
+      history.push(`/datasource-connector/loading/${runId}`);
+    },
+    [addSuccessToast, history],
+  );
 
   const renderCurrentStep = () => {
     switch (currentStep) {
@@ -208,12 +227,27 @@ export default function DatasourceConnector() {
           <ReviewSchemaPanel
             databaseName={state.databaseName}
             schemaName={state.schemaName}
-            onAnalysisComplete={handleContinueToGenerate}
+            onAnalysisComplete={handleAnalysisComplete}
+          />
+        );
+      case ConnectorStep.EDIT_SCHEMA:
+        return databaseReportId ? (
+          <DatasourceEditorPanel
+            reportId={databaseReportId}
+            dashboardId={dashboardId}
+            onBack={handleBackToReview}
+            onConfirm={handleConfirmAndGenerate}
           />
+        ) : (
+          <Flex vertical align="center" css={{ padding: 40 }}>
+            <Typography.Text type="danger">
+              {t('No report ID available. Please restart the process.')}
+            </Typography.Text>
+          </Flex>
         );
       case ConnectorStep.GENERATE_DASHBOARD:
         return (
-          <Flex vertical align="center" style={{ padding: '40px' }}>
+          <Flex vertical align="center" css={{ padding: 40 }}>
             <Typography.Title level={3}>
               {t('Generate Dashboard')}
             </Typography.Title>
diff --git a/superset-frontend/src/pages/DatasourceConnector/types.ts b/superset-frontend/src/pages/DatasourceConnector/types.ts
index e354da0643..fcaa7cdd26 100644
--- a/superset-frontend/src/pages/DatasourceConnector/types.ts
+++ b/superset-frontend/src/pages/DatasourceConnector/types.ts
@@ -40,5 +40,61 @@ export interface DatasourceAnalyzerResponse {
 export enum ConnectorStep {
   CONNECT_DATA_SOURCE = 0,
   REVIEW_SCHEMA = 1,
-  GENERATE_DASHBOARD = 2,
+  EDIT_SCHEMA = 2,
+  GENERATE_DASHBOARD = 3,
+}
+
+// Schema Editor Types
+export interface AnalyzedColumn {
+  id: number;
+  name: string;
+  type: string;
+  position: number;
+  description: string | null;
+  is_primary_key?: boolean;
+  is_foreign_key?: boolean;
+}
+
+export interface AnalyzedTable {
+  id: number;
+  name: string;
+  type: 'table' | 'view' | 'materialized_view';
+  description: string | null;
+  columns: AnalyzedColumn[];
+}
+
+// Selection types for the detail panel
+export type SchemaSelection =
+  | { type: 'table'; table: AnalyzedTable }
+  | { type: 'column'; column: AnalyzedColumn; table: AnalyzedTable }
+  | null;
+
+export interface DatabaseSchemaReport {
+  id: number;
+  database_id: number;
+  schema_name: string;
+  status: string;
+  created_at: string | null;
+  tables: AnalyzedTable[];
+}
+
+export interface SchemaReportResponse {
+  id: number;
+  database_id: number;
+  schema_name: string;
+  status: string;
+  created_at: string | null;
+  tables: AnalyzedTable[];
+  joins: unknown[];
+}
+
+export interface GenerateDashboardPayload {
+  report_id: number;
+  dashboard_id: number;
+}
+
+export interface GenerateDashboardResponse {
+  result: {
+    run_id: string;
+  };
 }
diff --git a/superset/commands/database_analyzer/analyze.py b/superset/commands/database_analyzer/analyze.py
index 67f99eed84..1af5e02d76 100644
--- a/superset/commands/database_analyzer/analyze.py
+++ b/superset/commands/database_analyzer/analyze.py
@@ -20,7 +20,7 @@ import logging
 from concurrent.futures import as_completed, ThreadPoolExecutor
 from typing import Any
 
-from flask import current_app
+from flask import current_app, Flask
 from sqlalchemy import inspect, MetaData, text
 
 from superset import db
@@ -30,10 +30,8 @@ from superset.models.core import Database
 from superset.models.database_analyzer import (
     AnalyzedColumn,
     AnalyzedTable,
-    Cardinality,
     DatabaseSchemaReport,
     InferredJoin,
-    JoinType,
     TableType,
 )
 from superset.utils import json
@@ -178,17 +176,25 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
                 result = engine.execute(text(sample_sql))
                 for row in result:
                     sample_rows.append(dict(row))
-                logger.debug("Fetched %d sample rows from %s", len(sample_rows), table_name)
-            except Exception as e:
+                logger.debug(
+                    "Fetched %d sample rows from %s", len(sample_rows), table_name
+                )
+            except Exception:
                 # Fallback to regular LIMIT if RANDOM() not supported
                 try:
                     fallback_sql = f'SELECT * FROM "{schema}"."{table_name}" LIMIT 3'  # noqa: S608, E501
                     result = engine.execute(text(fallback_sql))
                     for row in result:
                         sample_rows.append(dict(row))
-                    logger.debug("Fetched %d sample rows from %s (fallback)", len(sample_rows), table_name)
+                    logger.debug(
+                        "Fetched %d sample rows from %s (fallback)",
+                        len(sample_rows),
+                        table_name,
+                    )
                 except Exception as e2:
-                    logger.warning("Could not fetch sample data for %s: %s", table_name, str(e2))
+                    logger.warning(
+                        "Could not fetch sample data for %s: %s", table_name, str(e2)
+                    )
 
         # Get row count (try reltuples first, fallback to actual count)
         row_count = None
@@ -201,7 +207,7 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
             result = engine.execute(text(count_sql))
             row = result.fetchone()
             row_count = row[0] if row and row[0] >= 0 else None
-            
+
             # If reltuples is -1 or None, get actual count for small tables
             if row_count is None or row_count < 0:
                 actual_count_sql = f'SELECT COUNT(*) FROM "{schema}"."{table_name}"'  # noqa: S608
@@ -266,12 +272,12 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
                     column_name=col_data["name"],
                     data_type=col_data["type"],
                     ordinal_position=col_data["position"],
+                    is_primary_key=col_data["is_primary_key"],
+                    is_foreign_key=col_data["is_foreign_key"],
                     db_comment=col_data["comment"],
                     extra_json=json.dumps(
                         {
                             "is_nullable": col_data["nullable"],
-                            "is_primary_key": col_data["is_primary_key"],
-                            "is_foreign_key": col_data["is_foreign_key"],
                         }
                     ),
                 )
@@ -296,7 +302,7 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
             return
 
         max_workers = min(10, len(tables))
-        
+
         # Capture the current Flask app context
         app = current_app._get_current_object()
 
@@ -317,7 +323,7 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
                         str(e),
                     )
 
-    def _augment_table_with_ai_context(self, app, table: AnalyzedTable) -> None:
+    def _augment_table_with_ai_context(self, app: Flask, table: AnalyzedTable) -> None:
         """Wrapper to provide Flask context to the AI description thread"""
         with app.app_context():
             self._augment_table_with_ai(table)
@@ -458,10 +464,10 @@ class AnalyzeDatabaseSchemaCommand(BaseCommand):
         # Debug logging to see actual data being stored
         for i, join_data in enumerate(inferred_joins):
             logger.debug(
-                "Join %d data: join_type=%s, cardinality=%s", 
-                i, 
-                join_data.get("join_type"), 
-                join_data.get("cardinality")
+                "Join %d data: join_type=%s, cardinality=%s",
+                i,
+                join_data.get("join_type"),
+                join_data.get("cardinality"),
             )
 
         for join_data in inferred_joins:
diff --git a/superset/databases/analyzer_api.py b/superset/databases/analyzer_api.py
index 75e04d2fd2..8e3ee799ae 100644
--- a/superset/databases/analyzer_api.py
+++ b/superset/databases/analyzer_api.py
@@ -25,7 +25,11 @@ from marshmallow import fields, Schema, ValidationError
 
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
 from superset.extensions import db, event_logger
-from superset.models.database_analyzer import DatabaseSchemaReport
+from superset.models.database_analyzer import (
+    AnalyzedColumn,
+    AnalyzedTable,
+    DatabaseSchemaReport,
+)
 from superset.tasks.database_analyzer import (
     check_analysis_status,
     kickstart_analysis,
@@ -78,6 +82,48 @@ class CheckStatusResponseSchema(Schema):
     joins_count = fields.Integer(allow_none=True)
 
 
+class TableDescriptionPutSchema(Schema):
+    """Schema for updating table description"""
+
+    description = fields.String(
+        required=True,
+        allow_none=True,
+        metadata={"description": "The AI-generated description for the table"},
+    )
+
+
+class ColumnDescriptionPutSchema(Schema):
+    """Schema for updating column description"""
+
+    description = fields.String(
+        required=True,
+        allow_none=True,
+        metadata={"description": "The AI-generated description for the column"},
+    )
+
+
+class GenerateDashboardPostSchema(Schema):
+    """Schema for triggering dashboard generation"""
+
+    report_id = fields.Integer(
+        required=True,
+        metadata={"description": "The database schema report ID"},
+    )
+    dashboard_id = fields.Integer(
+        required=True,
+        metadata={"description": "The dashboard template ID to use for generation"},
+    )
+
+
+class GenerateDashboardResponseSchema(Schema):
+    """Schema for dashboard generation response"""
+
+    run_id = fields.String(
+        required=True,
+        metadata={"description": "The unique identifier for this generation run"},
+    )
+
+
 class DatasourceAnalyzerRestApi(BaseSupersetApi):
     """API endpoints for database schema analyzer"""
 
@@ -279,6 +325,8 @@ class DatasourceAnalyzerRestApi(BaseSupersetApi):
                             "type": column.data_type,
                             "position": column.ordinal_position,
                             "description": column.ai_description or column.db_comment,
+                            "is_primary_key": column.is_primary_key,
+                            "is_foreign_key": column.is_foreign_key,
                         }
                     )
 
@@ -304,3 +352,209 @@ class DatasourceAnalyzerRestApi(BaseSupersetApi):
         except Exception as e:
             logger.exception("Error retrieving report")
             return self.response_500(message=str(e))
+
+    @expose("/table/<int:table_id>", methods=("PUT",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @requires_json
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.update_table",
+        log_to_statsd=True,
+    )
+    def update_table(self, table_id: int) -> Response:
+        """
+        Update table description.
+        ---
+        put:
+          summary: Update table AI description
+          description: >-
+            Updates the AI-generated description for an analyzed table
+          parameters:
+          - in: path
+            name: table_id
+            required: true
+            schema:
+              type: integer
+            description: The table ID
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/TableDescriptionPutSchema'
+          responses:
+            200:
+              description: Table description updated successfully
+            400:
+              $ref: '#/components/responses/400'
+            404:
+              description: Table not found
+            500:
+              description: Internal server error
+        """
+        try:
+            schema = TableDescriptionPutSchema()
+            data = schema.load(request.json)
+
+            table = db.session.query(AnalyzedTable).get(table_id)
+            if not table:
+                return self.response_404(message="Table not found")
+
+            table.ai_description = data["description"]
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            return self.response(
+                200,
+                id=table.id,
+                name=table.table_name,
+                description=table.ai_description,
+            )
+
+        except ValidationError as error:
+            return self.response_400(message=str(error.messages))
+        except Exception as e:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            logger.exception("Error updating table description")
+            return self.response_500(message=str(e))
+
+    @expose("/column/<int:column_id>", methods=("PUT",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @requires_json
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.update_column",
+        log_to_statsd=True,
+    )
+    def update_column(self, column_id: int) -> Response:
+        """
+        Update column description.
+        ---
+        put:
+          summary: Update column AI description
+          description: >-
+            Updates the AI-generated description for an analyzed column
+          parameters:
+          - in: path
+            name: column_id
+            required: true
+            schema:
+              type: integer
+            description: The column ID
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/ColumnDescriptionPutSchema'
+          responses:
+            200:
+              description: Column description updated successfully
+            400:
+              $ref: '#/components/responses/400'
+            404:
+              description: Column not found
+            500:
+              description: Internal server error
+        """
+        try:
+            schema = ColumnDescriptionPutSchema()
+            data = schema.load(request.json)
+
+            column = db.session.query(AnalyzedColumn).get(column_id)
+            if not column:
+                return self.response_404(message="Column not found")
+
+            column.ai_description = data["description"]
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            return self.response(
+                200,
+                id=column.id,
+                name=column.column_name,
+                description=column.ai_description,
+            )
+
+        except ValidationError as error:
+            return self.response_400(message=str(error.messages))
+        except Exception as e:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            logger.exception("Error updating column description")
+            return self.response_500(message=str(e))
+
+    @expose("/generate", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @requires_json
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.generate",
+        log_to_statsd=True,
+    )
+    def generate_dashboard(self) -> Response:
+        """
+        Trigger dashboard generation from schema report.
+        ---
+        post:
+          summary: Generate dashboard from analyzed schema
+          description: >-
+            Triggers the dashboard generation Celery job using the analyzed
+            schema report and a dashboard template. Returns a run_id for
+            tracking the generation progress.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/GenerateDashboardPostSchema'
+          responses:
+            200:
+              description: Dashboard generation initiated successfully
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/GenerateDashboardResponseSchema'
+            400:
+              $ref: '#/components/responses/400'
+            404:
+              description: Report or dashboard not found
+            500:
+              description: Internal server error
+        """
+        try:
+            schema = GenerateDashboardPostSchema()
+            data = schema.load(request.json)
+
+            report_id = data["report_id"]
+            dashboard_id = data["dashboard_id"]
+
+            # Verify report exists
+            report = db.session.query(DatabaseSchemaReport).get(report_id)
+            if not report:
+                return self.response_404(message="Report not found")
+
+            # TODO: Integrate with Dashboard Generation Celery Job
+            # For now, return a placeholder run_id
+            # The actual implementation will call the Celery task and return
+            # its task ID
+            import uuid
+
+            placeholder_run_id = str(uuid.uuid4())
+
+            logger.info(
+                "Dashboard generation requested for report_id=%s, dashboard_id=%s",
+                report_id,
+                dashboard_id,
+            )
+
+            return self.response(200, result={"run_id": placeholder_run_id})
+
+        except ValidationError as error:
+            return self.response_400(message=str(error.messages))
+        except Exception as e:
+            logger.exception("Error initiating dashboard generation")
+            return self.response_500(message=str(e))
diff --git a/superset/models/database_analyzer.py b/superset/models/database_analyzer.py
index a3a1df6724..54fe3f6e03 100644
--- a/superset/models/database_analyzer.py
+++ b/superset/models/database_analyzer.py
@@ -148,6 +148,8 @@ class AnalyzedColumn(Model, AuditMixinNullable, UUIDMixin):
     column_name = sa.Column(sa.String(256), nullable=False)
     data_type = sa.Column(sa.String(256), nullable=False)
     ordinal_position = sa.Column(sa.Integer, nullable=False)
+    is_primary_key = sa.Column(sa.Boolean, default=False, nullable=False)
+    is_foreign_key = sa.Column(sa.Boolean, default=False, nullable=False)
     db_comment = sa.Column(sa.Text, nullable=True)
     ai_description = sa.Column(sa.Text, nullable=True)
     extra_json = sa.Column(sa.Text, nullable=True)
diff --git a/superset/views/datasource_connector.py b/superset/views/datasource_connector.py
index 4956d817ff..d51fc54f1e 100644
--- a/superset/views/datasource_connector.py
+++ b/superset/views/datasource_connector.py
@@ -16,12 +16,9 @@
 # under the License.
 """Views for the Datasource Connector feature"""
 
-from flask import redirect, request
 from flask_appbuilder import expose
 from flask_appbuilder.security.decorators import has_access, permission_name
 
-from superset import db
-from superset.models.dashboard import Dashboard
 from superset.superset_typing import FlaskResponse
 from superset.views.base import BaseSupersetView
 
@@ -30,33 +27,10 @@ class DatasourceConnectorView(BaseSupersetView):
     route_base = "/datasource-connector"
     class_permission_name = "Dashboard"
 
-    def _is_valid_template_dashboard(self, dashboard_id: str | None) -> bool:
-        """Check if the dashboard_id is valid and refers to a template dashboard."""
-        if not dashboard_id:
-            return False
-
-        try:
-            dashboard_id_int = int(dashboard_id)
-        except (ValueError, TypeError):
-            return False
-
-        dashboard = db.session.query(Dashboard).filter_by(id=dashboard_id_int).first()
-        if not dashboard:
-            return False
-
-        # Check if the dashboard is a template (is_template in json_metadata)
-        metadata = dashboard.params_dict
-        return metadata.get("is_template", False)
-
     @expose("/")
     @has_access
     @permission_name("read")
     def root(self) -> FlaskResponse:
-        dashboard_id = request.args.get("dashboard_id")
-
-        if not self._is_valid_template_dashboard(dashboard_id):
-            return redirect("/dashboard/templates/")
-
         return super().render_app_template()
 
     @expose("/loading/<run_id>")