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() link['created_at'] = link['created_at'].isoformat()
export_data['timer_function_shared_env_links'] = timer_shared_env_links or [] 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: except Exception as e:
return {"error": f"Query execution failed: {str(e)}"}, 500 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 --> <!-- ERD Diagram -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6"> <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> <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Entity Relationship Diagram</h2>
<div id="mermaid-diagram" <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"> class="bg-white dark:bg-gray-900 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
<pre class="mermaid"> <pre class="mermaid">
@@ -54,32 +53,26 @@ erDiagram
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4"> <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. Run SELECT queries on your data. Queries are automatically scoped to your user account for security.
</p> </p>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SQL Query</label> <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 id="sql-editor" class="border border-gray-300 dark:border-gray-600 rounded" style="height: 150px;">
</div> </div>
</div> </div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<button id="run-query-btn" <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"> 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"> <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" <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" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
Run Query Run Query
</button> </button>
<button id="clear-query-btn" <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"> 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>
Clear
</button>
</div> </div>
<div id="query-message" class="hidden mb-4 p-3 rounded-lg"></div> <div id="query-message" class="hidden mb-4 p-3 rounded-lg"></div>
<div id="query-results" class="hidden"> <div id="query-results" class="hidden">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Results</h3> <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"> <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> <tr id="results-header"></tr>
</thead> </thead>
<tbody id="results-body" <tbody id="results-body"
class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@@ -98,7 +90,6 @@ erDiagram
<!-- Tables Overview --> <!-- Tables Overview -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <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> <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"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for table in schema_info %} {% for table in schema_info %}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"> <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> </h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3"> <p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
{{ table.columns|length }} column{{ 's' if table.columns|length != 1 else '' }} {{ table.columns|length }} column{{ 's' if table.columns|length != 1 else '' }}
{% if table.primary_keys %} {% if table.primary_keys %}· {{ table.primary_keys|length }} PK{% endif %}
· {{ table.primary_keys|length }} PK
{% endif %}
</p> </p>
<div class="space-y-1 max-h-40 overflow-y-auto"> <div class="space-y-1 max-h-40 overflow-y-auto">
{% for col in table.columns %} {% for col in table.columns %}
<div class="text-xs flex items-start py-1"> <div class="text-xs flex items-start py-1">
@@ -131,14 +119,12 @@ erDiagram
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<span class="font-medium text-gray-700 dark:text-gray-300">{{ col.column_name }}</span> <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> <span class="text-gray-500 dark:text-gray-500 ml-1">{{ col.data_type }}</span>
{% if col.is_nullable == 'NO' %} {% if col.is_nullable == 'NO' %}<span class="text-red-500 text-[10px] ml-1">*</span>{% endif
<span class="text-red-500 text-[10px] ml-1">*</span> %}
{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if table.foreign_keys %} {% if table.foreign_keys %}
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700"> <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> <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>
</div> </div>
<!-- Ace Editor -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.js"></script>
<!-- SQL Query Interface Script -->
<script> <script>
// Initialize Ace Editor for SQL
const sqlEditor = ace.edit("sql-editor"); const sqlEditor = ace.edit("sql-editor");
sqlEditor.setTheme("ace/theme/monokai"); sqlEditor.setTheme("ace/theme/monokai");
sqlEditor.session.setMode("ace/mode/sql"); sqlEditor.session.setMode("ace/mode/sql");
sqlEditor.setOptions({ sqlEditor.setOptions({ fontSize: "14px", showPrintMargin: false, showGutter: true, highlightActiveLine: true, enableLiveAutocompletion: true });
fontSize: "14px",
showPrintMargin: false,
showGutter: true,
highlightActiveLine: true,
enableLiveAutocompletion: true
});
// Set default query
sqlEditor.setValue(`SELECT * FROM http_functions`, -1); sqlEditor.setValue(`SELECT * FROM http_functions`, -1);
// Run query button
document.getElementById('run-query-btn').addEventListener('click', async () => { document.getElementById('run-query-btn').addEventListener('click', async () => {
const query = sqlEditor.getValue().trim(); const query = sqlEditor.getValue().trim();
const messageDiv = document.getElementById('query-message');
const resultsDiv = document.getElementById('query-results');
const runBtn = document.getElementById('run-query-btn'); const runBtn = document.getElementById('run-query-btn');
if (!query) { showMessage('Please enter a SQL query', 'error'); return; }
if (!query) {
showMessage('Please enter a SQL query', 'error');
return;
}
// Disable button and show loading
runBtn.disabled = true; 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...'; 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 { try {
const response = await fetch('{{ url_for("settings.execute_query") }}', { const response = await fetch('{{ url_for("settings.execute_query") }}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) });
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query })
});
const data = await response.json(); const data = await response.json();
if (!response.ok) { showMessage(data.error || 'Query failed', 'error'); document.getElementById('query-results').classList.add('hidden'); }
if (!response.ok) { else { showMessage(data.message, 'success'); displayResults(data); }
showMessage(data.error || 'Query failed', 'error'); } catch (error) { showMessage('Error: ' + error.message, 'error'); document.getElementById('query-results').classList.add('hidden'); }
resultsDiv.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'; }
} 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';
}
}); });
// Clear button
document.getElementById('clear-query-btn').addEventListener('click', () => { document.getElementById('clear-query-btn').addEventListener('click', () => {
sqlEditor.setValue('', -1); sqlEditor.setValue('', -1);
document.getElementById('query-message').classList.add('hidden'); document.getElementById('query-message').classList.add('hidden');
@@ -234,15 +178,9 @@ erDiagram
function showMessage(message, type) { function showMessage(message, type) {
const messageDiv = document.getElementById('query-message'); const messageDiv = document.getElementById('query-message');
messageDiv.classList.remove('hidden', 'bg-green-50', 'bg-red-50', 'text-green-700', 'text-red-700', 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');
'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');
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; messageDiv.textContent = message;
} }
@@ -250,25 +188,15 @@ erDiagram
const resultsDiv = document.getElementById('query-results'); const resultsDiv = document.getElementById('query-results');
const headerRow = document.getElementById('results-header'); const headerRow = document.getElementById('results-header');
const tbody = document.getElementById('results-body'); const tbody = document.getElementById('results-body');
// Clear previous results
headerRow.innerHTML = ''; headerRow.innerHTML = '';
tbody.innerHTML = ''; tbody.innerHTML = '';
if (data.row_count === 0) { resultsDiv.classList.add('hidden'); return; }
if (data.row_count === 0) {
resultsDiv.classList.add('hidden');
return;
}
// Add column headers
data.columns.forEach(col => { data.columns.forEach(col => {
const th = document.createElement('th'); 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.className = 'px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider';
th.textContent = col; th.textContent = col;
headerRow.appendChild(th); headerRow.appendChild(th);
}); });
// Add rows
data.rows.forEach(row => { data.rows.forEach(row => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
row.forEach(cell => { row.forEach(cell => {
@@ -279,48 +207,24 @@ erDiagram
}); });
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
resultsDiv.classList.remove('hidden'); resultsDiv.classList.remove('hidden');
} }
</script> </script>
<!-- Mermaid Diagram Script -->
<script type="module"> <script type="module">
// Dynamically import mermaid
const mermaid = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'); 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'); const isDark = document.documentElement.classList.contains('dark');
mermaid.default.initialize({ mermaid.default.initialize({
startOnLoad: false, startOnLoad: false,
theme: isDark ? 'dark' : 'default', theme: isDark ? 'dark' : 'default',
er: { er: { layoutDirection: 'TB', fontSize: 12, useMaxWidth: true, minEntityWidth: 150, minEntityHeight: 60, entityPadding: 15, stroke: '#333', fill: '#f9f9f9', diagramPadding: 20 }
fontSize: 14,
useMaxWidth: true
}
}); });
// Render the diagram
try { try {
await mermaid.default.run({ await mermaid.default.run({ querySelector: '.mermaid' });
querySelector: '.mermaid'
});
console.log('Mermaid diagram rendered successfully'); console.log('Mermaid diagram rendered successfully');
} catch (error) { } catch (error) {
console.error('Mermaid rendering error:', error); console.error('Mermaid error:', error);
const diagramDiv = document.getElementById('mermaid-diagram'); 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>';
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>
`;
}
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -23,7 +23,8 @@
<div class="mb-6"> <div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Export Your Data</h1> <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> purposes.</p>
</div> </div>
@@ -38,7 +39,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">HTTP Functions</h3> <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> </p>
</div> </div>
</div> </div>
@@ -50,7 +52,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">Timer Functions</h3> <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> </p>
</div> </div>
</div> </div>
@@ -62,7 +65,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">Shared Environments</h3> <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> </div>
<div class="flex items-start space-x-3"> <div class="flex items-start space-x-3">
@@ -73,7 +77,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">API Keys</h3> <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> </div>
<div class="flex items-start space-x-3"> <div class="flex items-start space-x-3">
@@ -84,7 +89,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">Invocation Logs</h3> <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> </div>
<div class="flex items-start space-x-3"> <div class="flex items-start space-x-3">
@@ -95,7 +101,8 @@
</svg> </svg>
<div> <div>
<h3 class="font-medium text-gray-900 dark:text-white">Version History</h3> <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> </div>
</div> </div>
@@ -109,10 +116,12 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
<div> <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"> <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>• 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>• 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> <li>• File size may be large if you have many functions and invocations.</li>
</ul> </ul>
@@ -130,14 +139,131 @@
</a> </a>
</div> </div>
</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"> <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> <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"> <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>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>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> <li><strong>Version Control:</strong> Track changes to your functions over time</li>
</ul> </ul>
</div> </div>
</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 %} {% endblock %}