mirror of
https://github.com/apache/superset.git
synced 2026-05-09 18:05:52 +00:00
touchups
This commit is contained in:
105
docs/scripts/export_config_metadata.py
Normal file
105
docs/scripts/export_config_metadata.py
Normal file
@@ -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()
|
||||
40
docs/scripts/update_docs.sh
Executable file
40
docs/scripts/update_docs.sh
Executable file
@@ -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"
|
||||
331
docs/src/components/ConfigurationTable.tsx
Normal file
331
docs/src/components/ConfigurationTable.tsx
Normal file
@@ -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;
|
||||
181
docs/src/components/EnvironmentVariablesExample.tsx
Normal file
181
docs/src/components/EnvironmentVariablesExample.tsx
Normal file
@@ -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;
|
||||
225
docs/src/resources/config_metadata.json
Normal file
225
docs/src/resources/config_metadata.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
16
superset/commands/settings/__init__.py
Normal file
16
superset/commands/settings/__init__.py
Normal file
@@ -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.
|
||||
47
superset/commands/settings/exceptions.py
Normal file
47
superset/commands/settings/exceptions.py
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
16
superset/settings/__init__.py
Normal file
16
superset/settings/__init__.py
Normal file
@@ -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.
|
||||
379
superset/settings/api.py
Normal file
379
superset/settings/api.py
Normal file
@@ -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)
|
||||
110
superset/settings/schemas.py
Normal file
110
superset/settings/schemas.py
Normal file
@@ -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"],
|
||||
}
|
||||
Reference in New Issue
Block a user