Add functionality in settings to import data
This commit is contained in:
120
db.py
120
db.py
@@ -428,3 +428,123 @@ ORDER BY invocation_time DESC""", [http_function_id])
|
|||||||
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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user