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

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

commit 92bf3b9d4e112ec2e69c587d49b867197ef551dc
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Thu Jul 17 13:43:38 2025 -0700

    touchups
---
 docs/scripts/export_config_metadata.py             | 105 ++++++
 docs/scripts/update_docs.sh                        |  40 +++
 docs/src/components/ConfigurationTable.tsx         | 331 ++++++++++++++++++
 .../src/components/EnvironmentVariablesExample.tsx | 181 ++++++++++
 docs/src/resources/config_metadata.json            | 225 ++++++++++++
 superset/commands/settings/__init__.py             |  16 +
 superset/commands/settings/exceptions.py           |  47 +++
 superset/initialization/__init__.py                |   2 +
 superset/settings/__init__.py                      |  16 +
 superset/settings/api.py                           | 379 +++++++++++++++++++++
 superset/settings/schemas.py                       | 110 ++++++
 11 files changed, 1452 insertions(+)

diff --git a/docs/scripts/export_config_metadata.py 
b/docs/scripts/export_config_metadata.py
new file mode 100644
index 0000000000..15f93dda6a
--- /dev/null
+++ b/docs/scripts/export_config_metadata.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""
+Export configuration metadata to JSON for documentation generation.
+
+This script extracts configuration metadata from SupersetConfig and generates
+JSON files that can be imported into the documentation site.
+"""
+
+import sys
+from pathlib import Path
+from typing import Any, Dict, List
+
+# Add the superset directory to Python path
+superset_root = Path(__file__).parent.parent.parent
+sys.path.insert(0, str(superset_root))
+
+from superset.config_extensions import SupersetConfig  # noqa: E402
+from superset.utils import json  # noqa: E402
+
+
+def export_config_metadata() -> List[Dict[str, Any]]:
+    """Export configuration metadata as JSON."""
+    config = SupersetConfig()
+
+    # Get all settings metadata
+    settings_metadata = config.DATABASE_SETTINGS_SCHEMA
+
+    # Transform metadata for documentation
+    docs_metadata = []
+
+    for key, schema in settings_metadata.items():
+        # Skip readonly settings for user documentation
+        if schema.get("readonly", False):
+            continue
+
+        # Build environment variable name
+        env_var = f"SUPERSET__{key}"
+
+        # Extract nested example if available
+        nested_example = None
+        if schema.get("type") == "object" and "example" in schema:
+            nested_example = f"SUPERSET__{key}__example__nested_key=value"
+
+        # Format type information
+        type_info = str(schema.get("type", "unknown"))
+        if type_info == "integer":
+            min_val = schema.get("minimum")
+            max_val = schema.get("maximum")
+            if min_val is not None or max_val is not None:
+                min_str = str(min_val) if min_val is not None else "N/A"
+                max_str = str(max_val) if max_val is not None else "N/A"
+                type_info += f" ({min_str} - {max_str})"
+
+        doc_entry = {
+            "key": key,
+            "title": schema.get("title", key),
+            "description": schema.get("description", ""),
+            "type": type_info,
+            "category": schema.get("category", "general"),
+            "impact": schema.get("impact", "medium"),
+            "requires_restart": schema.get("requires_restart", True),
+            "default": schema.get("default"),
+            "env_var": env_var,
+            "nested_example": nested_example,
+            "documentation_url": schema.get("documentation_url"),
+        }
+
+        docs_metadata.append(doc_entry)
+
+    # Group by category
+    categories: Dict[str, List[Dict[str, Any]]] = {}
+    for entry in docs_metadata:
+        category = str(entry["category"])
+        if category not in categories:
+            categories[category] = []
+        categories[category].append(entry)
+
+    # Sort entries within each category
+    for category in categories:
+        categories[category].sort(key=lambda x: x["key"])
+
+    # Export as JSON
+    output_dir = Path(__file__).parent.parent / "src" / "resources"
+    output_dir.mkdir(exist_ok=True)
+
+    # Export all settings
+    with open(output_dir / "config_metadata.json", "w") as f:
+        f.write(
+            json.dumps(
+                {
+                    "all_settings": docs_metadata,
+                    "by_category": categories,
+                    "categories": list(categories.keys()),
+                },
+                indent=2,
+            )
+        )
+
+    output_file = output_dir / "config_metadata.json"
+    print(f"Exported {len(docs_metadata)} configuration settings to 
{output_file}")
+    return docs_metadata
+
+
+if __name__ == "__main__":
+    export_config_metadata()
diff --git a/docs/scripts/update_docs.sh b/docs/scripts/update_docs.sh
new file mode 100755
index 0000000000..d91e687c0b
--- /dev/null
+++ b/docs/scripts/update_docs.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# 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.
+
+# Update documentation with latest configuration metadata
+# This script should be run before building the documentation
+
+set -e
+
+echo "Updating configuration metadata for documentation..."
+
+# Navigate to the docs directory
+cd "$(dirname "$0")/.."
+
+# Export configuration metadata
+echo "Exporting configuration metadata..."
+python scripts/export_config_metadata.py
+
+echo "Configuration metadata updated successfully!"
+echo "The following files were updated:"
+echo "- src/resources/config_metadata.json"
+echo ""
+echo "To build the documentation with the latest metadata:"
+echo "  npm install"
+echo "  npm run build"
diff --git a/docs/src/components/ConfigurationTable.tsx 
b/docs/src/components/ConfigurationTable.tsx
new file mode 100644
index 0000000000..d2e67e81bf
--- /dev/null
+++ b/docs/src/components/ConfigurationTable.tsx
@@ -0,0 +1,331 @@
+/**
+ * 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 React, { useState } from 'react';
+import configMetadata from '../resources/config_metadata.json';
+
+interface ConfigSetting {
+  key: string;
+  title: string;
+  description: string;
+  type: string;
+  category: string;
+  impact: string;
+  requires_restart: boolean;
+  default: any;
+  env_var: string;
+  nested_example: string | null;
+  documentation_url: string | null;
+}
+
+interface ConfigurationTableProps {
+  category?: string;
+  showEnvironmentVariables?: boolean;
+}
+
+const ImpactBadge: React.FC<{ impact: string }> = ({ impact }) => {
+  const colors = {
+    low: '#52c41a',
+    medium: '#faad14',
+    high: '#ff4d4f',
+  };
+
+  return (
+    <span
+      style={{
+        backgroundColor: colors[impact] || '#d9d9d9',
+        color: 'white',
+        padding: '2px 8px',
+        borderRadius: '4px',
+        fontSize: '12px',
+        fontWeight: 'bold',
+      }}
+    >
+      {impact.toUpperCase()}
+    </span>
+  );
+};
+
+const RestartBadge: React.FC<{ requiresRestart: boolean }> = ({
+  requiresRestart,
+}) => {
+  if (!requiresRestart) return null;
+
+  return (
+    <span
+      style={{
+        backgroundColor: '#ff7875',
+        color: 'white',
+        padding: '2px 8px',
+        borderRadius: '4px',
+        fontSize: '12px',
+        fontWeight: 'bold',
+        marginLeft: '8px',
+      }}
+    >
+      RESTART
+    </span>
+  );
+};
+
+const ConfigurationTable: React.FC<ConfigurationTableProps> = ({
+  category,
+  showEnvironmentVariables = false,
+}) => {
+  const [selectedCategory, setSelectedCategory] = useState<string>(
+    category || 'all',
+  );
+
+  // Get settings based on selected category
+  const getSettings = (): ConfigSetting[] => {
+    if (selectedCategory === 'all') {
+      return configMetadata.all_settings;
+    }
+    return configMetadata.by_category[selectedCategory] || [];
+  };
+
+  const settings = getSettings();
+
+  const formatDefault = (value: any): string => {
+    if (value === null || value === undefined) return 'None';
+    if (typeof value === 'object') {
+      return JSON.stringify(value, null, 2);
+    }
+    return String(value);
+  };
+
+  const copyToClipboard = (text: string) => {
+    navigator.clipboard.writeText(text);
+  };
+
+  return (
+    <div style={{ margin: '20px 0' }}>
+      {/* Category selector */}
+      {!category && (
+        <div style={{ marginBottom: '20px' }}>
+          <label style={{ marginRight: '10px', fontWeight: 'bold' }}>
+            Category:
+          </label>
+          <select
+            value={selectedCategory}
+            onChange={e => setSelectedCategory(e.target.value)}
+            style={{ padding: '5px', marginRight: '10px' }}
+          >
+            <option value="all">All Categories</option>
+            {configMetadata.categories.map(cat => (
+              <option key={cat} value={cat}>
+                {cat.charAt(0).toUpperCase() + cat.slice(1)}
+              </option>
+            ))}
+          </select>
+          <span style={{ fontSize: '14px', color: '#666' }}>
+            Showing {settings.length} configuration settings
+          </span>
+        </div>
+      )}
+
+      {/* Table */}
+      <div style={{ overflowX: 'auto' }}>
+        <table
+          style={{
+            width: '100%',
+            borderCollapse: 'collapse',
+            border: '1px solid #ddd',
+          }}
+        >
+          <thead>
+            <tr style={{ backgroundColor: '#f5f5f5' }}>
+              <th
+                style={{
+                  padding: '12px',
+                  border: '1px solid #ddd',
+                  textAlign: 'left',
+                }}
+              >
+                Setting
+              </th>
+              <th
+                style={{
+                  padding: '12px',
+                  border: '1px solid #ddd',
+                  textAlign: 'left',
+                }}
+              >
+                Description
+              </th>
+              <th
+                style={{
+                  padding: '12px',
+                  border: '1px solid #ddd',
+                  textAlign: 'left',
+                }}
+              >
+                Type
+              </th>
+              <th
+                style={{
+                  padding: '12px',
+                  border: '1px solid #ddd',
+                  textAlign: 'left',
+                }}
+              >
+                Default
+              </th>
+              {showEnvironmentVariables && (
+                <th
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    textAlign: 'left',
+                  }}
+                >
+                  Environment Variable
+                </th>
+              )}
+              <th
+                style={{
+                  padding: '12px',
+                  border: '1px solid #ddd',
+                  textAlign: 'left',
+                }}
+              >
+                Impact
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            {settings.map((setting: ConfigSetting) => (
+              <tr key={setting.key}>
+                <td
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    verticalAlign: 'top',
+                  }}
+                >
+                  <div>
+                    <strong>{setting.title}</strong>
+                    <br />
+                    <code style={{ fontSize: '12px', color: '#666' }}>
+                      {setting.key}
+                    </code>
+                    {setting.documentation_url && (
+                      <div style={{ marginTop: '4px' }}>
+                        <a
+                          href={setting.documentation_url}
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          style={{ fontSize: '12px' }}
+                        >
+                          📖 Docs
+                        </a>
+                      </div>
+                    )}
+                  </div>
+                </td>
+                <td
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    verticalAlign: 'top',
+                  }}
+                >
+                  {setting.description}
+                </td>
+                <td
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    verticalAlign: 'top',
+                  }}
+                >
+                  <code>{setting.type}</code>
+                </td>
+                <td
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    verticalAlign: 'top',
+                  }}
+                >
+                  <code style={{ fontSize: '12px' }}>
+                    {formatDefault(setting.default)}
+                  </code>
+                </td>
+                {showEnvironmentVariables && (
+                  <td
+                    style={{
+                      padding: '12px',
+                      border: '1px solid #ddd',
+                      verticalAlign: 'top',
+                    }}
+                  >
+                    <div style={{ display: 'flex', alignItems: 'center' }}>
+                      <code style={{ fontSize: '12px', marginRight: '8px' }}>
+                        {setting.env_var}
+                      </code>
+                      <button
+                        onClick={() => copyToClipboard(setting.env_var)}
+                        style={{
+                          background: 'none',
+                          border: 'none',
+                          cursor: 'pointer',
+                          fontSize: '12px',
+                          color: '#1890ff',
+                        }}
+                        title="Copy to clipboard"
+                      >
+                        📋
+                      </button>
+                    </div>
+                    {setting.nested_example && (
+                      <div style={{ marginTop: '4px' }}>
+                        <code style={{ fontSize: '10px', color: '#666' }}>
+                          {setting.nested_example}
+                        </code>
+                      </div>
+                    )}
+                  </td>
+                )}
+                <td
+                  style={{
+                    padding: '12px',
+                    border: '1px solid #ddd',
+                    verticalAlign: 'top',
+                  }}
+                >
+                  <ImpactBadge impact={setting.impact} />
+                  <RestartBadge requiresRestart={setting.requires_restart} />
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {settings.length === 0 && (
+        <div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
+          No configuration settings found for the selected category.
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default ConfigurationTable;
diff --git a/docs/src/components/EnvironmentVariablesExample.tsx 
b/docs/src/components/EnvironmentVariablesExample.tsx
new file mode 100644
index 0000000000..7de8b61952
--- /dev/null
+++ b/docs/src/components/EnvironmentVariablesExample.tsx
@@ -0,0 +1,181 @@
+/**
+ * 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 React, { useState } from 'react';
+import configMetadata from '../resources/config_metadata.json';
+
+interface EnvironmentVariablesExampleProps {
+  category?: string;
+}
+
+const EnvironmentVariablesExample: React.FC<
+  EnvironmentVariablesExampleProps
+> = ({ category }) => {
+  const [showAll, setShowAll] = useState(false);
+
+  // Get settings based on category
+  const getSettings = () => {
+    if (category && configMetadata.by_category[category]) {
+      return configMetadata.by_category[category];
+    }
+    return configMetadata.all_settings;
+  };
+
+  const settings = getSettings();
+  const displaySettings = showAll ? settings : settings.slice(0, 5);
+
+  const formatDefaultForEnv = (value: any): string => {
+    if (value === null || value === undefined) return '""';
+    if (typeof value === 'object') {
+      return `'${JSON.stringify(value)}'`;
+    }
+    if (typeof value === 'string') {
+      return `"${value}"`;
+    }
+    return String(value);
+  };
+
+  const copyToClipboard = (text: string) => {
+    navigator.clipboard.writeText(text);
+  };
+
+  const generateEnvExample = (setting: any): string => {
+    const example = formatDefaultForEnv(setting.default);
+    return `export ${setting.env_var}=${example}`;
+  };
+
+  const generateAllEnvVars = (): string => {
+    return [
+      '# Superset Configuration Environment Variables',
+      '# Generated from configuration metadata',
+      '',
+      ...displaySettings.map(setting =>
+        [
+          `# ${setting.title}`,
+          `# ${setting.description}`,
+          `# Type: ${setting.type}`,
+          `# Impact: ${setting.impact}${
+            setting.requires_restart ? ' (requires restart)' : ''
+          }`,
+          generateEnvExample(setting),
+          '',
+        ].join('\n'),
+      ),
+    ].join('\n');
+  };
+
+  return (
+    <div style={{ margin: '20px 0' }}>
+      <div
+        style={{
+          backgroundColor: '#f6f8fa',
+          border: '1px solid #e1e4e8',
+          borderRadius: '6px',
+          padding: '16px',
+          position: 'relative',
+        }}
+      >
+        <div
+          style={{
+            display: 'flex',
+            justifyContent: 'space-between',
+            alignItems: 'center',
+            marginBottom: '10px',
+          }}
+        >
+          <h4 style={{ margin: 0, color: '#24292e' }}>
+            Environment Variables {category && `(${category})`}
+          </h4>
+          <button
+            onClick={() => copyToClipboard(generateAllEnvVars())}
+            style={{
+              backgroundColor: '#0366d6',
+              color: 'white',
+              border: 'none',
+              padding: '6px 12px',
+              borderRadius: '4px',
+              cursor: 'pointer',
+              fontSize: '12px',
+            }}
+            title="Copy all environment variables"
+          >
+            📋 Copy All
+          </button>
+        </div>
+
+        <pre
+          style={{
+            backgroundColor: '#f6f8fa',
+            border: 'none',
+            padding: '0',
+            margin: '0',
+            fontFamily:
+              'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace',
+            fontSize: '12px',
+            lineHeight: '1.45',
+            overflow: 'auto',
+            maxHeight: '400px',
+          }}
+        >
+          <code>{generateAllEnvVars()}</code>
+        </pre>
+
+        {!showAll && settings.length > 5 && (
+          <div
+            style={{
+              textAlign: 'center',
+              marginTop: '10px',
+              borderTop: '1px solid #e1e4e8',
+              paddingTop: '10px',
+            }}
+          >
+            <button
+              onClick={() => setShowAll(true)}
+              style={{
+                backgroundColor: 'transparent',
+                border: '1px solid #0366d6',
+                color: '#0366d6',
+                padding: '6px 12px',
+                borderRadius: '4px',
+                cursor: 'pointer',
+                fontSize: '12px',
+              }}
+            >
+              Show all {settings.length} settings
+            </button>
+          </div>
+        )}
+      </div>
+
+      <div
+        style={{
+          marginTop: '10px',
+          fontSize: '14px',
+          color: '#586069',
+        }}
+      >
+        <strong>Usage:</strong> Save to a <code>.env</code> file or export
+        directly in your shell.
+        {category && ` Showing ${settings.length} ${category} settings.`}
+      </div>
+    </div>
+  );
+};
+
+export default EnvironmentVariablesExample;
diff --git a/docs/src/resources/config_metadata.json 
b/docs/src/resources/config_metadata.json
new file mode 100644
index 0000000000..a2cdb41281
--- /dev/null
+++ b/docs/src/resources/config_metadata.json
@@ -0,0 +1,225 @@
+{
+  "all_settings": [
+    {
+      "key": "ROW_LIMIT",
+      "title": "Row Limit",
+      "description": "Maximum number of rows returned from queries",
+      "type": "integer (1 - 1000000)",
+      "category": "performance",
+      "impact": "medium",
+      "requires_restart": false,
+      "default": 50000,
+      "env_var": "SUPERSET__ROW_LIMIT",
+      "nested_example": null,
+      "documentation_url": 
"https://superset.apache.org/docs/configuration/databases";
+    },
+    {
+      "key": "SAMPLES_ROW_LIMIT",
+      "title": "Samples Row Limit",
+      "description": "Default row limit when requesting samples from 
datasource",
+      "type": "integer (1 - 10000)",
+      "category": "performance",
+      "impact": "low",
+      "requires_restart": false,
+      "default": 1000,
+      "env_var": "SUPERSET__SAMPLES_ROW_LIMIT",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "NATIVE_FILTER_DEFAULT_ROW_LIMIT",
+      "title": "Native Filter Default Row Limit",
+      "description": "Default row limit for native filters",
+      "type": "integer (1 - 10000)",
+      "category": "performance",
+      "impact": "low",
+      "requires_restart": false,
+      "default": 1000,
+      "env_var": "SUPERSET__NATIVE_FILTER_DEFAULT_ROW_LIMIT",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "SQLLAB_TIMEOUT",
+      "title": "SQL Lab Timeout",
+      "description": "Timeout duration for SQL Lab synchronous queries 
(seconds)",
+      "type": "integer (1 - 3600)",
+      "category": "performance",
+      "impact": "high",
+      "requires_restart": false,
+      "default": 30,
+      "env_var": "SUPERSET__SQLLAB_TIMEOUT",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "FEATURE_FLAGS",
+      "title": "Feature Flags",
+      "description": "Feature flags to enable/disable functionality",
+      "type": "object",
+      "category": "features",
+      "impact": "high",
+      "requires_restart": true,
+      "default": {},
+      "env_var": "SUPERSET__FEATURE_FLAGS",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "THEME_DEFAULT",
+      "title": "Default Theme",
+      "description": "Default theme configuration (Ant Design format)",
+      "type": "object",
+      "category": "ui",
+      "impact": "medium",
+      "requires_restart": false,
+      "default": {},
+      "env_var": "SUPERSET__THEME_DEFAULT",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "THEME_DARK",
+      "title": "Dark Theme",
+      "description": "Dark theme configuration (Ant Design format)",
+      "type": "object",
+      "category": "ui",
+      "impact": "medium",
+      "requires_restart": false,
+      "default": {},
+      "env_var": "SUPERSET__THEME_DARK",
+      "nested_example": null,
+      "documentation_url": null
+    },
+    {
+      "key": "THEME_SETTINGS",
+      "title": "Theme Settings",
+      "description": "Theme behavior and user preference settings",
+      "type": "object",
+      "category": "ui",
+      "impact": "medium",
+      "requires_restart": false,
+      "default": {},
+      "env_var": "SUPERSET__THEME_SETTINGS",
+      "nested_example": null,
+      "documentation_url": null
+    }
+  ],
+  "by_category": {
+    "performance": [
+      {
+        "key": "NATIVE_FILTER_DEFAULT_ROW_LIMIT",
+        "title": "Native Filter Default Row Limit",
+        "description": "Default row limit for native filters",
+        "type": "integer (1 - 10000)",
+        "category": "performance",
+        "impact": "low",
+        "requires_restart": false,
+        "default": 1000,
+        "env_var": "SUPERSET__NATIVE_FILTER_DEFAULT_ROW_LIMIT",
+        "nested_example": null,
+        "documentation_url": null
+      },
+      {
+        "key": "ROW_LIMIT",
+        "title": "Row Limit",
+        "description": "Maximum number of rows returned from queries",
+        "type": "integer (1 - 1000000)",
+        "category": "performance",
+        "impact": "medium",
+        "requires_restart": false,
+        "default": 50000,
+        "env_var": "SUPERSET__ROW_LIMIT",
+        "nested_example": null,
+        "documentation_url": 
"https://superset.apache.org/docs/configuration/databases";
+      },
+      {
+        "key": "SAMPLES_ROW_LIMIT",
+        "title": "Samples Row Limit",
+        "description": "Default row limit when requesting samples from 
datasource",
+        "type": "integer (1 - 10000)",
+        "category": "performance",
+        "impact": "low",
+        "requires_restart": false,
+        "default": 1000,
+        "env_var": "SUPERSET__SAMPLES_ROW_LIMIT",
+        "nested_example": null,
+        "documentation_url": null
+      },
+      {
+        "key": "SQLLAB_TIMEOUT",
+        "title": "SQL Lab Timeout",
+        "description": "Timeout duration for SQL Lab synchronous queries 
(seconds)",
+        "type": "integer (1 - 3600)",
+        "category": "performance",
+        "impact": "high",
+        "requires_restart": false,
+        "default": 30,
+        "env_var": "SUPERSET__SQLLAB_TIMEOUT",
+        "nested_example": null,
+        "documentation_url": null
+      }
+    ],
+    "features": [
+      {
+        "key": "FEATURE_FLAGS",
+        "title": "Feature Flags",
+        "description": "Feature flags to enable/disable functionality",
+        "type": "object",
+        "category": "features",
+        "impact": "high",
+        "requires_restart": true,
+        "default": {},
+        "env_var": "SUPERSET__FEATURE_FLAGS",
+        "nested_example": null,
+        "documentation_url": null
+      }
+    ],
+    "ui": [
+      {
+        "key": "THEME_DARK",
+        "title": "Dark Theme",
+        "description": "Dark theme configuration (Ant Design format)",
+        "type": "object",
+        "category": "ui",
+        "impact": "medium",
+        "requires_restart": false,
+        "default": {},
+        "env_var": "SUPERSET__THEME_DARK",
+        "nested_example": null,
+        "documentation_url": null
+      },
+      {
+        "key": "THEME_DEFAULT",
+        "title": "Default Theme",
+        "description": "Default theme configuration (Ant Design format)",
+        "type": "object",
+        "category": "ui",
+        "impact": "medium",
+        "requires_restart": false,
+        "default": {},
+        "env_var": "SUPERSET__THEME_DEFAULT",
+        "nested_example": null,
+        "documentation_url": null
+      },
+      {
+        "key": "THEME_SETTINGS",
+        "title": "Theme Settings",
+        "description": "Theme behavior and user preference settings",
+        "type": "object",
+        "category": "ui",
+        "impact": "medium",
+        "requires_restart": false,
+        "default": {},
+        "env_var": "SUPERSET__THEME_SETTINGS",
+        "nested_example": null,
+        "documentation_url": null
+      }
+    ]
+  },
+  "categories": [
+    "performance",
+    "features",
+    "ui"
+  ]
+}
diff --git a/superset/commands/settings/__init__.py 
b/superset/commands/settings/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/superset/commands/settings/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/commands/settings/exceptions.py 
b/superset/commands/settings/exceptions.py
new file mode 100644
index 0000000000..d45cf712bb
--- /dev/null
+++ b/superset/commands/settings/exceptions.py
@@ -0,0 +1,47 @@
+# 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.
+from superset.commands.exceptions import CommandException
+
+
+class SettingsCommandException(CommandException):
+    """Base exception for settings commands."""
+
+    pass
+
+
+class SettingNotFoundError(SettingsCommandException):
+    """Exception raised when a setting is not found."""
+
+    pass
+
+
+class SettingNotAllowedInDatabaseError(SettingsCommandException):
+    """Exception raised when a setting cannot be stored in the database."""
+
+    pass
+
+
+class SettingValidationError(SettingsCommandException):
+    """Exception raised when a setting value is invalid."""
+
+    pass
+
+
+class SettingAlreadyExistsError(SettingsCommandException):
+    """Exception raised when trying to create a setting that already exists."""
+
+    pass
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index 9ac70a4ad1..1ec660c984 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -160,6 +160,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
             SecurityRestApi,
             UserRegistrationsRestAPI,
         )
+        from superset.settings.api import SettingsRestApi
         from superset.sqllab.api import SqlLabRestApi
         from superset.sqllab.permalink.api import SqlLabPermalinkRestApi
         from superset.tags.api import TagRestApi
@@ -242,6 +243,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_api(ReportExecutionLogRestApi)
         appbuilder.add_api(RLSRestApi)
         appbuilder.add_api(SavedQueryRestApi)
+        appbuilder.add_api(SettingsRestApi)
         appbuilder.add_api(TagRestApi)
         appbuilder.add_api(SqlLabRestApi)
         appbuilder.add_api(SqlLabPermalinkRestApi)
diff --git a/superset/settings/__init__.py b/superset/settings/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/superset/settings/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/settings/api.py b/superset/settings/api.py
new file mode 100644
index 0000000000..27f05254d3
--- /dev/null
+++ b/superset/settings/api.py
@@ -0,0 +1,379 @@
+# 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 logging
+from typing import Any, Dict, Optional
+
+from flask import current_app, request
+from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from marshmallow import ValidationError
+
+from superset.config_extensions import SupersetConfig
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
+from superset.daos.settings import SettingsDAO
+from superset.models.core import Settings
+from superset.settings.schemas import (
+    SettingCreateSchema,
+    SettingListResponseSchema,
+    SettingResponseSchema,
+    SettingUpdateSchema,
+)
+from superset.views.base_api import BaseSupersetModelRestApi
+
+logger = logging.getLogger(__name__)
+
+
+class SettingsRestApi(BaseSupersetModelRestApi):
+    """REST API for configuration settings management."""
+
+    datamodel = SQLAInterface(Settings)
+    resource_name = "settings"
+    allow_browser_login = True
+
+    # Only allow specific methods
+    include_route_methods = {
+        "get_list",
+        "get",
+        "post",
+        "put",
+        "delete",
+        "info",
+    }
+
+    # Schemas for serialization/deserialization
+    list_schema = SettingListResponseSchema()
+    show_schema = SettingResponseSchema()
+    add_schema = SettingCreateSchema()
+    edit_schema = SettingUpdateSchema()
+
+    # Permissions - only admins can read/write settings
+    method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP.copy()
+    method_permission_name.update(
+        {
+            "get_list": "can_read",
+            "get": "can_read",
+            "post": "can_write",
+            "put": "can_write",
+            "delete": "can_write",
+            "validate": "can_read",
+            "metadata": "can_read",
+            "effective_config": "can_read",
+        }
+    )
+
+    # Only Admin role can access settings
+    class_permission_name = "Settings"
+
+    def _get_setting_metadata(self, key: str) -> Optional[Dict[str, Any]]:
+        """Get metadata for a configuration setting."""
+        if isinstance(current_app.config, SupersetConfig):
+            return current_app.config.get_setting_metadata(key)
+        return None
+
+    def _get_setting_source(self, key: str) -> str:
+        """Determine where a setting value comes from."""
+        import os
+
+        # Check if it's from environment variables
+        env_key = f"SUPERSET__{key}"
+        if env_key in os.environ:
+            return f"environment ({env_key})"
+
+        # Check if it's from superset_config.py
+        try:
+            import superset_config
+
+            if hasattr(superset_config, key):
+                return "superset_config.py"
+        except ImportError:
+            pass
+
+        # Check if it's in database
+        if SettingsDAO.find_by_key(key):
+            return "database"
+
+        # Otherwise it's from defaults
+        return "config_defaults.py"
+
+    def _is_setting_allowed_in_database(self, key: str) -> bool:
+        """Check if a setting is allowed to be stored in the database."""
+        metadata = self._get_setting_metadata(key)
+        if not metadata:
+            return False
+
+        # Only allow settings that don't require restart and aren't readonly
+        return not metadata.get("requires_restart", True) and not metadata.get(
+            "readonly", False
+        )
+
+    def _validate_setting_value(self, key: str, value: Any) -> tuple[bool, 
list[str]]:
+        """Validate a setting value against its metadata."""
+        metadata = self._get_setting_metadata(key)
+        if not metadata:
+            return True, []
+
+        if isinstance(current_app.config, SupersetConfig):
+            if current_app.config.validate_setting(key, value):
+                return True, []
+            else:
+                return False, [
+                    f"Value does not match expected type or constraints for 
{key}"
+                ]
+
+        return True, []
+
+    @expose("/", methods=["GET"])
+    @protect()
+    @safe
+    def get_list(self) -> str:
+        """Get list of all database settings."""
+        # Parse query parameters
+        args = request.args
+        namespace = args.get("namespace")
+        category = args.get("category")
+        include_metadata = args.get("include_metadata", "false").lower() == 
"true"
+        include_source = args.get("include_source", "false").lower() == "true"
+
+        # Get settings from database
+        if namespace:
+            settings = SettingsDAO.get_by_namespace(namespace)
+        else:
+            settings = SettingsDAO.get_all_as_dict()
+
+        # Build response
+        result = []
+        for key, value in settings.items():
+            setting_data = {
+                "key": key,
+                "value": value,
+                "namespace": namespace,  # This would need to be fetched from 
the model
+            }
+
+            if include_metadata:
+                setting_data["metadata"] = self._get_setting_metadata(key)
+
+            if include_source:
+                setting_data["source"] = self._get_setting_source(key)
+
+            # Filter by category if specified
+            if category:
+                metadata = self._get_setting_metadata(key)
+                if not metadata or metadata.get("category") != category:
+                    continue
+
+            result.append(setting_data)
+
+        return self.response(200, result=result, count=len(result))
+
+    @expose("/<pk>", methods=["GET"])
+    @protect()
+    @safe
+    def get(self, pk: str) -> str:
+        """Get a specific setting by key."""
+        # Parse query parameters
+        args = request.args
+        include_metadata = args.get("include_metadata", "false").lower() == 
"true"
+        include_source = args.get("include_source", "false").lower() == "true"
+
+        # Get setting value
+        value = SettingsDAO.get_value(pk)
+        if value is None:
+            return self.response_404()
+
+        # Build response
+        setting_data = {
+            "key": pk,
+            "value": value,
+        }
+
+        if include_metadata:
+            setting_data["metadata"] = self._get_setting_metadata(pk)
+
+        if include_source:
+            setting_data["source"] = self._get_setting_source(pk)
+
+        return self.response(200, **setting_data)
+
+    @expose("/", methods=["POST"])
+    @protect()
+    @safe
+    def post(self) -> str:
+        """Create a new setting."""
+        try:
+            item = self.add_schema.load(request.json)
+        except ValidationError as error:
+            return self.response_422(message=error.messages)
+
+        key = item["key"]
+        value = item["value"]
+        namespace = item.get("namespace")
+
+        # Check if setting is allowed in database
+        if not self._is_setting_allowed_in_database(key):
+            return self.response_422(
+                message=f"Setting '{key}' cannot be stored in database. "
+                f"It may require a restart or be read-only."
+            )
+
+        # Validate value
+        is_valid, errors = self._validate_setting_value(key, value)
+        if not is_valid:
+            return self.response_422(message={"validation_errors": errors})
+
+        # Check if setting already exists
+        if SettingsDAO.find_by_key(key):
+            return self.response_422(
+                message=f"Setting '{key}' already exists. Use PUT to update."
+            )
+
+        # Create setting
+        try:
+            SettingsDAO.set_value(key, value, namespace)
+            return self.response(201, key=key, value=value)
+        except Exception as ex:
+            logger.exception("Error creating setting")
+            return self.response_422(message=str(ex))
+
+    @expose("/<pk>", methods=["PUT"])
+    @protect()
+    @safe
+    def put(self, pk: str) -> str:
+        """Update an existing setting."""
+        try:
+            item = self.edit_schema.load(request.json)
+        except ValidationError as error:
+            return self.response_422(message=error.messages)
+
+        value = item["value"]
+        namespace = item.get("namespace")
+
+        # Check if setting is allowed in database
+        if not self._is_setting_allowed_in_database(pk):
+            return self.response_422(
+                message=f"Setting '{pk}' cannot be stored in database. "
+                f"It may require a restart or be read-only."
+            )
+
+        # Validate value
+        is_valid, errors = self._validate_setting_value(pk, value)
+        if not is_valid:
+            return self.response_422(message={"validation_errors": errors})
+
+        # Update setting
+        try:
+            SettingsDAO.set_value(pk, value, namespace)
+            return self.response(200, key=pk, value=value)
+        except Exception as ex:
+            logger.exception("Error updating setting")
+            return self.response_422(message=str(ex))
+
+    @expose("/<pk>", methods=["DELETE"])
+    @protect()
+    @safe
+    def delete(self, pk: str) -> str:
+        """Delete a setting."""
+        # Check if setting exists
+        if not SettingsDAO.find_by_key(pk):
+            return self.response_404()
+
+        # Delete setting
+        try:
+            SettingsDAO.delete_by_key(pk)
+            return self.response(200, message=f"Setting '{pk}' deleted")
+        except Exception as ex:
+            logger.exception("Error deleting setting")
+            return self.response_422(message=str(ex))
+
+    @expose("/validate", methods=["POST"])
+    @protect()
+    @safe
+    def validate(self) -> str:
+        """Validate a setting value without saving it."""
+        if not request.json:
+            return self.response_400()
+
+        key = request.json.get("key")
+        value = request.json.get("value")
+
+        if not key:
+            return self.response_422(message="Key is required")
+
+        # Get metadata
+        metadata = self._get_setting_metadata(key)
+
+        # Validate value
+        is_valid, errors = self._validate_setting_value(key, value)
+
+        # Check if allowed in database
+        allowed_in_db = self._is_setting_allowed_in_database(key)
+
+        response_data = {
+            "key": key,
+            "value": value,
+            "valid": is_valid,
+            "errors": errors,
+            "metadata": metadata,
+            "allowed_in_database": allowed_in_db,
+        }
+
+        return self.response(200, **response_data)
+
+    @expose("/metadata", methods=["GET"])
+    @protect()
+    @safe
+    def metadata(self) -> str:
+        """Get metadata for all documented settings."""
+        if not isinstance(current_app.config, SupersetConfig):
+            return self.response(200, metadata={})
+
+        metadata = current_app.config.DATABASE_SETTINGS_SCHEMA
+
+        # Filter by category if specified
+        if category := request.args.get("category"):
+            metadata = current_app.config.get_settings_by_category(category)
+
+        return self.response(200, metadata=metadata)
+
+    @expose("/effective_config", methods=["GET"])
+    @protect()
+    @safe
+    def effective_config(self) -> str:
+        """Get effective configuration (database + env + defaults)."""
+        # Get current config values
+        config_dict = {}
+
+        # Get documented settings
+        if isinstance(current_app.config, SupersetConfig):
+            for key in current_app.config.DATABASE_SETTINGS_SCHEMA:
+                if key in current_app.config:
+                    config_dict[key] = {
+                        "value": current_app.config[key],
+                        "source": self._get_setting_source(key),
+                        "metadata": self._get_setting_metadata(key),
+                    }
+
+        # Add database settings
+        db_settings = SettingsDAO.get_all_as_dict()
+        for key, value in db_settings.items():
+            if key not in config_dict:
+                config_dict[key] = {
+                    "value": value,
+                    "source": "database",
+                    "metadata": self._get_setting_metadata(key),
+                }
+
+        return self.response(200, config=config_dict)
diff --git a/superset/settings/schemas.py b/superset/settings/schemas.py
new file mode 100644
index 0000000000..dfd76fd797
--- /dev/null
+++ b/superset/settings/schemas.py
@@ -0,0 +1,110 @@
+# 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.
+from marshmallow import fields, Schema, validate
+
+
+class SettingCreateSchema(Schema):
+    """Schema for creating a new setting."""
+
+    key = fields.String(required=True, validate=validate.Length(min=1, 
max=255))
+    value = fields.Raw(required=True)
+    namespace = fields.String(allow_none=True, 
validate=validate.Length(max=100))
+
+
+class SettingUpdateSchema(Schema):
+    """Schema for updating an existing setting."""
+
+    value = fields.Raw(required=True)
+    namespace = fields.String(allow_none=True, 
validate=validate.Length(max=100))
+
+
+class SettingResponseSchema(Schema):
+    """Schema for setting response."""
+
+    key = fields.String()
+    value = fields.Raw()
+    namespace = fields.String(allow_none=True)
+    created_on = fields.DateTime()
+    changed_on = fields.DateTime()
+    created_by = fields.Nested("UserSchema", only=["id", "first_name", 
"last_name"])
+    changed_by = fields.Nested("UserSchema", only=["id", "first_name", 
"last_name"])
+    metadata = fields.Dict(allow_none=True)  # Configuration metadata if 
available
+    source = fields.String(
+        allow_none=True
+    )  # Source of the setting (database, env, defaults)
+
+
+class SettingListResponseSchema(Schema):
+    """Schema for listing settings."""
+
+    result = fields.List(fields.Nested(SettingResponseSchema))
+    count = fields.Integer()
+
+
+class SettingMetadataSchema(Schema):
+    """Schema for configuration metadata."""
+
+    title = fields.String()
+    description = fields.String()
+    type = fields.String()
+    category = fields.String()
+    impact = fields.String()
+    requires_restart = fields.Boolean()
+    default = fields.Raw()
+    minimum = fields.Integer(allow_none=True)
+    maximum = fields.Integer(allow_none=True)
+    readonly = fields.Boolean()
+    documentation_url = fields.String(allow_none=True)
+
+
+class SettingValidationSchema(Schema):
+    """Schema for setting validation."""
+
+    key = fields.String(required=True)
+    value = fields.Raw(required=True)
+    valid = fields.Boolean()
+    errors = fields.List(fields.String())
+    metadata = fields.Nested(SettingMetadataSchema, allow_none=True)
+
+
+# Query schemas for API endpoints
+setting_get_schema = {
+    "type": "object",
+    "properties": {
+        "include_metadata": {"type": "boolean"},
+        "include_source": {"type": "boolean"},
+    },
+}
+
+setting_list_schema = {
+    "type": "object",
+    "properties": {
+        "namespace": {"type": "string"},
+        "category": {"type": "string"},
+        "include_metadata": {"type": "boolean"},
+        "include_source": {"type": "boolean"},
+    },
+}
+
+setting_validate_schema = {
+    "type": "object",
+    "properties": {
+        "key": {"type": "string"},
+        "value": {},  # Any type
+    },
+    "required": ["key", "value"],
+}

Reply via email to