FHIRFLARE-IG-Toolkit/templates/upload_test_data.html
Sudo-JHare fa091db463 QOL Changes
added Basic Auth options for custom server  usage across multiple modules.

new footer option - OpenAPI docs - integrated swagger UI for open API documentation
2025-05-22 20:07:11 +10:00

302 lines
18 KiB
HTML

{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
<h1 class="display-5 fw-bold text-body-emphasis">{{ title }}</h1>
<div class="col-lg-8 mx-auto">
<p class="lead mb-4">
Upload FHIR test data (JSON, XML, or ZIP containing JSON/XML) to a target server. The tool will attempt to parse resources, determine dependencies based on references, and upload them in the correct order. Optionally validate resources before uploading.
</p>
</div>
</div>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-cloud-upload me-2"></i>Upload Configuration</h4>
</div>
<div class="card-body">
{% if form.errors %}
<div class="alert alert-danger">
<p><strong>Please correct the following errors:</strong></p>
<ul>
{% for field, errors in form.errors.items() %}
<li>{{ form[field].label.text }}: {{ errors|join(', ') }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form id="uploadTestDataForm" method="POST" enctype="multipart/form-data">
{{ form.csrf_token }}
{{ render_field(form.fhir_server_url, class="form-control form-control-lg") }}
<div class="row g-3 mb-3 align-items-end">
<div class="col-md-5">
{{ render_field(form.auth_type, class="form-select") }}
</div>
<div class="col-md-7" id="authInputsGroup" style="display: none;">
<div id="bearerTokenInput" style="display: none;">
{{ render_field(form.auth_token, class="form-control") }}
</div>
<div id="basicAuthInputs" style="display: none;">
{{ render_field(form.username, class="form-control mb-2", placeholder="Username") }}
{{ render_field(form.password, class="form-control", placeholder="Password", type="password") }}
</div>
</div>
</div>
{{ render_field(form.test_data_file, class="form-control") }}
<small class="form-text text-muted">Select one or more .json, .xml files, or a single .zip file containing them.</small>
<div class="row g-3 mt-3 mb-3 align-items-center">
<div class="col-md-6">
{{ render_field(form.validate_before_upload) }}
<small class="form-text text-muted">It is suggested to not validate against more than 500 files</small>
</div>
<div class="col-md-6" id="validationPackageGroup" style="display: none;">
{{ render_field(form.validation_package_id, class="form-select") }}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
{{ render_field(form.upload_mode, class="form-select") }}
</div>
<div class="col-md-4 d-flex align-items-end">
{{ render_field(form.use_conditional_uploads) }}
</div>
<div class="col-md-4">
{{ render_field(form.error_handling, class="form-select") }}
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="uploadButton">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
<i class="bi bi-arrow-up-circle me-2"></i>Upload and Process
</button>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-terminal me-2"></i>Processing Log & Results</h4>
</div>
<div class="card-body">
<div id="liveConsole" class="border p-3 rounded bg-dark text-light mb-3" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
<span class="text-muted">Processing output will appear here...</span>
</div>
<div id="uploadResponse" class="mt-3">
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('uploadTestDataForm');
const uploadButton = document.getElementById('uploadButton');
const spinner = uploadButton ? uploadButton.querySelector('.spinner-border') : null;
const liveConsole = document.getElementById('liveConsole');
const responseDiv = document.getElementById('uploadResponse');
const authTypeSelect = document.getElementById('auth_type');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const authTokenInput = document.getElementById('auth_token');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const fileInput = document.getElementById('test_data_file');
const validateCheckbox = document.getElementById('validate_before_upload');
const validationPackageGroup = document.getElementById('validationPackageGroup');
const validationPackageSelect = document.getElementById('validation_package_id');
const uploadModeSelect = document.getElementById('upload_mode');
const conditionalUploadCheckbox = document.getElementById('use_conditional_uploads');
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
if (authTypeSelect && authInputsGroup && bearerTokenInput && basicAuthInputs) {
authTypeSelect.addEventListener('change', function() {
authInputsGroup.style.display = (this.value === 'bearerToken' || this.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = this.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = this.value === 'basic' ? 'block' : 'none';
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
if (this.value !== 'basic' && usernameInput) usernameInput.value = '';
if (this.value !== 'basic' && passwordInput) passwordInput.value = '';
});
authInputsGroup.style.display = (authTypeSelect.value === 'bearerToken' || authTypeSelect.value === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
basicAuthInputs.style.display = authTypeSelect.value === 'basic' ? 'block' : 'none';
} else {
console.error("Auth elements not found.");
}
if (validateCheckbox && validationPackageGroup) {
const toggleValidationPackage = () => {
validationPackageGroup.style.display = validateCheckbox.checked ? 'block' : 'none';
};
validateCheckbox.addEventListener('change', toggleValidationPackage);
toggleValidationPackage();
} else {
console.error("Validation checkbox or package group not found.");
}
if (uploadModeSelect && conditionalUploadCheckbox) {
const toggleConditionalCheckbox = () => {
conditionalUploadCheckbox.disabled = (uploadModeSelect.value !== 'individual');
if (conditionalUploadCheckbox.disabled) {
conditionalUploadCheckbox.checked = false;
}
};
uploadModeSelect.addEventListener('change', toggleConditionalCheckbox);
toggleConditionalCheckbox();
} else {
console.error("Upload mode select or conditional upload checkbox not found.");
}
if (form && uploadButton && spinner && liveConsole && responseDiv && fileInput && validateCheckbox && validationPackageSelect && conditionalUploadCheckbox) {
form.addEventListener('submit', async function(event) {
event.preventDefault();
console.log("Form submitted");
if (!fileInput.files || fileInput.files.length === 0) { alert('Please select at least one file.'); return; }
const fhirServerUrl = document.getElementById('fhir_server_url').value.trim();
if (!fhirServerUrl) { alert('Please enter the Target FHIR Server URL.'); return; }
if (validateCheckbox.checked && !validationPackageSelect.value) { alert('Please select a package for validation.'); return; }
if (authTypeSelect.value === 'basic') {
if (!usernameInput.value.trim()) { alert('Please enter a username for Basic Authentication.'); return; }
if (!passwordInput.value) { alert('Please enter a password for Basic Authentication.'); return; }
}
uploadButton.disabled = true;
if(spinner) spinner.style.display = 'inline-block';
const uploadIcon = uploadButton.querySelector('i');
if (uploadIcon) uploadIcon.style.display = 'none';
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting upload process...</div>`;
responseDiv.innerHTML = '';
const formData = new FormData();
formData.append('fhir_server_url', fhirServerUrl);
formData.append('auth_type', authTypeSelect.value);
if (authTypeSelect.value === 'bearerToken' && authTokenInput) {
formData.append('auth_token', authTokenInput.value);
} else if (authTypeSelect.value === 'basic') {
formData.append('username', usernameInput.value);
formData.append('password', passwordInput.value);
}
formData.append('upload_mode', uploadModeSelect.value);
formData.append('error_handling', document.getElementById('error_handling').value);
formData.append('validate_before_upload', validateCheckbox.checked ? 'true' : 'false');
if (validateCheckbox.checked) { formData.append('validation_package_id', validationPackageSelect.value); }
formData.append('use_conditional_uploads', conditionalUploadCheckbox.checked ? 'true' : 'false');
for (let i = 0; i < fileInput.files.length; i++) {
formData.append('test_data_files', fileInput.files[i]);
}
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : "";
const internalApiKey = {{ api_key | default("") | tojson }};
try {
const response = await fetch('/api/upload-test-data', {
method: 'POST',
headers: { 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
body: formData
});
if (!response.ok) {
let errorMsg = `Upload failed: ${response.status} ${response.statusText}`;
try { const errorData = await response.json(); errorMsg = errorData.message || JSON.stringify(errorData); } catch (e) {}
throw new Error(errorMsg);
}
if (!response.body) { throw new Error("Response body missing."); }
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
let messageClass = 'text-info'; let prefix = '[INFO]';
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
else if (data.type === 'validation_info') { prefix = '[VALIDATION]'; messageClass = 'text-info'; }
else if (data.type === 'validation_warning') { prefix = '[VALIDATION]'; messageClass = 'text-warning'; }
else if (data.type === 'validation_error') { prefix = '[VALIDATION]'; messageClass = 'text-danger'; }
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
let messageText = sanitizeText(data.message) || '...';
if (data.details) { messageText += ` <small>(${sanitizeText(data.details)})</small>`; }
messageDiv.innerHTML = `${timestamp} ${prefix} ${messageText}`;
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
if (data.type === 'complete' && data.data) {
const summary = data.data;
let alertClass = 'alert-secondary';
if (summary.status === 'success') alertClass = 'alert-success';
else if (summary.status === 'partial') alertClass = 'alert-warning';
else if (summary.status === 'failure') alertClass = 'alert-danger';
responseDiv.innerHTML = `
<div class="alert ${alertClass} mt-3">
<strong>Status: ${sanitizeText(summary.status)}</strong><br>
${sanitizeText(summary.message)}<hr>
Files Processed: ${summary.files_processed ?? 'N/A'}<br>
Resources Parsed: ${summary.resources_parsed ?? 'N/A'}<br>
Validation Errors: ${summary.validation_errors ?? 'N/A'}<br>
Validation Warnings: ${summary.validation_warnings ?? 'N/A'}<br>
Resources Uploaded: ${summary.resources_uploaded ?? 'N/A'}<br>
Upload Errors: ${summary.error_count ?? 'N/A'}
${summary.errors?.length > 0 ? '<br><strong>Details:</strong><ul>' + summary.errors.map(e => `<li>${sanitizeText(e)}</li>`).join('') + '</ul>' : ''}
</div>`;
}
} catch (parseError) { console.error('Stream parse error:', parseError, 'Line:', line); }
}
}
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
if (data.type === 'complete' && data.data) { /* ... update summary ... */ }
} catch (parseError) { console.error('Final buffer parse error:', parseError); }
}
} catch (error) {
console.error("Upload failed:", error);
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message)}</div>`;
const ts = new Date().toLocaleTimeString(); const errDiv = document.createElement('div'); errDiv.className = 'text-danger'; errDiv.textContent = `${ts} [CLIENT_ERROR] ${sanitizeText(error.message)}`; liveConsole.appendChild(errDiv);
} finally {
uploadButton.disabled = false;
if(spinner) spinner.style.display = 'none';
const uploadIcon = uploadButton.querySelector('i');
if (uploadIcon) uploadIcon.style.display = 'inline-block';
}
});
} else { console.error("Could not find all required elements for form submission."); }
});
</script>
{% endblock %}