mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +00:00
added Basic Auth options for custom server usage across multiple modules. new footer option - OpenAPI docs - integrated swagger UI for open API documentation
302 lines
18 KiB
HTML
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, "<").replace(/>/g, ">") : "";
|
|
|
|
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 %} |