Add functionality in settings to import data

This commit is contained in:
Peter Stockings
2025-12-02 15:19:20 +11:00
parent 290b141d32
commit ab7079f87e
4 changed files with 406 additions and 128 deletions

122
db.py
View File

@@ -427,4 +427,124 @@ ORDER BY invocation_time DESC""", [http_function_id])
link['created_at'] = link['created_at'].isoformat()
export_data['timer_function_shared_env_links'] = timer_shared_env_links or []
return export_data
return export_data
def import_http_function(self, user_id, func_data):
"""Import a single HTTP function, returns (success, message, function_id)"""
try:
# Check if function with same name exists
existing = self.execute(
"SELECT id FROM http_functions WHERE user_id = %s AND name = %s",
(user_id, func_data['name']),
one=True
)
if existing:
return (False, f"Function '{func_data['name']}' already exists", None)
# Insert the function
result = self.execute(
"""INSERT INTO http_functions
(name, code, environment, version_number, user_id, runtime)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id""",
(
func_data['name'],
func_data['code'],
json.dumps(func_data.get('environment', {})),
1, # Start at version 1
user_id,
func_data.get('runtime', 'python')
),
one=True,
commit=True
)
return (True, f"Imported function '{func_data['name']}'", result['id'])
except Exception as e:
return (False, f"Error importing '{func_data.get('name', 'unknown')}': {str(e)}", None)
def import_timer_function(self,user_id, func_data):
"""Import a single timer function, returns (success, message, function_id)"""
try:
# Check if function with same name exists
existing = self.execute(
"SELECT id FROM timer_functions WHERE user_id = %s AND name = %s",
(user_id, func_data['name']),
one=True
)
if existing:
return (False, f"Timer function '{func_data['name']}' already exists", None)
# Calculate next_run based on trigger type
from routes.timer import calculate_next_run
next_run = calculate_next_run(
func_data['trigger_type'],
func_data.get('frequency_minutes'),
func_data.get('run_date'),
func_data.get('cron_expression')
)
# Insert the function
result = self.execute(
"""INSERT INTO timer_functions
(name, code, environment, version_number, user_id, runtime,
trigger_type, frequency_minutes, run_date, cron_expression,
next_run, enabled)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id""",
(
func_data['name'],
func_data['code'],
json.dumps(func_data.get('environment', {})),
1, # Start at version 1
user_id,
func_data.get('runtime', 'python'),
func_data['trigger_type'],
func_data.get('frequency_minutes'),
func_data.get('run_date'),
func_data.get('cron_expression'),
next_run,
func_data.get('enabled', True)
),
one=True,
commit=True
)
return (True, f"Imported timer function '{func_data['name']}'", result['id'])
except Exception as e:
return (False, f"Error importing timer '{func_data.get('name', 'unknown')}': {str(e)}", None)
def import_shared_environment(self, user_id, env_data):
"""Import a single shared environment, returns (success, message, env_id)"""
try:
# Check if environment with same name exists
existing = self.execute(
"SELECT id FROM shared_environments WHERE user_id = %s AND name = %s",
(user_id, env_data['name']),
one=True
)
if existing:
return (False, f"Shared environment '{env_data['name']}' already exists", None)
# Insert the environment
result = self.execute(
"""INSERT INTO shared_environments
(name, environment, user_id, version_number)
VALUES (%s, %s, %s, %s)
RETURNING id""",
(
env_data['name'],
json.dumps(env_data.get('environment', {})),
user_id,
1 # Start at version 1
),
one=True,
commit=True
)
return (True, f"Imported shared environment '{env_data['name']}'", result['id'])
except Exception as e:
return (False, f"Error importing environment '{env_data.get('name', 'unknown')}': {str(e)}", None)

View File

@@ -285,3 +285,131 @@ def execute_query():
except Exception as e:
return {"error": f"Query execution failed: {str(e)}"}, 500
@settings.route("/import", methods=["POST"])
@login_required
def import_data():
"""Import user data from JSON file"""
try:
# Check if file was uploaded
if 'import_file' not in request.files:
return {"error": "No file uploaded"}, 400
file = request.files['import_file']
if file.filename == '':
return {"error": "No file selected"}, 400
# Validate file type
if not file.filename.endswith('.json'):
return {"error": "File must be a JSON file"}, 400
# Read and parse JSON
try:
file_content = file.read()
# Check file size (max 10MB)
if len(file_content) > 10 * 1024 * 1024:
return {"error": "File too large (max 10MB)"}, 400
import_data = json.loads(file_content)
except json.JSONDecodeError as e:
return {"error": f"Invalid JSON format: {str(e)}"}, 400
# Validate structure
if not isinstance(import_data, dict):
return {"error": "Invalid data format: expected JSON object"}, 400
user_id = current_user.id
results = {
"http_functions": {"success": [], "skipped": [], "failed": []},
"timer_functions": {"success": [], "skipped": [], "failed": []},
"shared_environments": {"success": [], "skipped": [], "failed": []}
}
# Import HTTP Functions
http_functions = import_data.get('http_functions', [])
for func in http_functions:
# Map old export column names to new import method requirements
func_data = {
'name': func.get('name'),
'code': func.get('script_content'), # Export uses 'script_content'
'environment': func.get('environment_info'), # Export uses 'environment_info'
'runtime': func.get('runtime', 'python')
}
success, message, func_id = db.import_http_function(user_id, func_data)
if success:
results['http_functions']['success'].append(message)
elif 'already exists' in message:
results['http_functions']['skipped'].append(message)
else:
results['http_functions']['failed'].append(message)
# Import Timer Functions
timer_functions = import_data.get('timer_functions', [])
for func in timer_functions:
func_data = {
'name': func.get('name'),
'code': func.get('code'),
'environment': func.get('environment'),
'runtime': func.get('runtime', 'python'),
'trigger_type': func.get('trigger_type'),
'frequency_minutes': func.get('frequency_minutes'),
'run_date': func.get('run_date'),
'cron_expression': func.get('cron_expression'),
'enabled': func.get('enabled', True)
}
success, message, func_id = db.import_timer_function(user_id, func_data)
if success:
results['timer_functions']['success'].append(message)
elif 'already exists' in message:
results['timer_functions']['skipped'].append(message)
else:
results['timer_functions']['failed'].append(message)
# Import Shared Environments
shared_envs = import_data.get('shared_environments', [])
for env in shared_envs:
env_data = {
'name': env.get('name'),
'environment': env.get('environment')
}
success, message, env_id = db.import_shared_environment(user_id, env_data)
if success:
results['shared_environments']['success'].append(message)
elif 'already exists' in message:
results['shared_environments']['skipped'].append(message)
else:
results['shared_environments']['failed'].append(message)
# Calculate totals
total_success = (len(results['http_functions']['success']) +
len(results['timer_functions']['success']) +
len(results['shared_environments']['success']))
total_skipped = (len(results['http_functions']['skipped']) +
len(results['timer_functions']['skipped']) +
len(results['shared_environments']['skipped']))
total_failed = (len(results['http_functions']['failed']) +
len(results['timer_functions']['failed']) +
len(results['shared_environments']['failed']))
return {
"success": True,
"results": results,
"summary": {
"total_success": total_success,
"total_skipped": total_skipped,
"total_failed": total_failed
}
}
except Exception as e:
return {"error": f"Import failed: {str(e)}"}, 500

View File

@@ -29,7 +29,6 @@
<!-- ERD Diagram -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Entity Relationship Diagram</h2>
<div id="mermaid-diagram"
class="bg-white dark:bg-gray-900 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
<pre class="mermaid">
@@ -54,32 +53,26 @@ erDiagram
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Run SELECT queries on your data. Queries are automatically scoped to your user account for security.
</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SQL Query</label>
<div id="sql-editor" class="border border-gray-300 dark:border-gray-600 rounded" style="height: 150px;">
</div>
</div>
<div class="flex items-center justify-between mb-4">
<button id="run-query-btn"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4 .263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Run Query
</button>
<button id="clear-query-btn"
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Clear
</button>
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Clear</button>
</div>
<div id="query-message" class="hidden mb-4 p-3 rounded-lg"></div>
<div id="query-results" class="hidden">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Results</h3>
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg max-h-96">
@@ -88,8 +81,7 @@ erDiagram
<tr id="results-header"></tr>
</thead>
<tbody id="results-body"
class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
</tbody>
class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
@@ -98,7 +90,6 @@ erDiagram
<!-- Tables Overview -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Tables Overview</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for table in schema_info %}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow">
@@ -111,11 +102,8 @@ erDiagram
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
{{ table.columns|length }} column{{ 's' if table.columns|length != 1 else '' }}
{% if table.primary_keys %}
· {{ table.primary_keys|length }} PK
{% endif %}
{% if table.primary_keys %}· {{ table.primary_keys|length }} PK{% endif %}
</p>
<div class="space-y-1 max-h-40 overflow-y-auto">
{% for col in table.columns %}
<div class="text-xs flex items-start py-1">
@@ -131,14 +119,12 @@ erDiagram
<div class="flex-1 min-w-0">
<span class="font-medium text-gray-700 dark:text-gray-300">{{ col.column_name }}</span>
<span class="text-gray-500 dark:text-gray-500 ml-1">{{ col.data_type }}</span>
{% if col.is_nullable == 'NO' %}
<span class="text-red-500 text-[10px] ml-1">*</span>
{% endif %}
{% if col.is_nullable == 'NO' %}<span class="text-red-500 text-[10px] ml-1">*</span>{% endif
%}
</div>
</div>
{% endfor %}
</div>
{% if table.foreign_keys %}
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">References:</p>
@@ -161,71 +147,29 @@ erDiagram
</div>
</div>
<!-- Ace Editor -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.js"></script>
<!-- SQL Query Interface Script -->
<script>
// Initialize Ace Editor for SQL
const sqlEditor = ace.edit("sql-editor");
sqlEditor.setTheme("ace/theme/monokai");
sqlEditor.session.setMode("ace/mode/sql");
sqlEditor.setOptions({
fontSize: "14px",
showPrintMargin: false,
showGutter: true,
highlightActiveLine: true,
enableLiveAutocompletion: true
});
// Set default query
sqlEditor.setOptions({ fontSize: "14px", showPrintMargin: false, showGutter: true, highlightActiveLine: true, enableLiveAutocompletion: true });
sqlEditor.setValue(`SELECT * FROM http_functions`, -1);
// Run query button
document.getElementById('run-query-btn').addEventListener('click', async () => {
const query = sqlEditor.getValue().trim();
const messageDiv = document.getElementById('query-message');
const resultsDiv = document.getElementById('query-results');
const runBtn = document.getElementById('run-query-btn');
if (!query) {
showMessage('Please enter a SQL query', 'error');
return;
}
// Disable button and show loading
if (!query) { showMessage('Please enter a SQL query', 'error'); return; }
runBtn.disabled = true;
runBtn.innerHTML = '<svg class="w-4 h-4 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Running...';
try {
const response = await fetch('{{ url_for("settings.execute_query") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query })
});
const response = await fetch('{{ url_for("settings.execute_query") }}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) });
const data = await response.json();
if (!response.ok) {
showMessage(data.error || 'Query failed', 'error');
resultsDiv.classList.add('hidden');
} else {
showMessage(data.message, 'success');
displayResults(data);
}
} catch (error) {
showMessage('Error executing query: ' + error.message, 'error');
resultsDiv.classList.add('hidden');
} finally {
// Re-enable button
runBtn.disabled = false;
runBtn.innerHTML = '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>Run Query';
}
if (!response.ok) { showMessage(data.error || 'Query failed', 'error'); document.getElementById('query-results').classList.add('hidden'); }
else { showMessage(data.message, 'success'); displayResults(data); }
} catch (error) { showMessage('Error: ' + error.message, 'error'); document.getElementById('query-results').classList.add('hidden'); }
finally { runBtn.disabled = false; runBtn.innerHTML = '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>Run Query'; }
});
// Clear button
document.getElementById('clear-query-btn').addEventListener('click', () => {
sqlEditor.setValue('', -1);
document.getElementById('query-message').classList.add('hidden');
@@ -234,15 +178,9 @@ erDiagram
function showMessage(message, type) {
const messageDiv = document.getElementById('query-message');
messageDiv.classList.remove('hidden', 'bg-green-50', 'bg-red-50', 'text-green-700', 'text-red-700',
'dark:bg-green-900/20', 'dark:bg-red-900/20', 'dark:text-green-300', 'dark:text-red-300');
if (type === 'success') {
messageDiv.classList.add('bg-green-50', 'text-green-700', 'dark:bg-green-900/20', 'dark:text-green-300');
} else {
messageDiv.classList.add('bg-red-50', 'text-red-700', 'dark:bg-red-900/20', 'dark:text-red-300');
}
messageDiv.classList.remove('hidden', 'bg-green-50', 'bg-red-50', 'text-green-700', 'text-red-700', 'dark:bg-green-900/20', 'dark:bg-red-900/20', 'dark:text-green-300', 'dark:text-red-300');
if (type === 'success') messageDiv.classList.add('bg-green-50', 'text-green-700', 'dark:bg-green-900/20', 'dark:text-green-300');
else messageDiv.classList.add('bg-red-50', 'text-red-700', 'dark:bg-red-900/20', 'dark:text-red-300');
messageDiv.textContent = message;
}
@@ -250,25 +188,15 @@ erDiagram
const resultsDiv = document.getElementById('query-results');
const headerRow = document.getElementById('results-header');
const tbody = document.getElementById('results-body');
// Clear previous results
headerRow.innerHTML = '';
tbody.innerHTML = '';
if (data.row_count === 0) {
resultsDiv.classList.add('hidden');
return;
}
// Add column headers
if (data.row_count === 0) { resultsDiv.classList.add('hidden'); return; }
data.columns.forEach(col => {
const th = document.createElement('th');
th.className = 'px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider';
th.textContent = col;
headerRow.appendChild(th);
});
// Add rows
data.rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
@@ -279,48 +207,24 @@ erDiagram
});
tbody.appendChild(tr);
});
resultsDiv.classList.remove('hidden');
}
</script>
<!-- Mermaid Diagram Script -->
<script type="module">
// Dynamically import mermaid
const mermaid = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
// Initialize mermaid with theme
const isDark = document.documentElement.classList.contains('dark');
mermaid.default.initialize({
startOnLoad: false,
theme: isDark ? 'dark' : 'default',
er: {
fontSize: 14,
useMaxWidth: true
}
er: { layoutDirection: 'TB', fontSize: 12, useMaxWidth: true, minEntityWidth: 150, minEntityHeight: 60, entityPadding: 15, stroke: '#333', fill: '#f9f9f9', diagramPadding: 20 }
});
// Render the diagram
try {
await mermaid.default.run({
querySelector: '.mermaid'
});
await mermaid.default.run({ querySelector: '.mermaid' });
console.log('Mermaid diagram rendered successfully');
} catch (error) {
console.error('Mermaid rendering error:', error);
const diagramDiv = document.getElementById('mermaid-diagram');
if (diagramDiv) {
diagramDiv.innerHTML = `
<div class="text-amber-600 dark:text-amber-400 p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<p class="font-medium mb-2">Unable to render ERD</p>
<p class="text-sm">View the tables overview below for schema information.</p>
</div>
`;
}
console.error('Mermaid error:', error);
document.getElementById('mermaid-diagram').innerHTML = '<div class="text-amber-600 dark:text-amber-400 p-6 text-center"><svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg><p class="font-medium mb-2">Unable to render ERD</p><p class="text-sm">View the tables overview below for schema information.</p></div>';
}
</script>
{% endblock %}

View File

@@ -23,7 +23,8 @@
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Export Your Data</h1>
<p class="text-gray-600 dark:text-gray-400">Download all your data in JSON format for backup or migration
<p class="text-gray-600 dark:text-gray-400">Download all your data in JSON format for backup or
migration
purposes.</p>
</div>
@@ -38,7 +39,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">HTTP Functions</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">All your HTTP functions with code and settings
<p class="text-sm text-gray-600 dark:text-gray-400">All your HTTP functions with
code and settings
</p>
</div>
</div>
@@ -50,7 +52,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">Timer Functions</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">All scheduled functions and their configurations
<p class="text-sm text-gray-600 dark:text-gray-400">All scheduled functions and
their configurations
</p>
</div>
</div>
@@ -62,7 +65,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">Shared Environments</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Environment variables and configurations</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Environment variables and
configurations</p>
</div>
</div>
<div class="flex items-start space-x-3">
@@ -73,7 +77,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">API Keys</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">API key names and scopes (keys are masked)</p>
<p class="text-sm text-gray-600 dark:text-gray-400">API key names and scopes (keys
are masked)</p>
</div>
</div>
<div class="flex items-start space-x-3">
@@ -84,7 +89,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">Invocation Logs</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Last 100 invocations per function</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Last 100 invocations per
function</p>
</div>
</div>
<div class="flex items-start space-x-3">
@@ -95,7 +101,8 @@
</svg>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">Version History</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Complete version history for all functions</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Complete version history for all
functions</p>
</div>
</div>
</div>
@@ -109,10 +116,12 @@
clip-rule="evenodd" />
</svg>
<div>
<h3 class="font-medium text-yellow-900 dark:text-yellow-200 mb-1">Important Information</h3>
<h3 class="font-medium text-yellow-900 dark:text-yellow-200 mb-1">Important
Information</h3>
<ul class="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
<li>• The export file contains sensitive data. Store it securely.</li>
<li>• API keys are partially masked for security (only first 8 characters shown).</li>
<li>• API keys are partially masked for security (only first 8 characters
shown).</li>
<li>• The export is in JSON format for easy import/migration.</li>
<li>• File size may be large if you have many functions and invocations.</li>
</ul>
@@ -130,14 +139,131 @@
</a>
</div>
</div>
<!-- Import Data Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Import Your Data</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">Upload a previously exported JSON file to
restore your functions and environments.</p>
<form id="import-form" enctype="multipart/form-data">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Select
JSON Export File</label>
<input type="file" id="import-file-input" name="import_file" accept=".json"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white">
</div>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
<h3 class="font-medium text-blue-900 dark:text-blue-200 mb-2">Import Behavior</h3>
<ul class="text-sm text-blue-800 dark:text-blue-300 space-y-1">
<li>• Functions with duplicate names will be <strong>skipped</strong></li>
<li>• Successfully imported items will be listed in green</li>
<li>• Skipped items will be shown in yellow with reasons</li>
</ul>
</div>
<button type="submit" id="import-btn"
class="px-6 py-3 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<span>Import Data</span>
</button>
</form>
<div id="import-results" class="hidden mt-6">
<h3 class="font-medium text-gray-900 dark:text-white mb-3">Import Results</h3>
<div id="import-summary" class="grid grid-cols-3 gap-4 mb-4"></div>
<div id="import-details"></div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h3 class="font-medium text-gray-900 dark:text-white mb-2">What to Do With Your Export</h3>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><strong>Backup:</strong> Save the file in a secure location for disaster recovery</li>
<li><strong>Migration:</strong> Use the export to migrate to another instance or platform</li>
<li><strong>Migration:</strong> Use the export to migrate to another instance or platform
</li>
<li><strong>Analysis:</strong> Parse the JSON data for auditing or analytics purposes</li>
<li><strong>Version Control:</strong> Track changes to your functions over time</li>
</ul>
</div>
</div>
<script>
document.getElementById('import-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('import-file-input');
const importBtn = document.getElementById('import-btn');
const resultsDiv = document.getElementById('import-results');
if (!fileInput.files[0]) {
alert('Please select a file to import');
return;
}
importBtn.disabled = true;
const formData = new FormData();
formData.append('import_file', fileInput.files[0]);
try {
const response = await fetch('{{ url_for("settings.import_data") }}', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
alert('Import failed: ' + (data.error || 'Unknown error'));
return;
}
displayImportResults(data);
resultsDiv.classList.remove('hidden');
fileInput.value = '';
} catch (error) {
alert('Import error: ' + error.message);
} finally {
importBtn.disabled = false;
}
});
function displayImportResults(data) {
const summaryDiv = document.getElementById('import-summary');
const detailsDiv = document.getElementById('import-details');
summaryDiv.innerHTML = `
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">${data.summary.total_success}</div>
<div class="text-sm text-green-700 dark:text-green-300">Imported</div>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">${data.summary.total_skipped}</div>
<div class="text-sm text-yellow-700 dark:text-yellow-300">Skipped</div>
</div>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-red-600 dark:text-red-400">${data.summary.total_failed}</div>
<div class="text-sm text-red-700 dark:text-red-300">Failed</div>
</div>
`;
let detailsHTML = '';
for (const [type, results] of Object.entries(data.results)) {
if (results.success.length + results.skipped.length + results.failed.length > 0) {
const title = type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
detailsHTML += `<div class="mb-4"><h4 class="font-medium text-gray-900 dark:text-white mb-2">${title}</h4><div class="space-y-1">`;
results.success.forEach(msg => detailsHTML += `<div class="text-sm text-green-700 dark:text-green-300">✓ ${msg}</div>`);
results.skipped.forEach(msg => detailsHTML += `<div class="text-sm text-yellow-700 dark:text-yellow-300">⊘ ${msg}</div>`);
results.failed.forEach(msg => detailsHTML += `<div class="text-sm text-red-700 dark:text-red-300">✗ ${msg}</div>`);
detailsHTML += '</div></div>';
}
}
detailsDiv.innerHTML = detailsHTML || '<p class="text-sm text-gray-500 dark:text-gray-400">No items to import.</p>';
}
</script>
{% endblock %}