FHIRFLARE-IG-Toolkit/templates/validate_sample.html
Sudo-JHare 8e769bd126 incremental fixes
operation UI fixes, CRSF token handling for security internally
Fhirpath added to IG processing view
explicit removal of narrative views in the ig viewer
changes to enforce hapi server config is read.
added Prism for code highlighting in the Base and ui operations
2025-04-20 09:44:38 +10:00

374 lines
18 KiB
HTML

<!-- templates/validate_sample.html -->
{% extends "base.html" %}
{% 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">Validate FHIR Sample</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
</div>
</div>
</div>
<div class="container mt-4">
<div class="card">
<div class="card-header">Validation Form</div>
<div class="card-body">
<form id="validationForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<small class="form-text text-muted">
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
</small>
<div class="mt-2">
<select class="form-select" id="packageSelect">
<option value="">-- Select a Package --</option>
{% if packages %}
{% for pkg in packages %}
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
{% endfor %}
{% else %}
<option value="" disabled>No packages available</option>
{% endif %}
</select>
</div>
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
{{ form.package_name(class="form-control", id=form.package_name.id) }}
{% for error in form.package_name.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
{{ form.version(class="form-control", id=form.version.id) }}
{% for error in form.version.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
<div class="form-check">
{{ form.include_dependencies(class="form-check-input") }}
{{ form.include_dependencies.label(class="form-check-label") }}
</div>
</div>
<div class="mb-3">
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
{{ form.mode(class="form-select") }}
</div>
<div class="mb-3">
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
{% for error in form.sample_input.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
</form>
</div>
</div>
<div class="card mt-4" id="validationResult" style="display: none;">
<div class="card-header">Validation Report</div>
<div class="card-body" id="validationContent">
<!-- Results will be populated dynamically -->
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const packageSelect = document.getElementById('packageSelect');
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
const versionInput = document.getElementById('{{ form.version.id }}');
const validateButton = document.getElementById('validateButton');
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
const validationResult = document.getElementById('validationResult');
const validationContent = document.getElementById('validationContent');
// Debug DOM elements
console.log('Package Select:', packageSelect ? 'Found' : 'Missing');
console.log('Package Name Input:', packageNameInput ? 'Found' : 'Missing');
console.log('Version Input:', versionInput ? 'Found' : 'Missing');
// Update package_name and version fields from dropdown
function updatePackageFields(select) {
const value = select.value;
console.log('Selected package:', value);
if (!packageNameInput || !versionInput) {
console.error('Input fields not found');
return;
}
if (value) {
const [name, version] = value.split('#');
if (name && version) {
packageNameInput.value = name;
versionInput.value = version;
console.log('Updated fields:', { packageName: name, version });
} else {
console.warn('Invalid package format:', value);
packageNameInput.value = '';
versionInput.value = '';
}
} else {
packageNameInput.value = '';
versionInput.value = '';
console.log('Cleared fields: no package selected');
}
}
// Initialize dropdown based on form values
if (packageNameInput && versionInput && packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
if (packageSelect.querySelector(`option[value="${currentValue}"]`)) {
packageSelect.value = currentValue;
console.log('Initialized dropdown with:', currentValue);
}
}
// Debug dropdown options
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => opt.value));
// Attach event listener for package selection
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
// Client-side JSON validation preview
if (sampleInput) {
sampleInput.addEventListener('input', function() {
try {
if (this.value.trim()) {
JSON.parse(this.value);
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid', 'is-invalid');
}
} catch (e) {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
});
}
// Check HAPI server status
function checkHapiStatus() {
return fetch('/fhir/metadata', { method: 'GET' })
.then(response => {
if (!response.ok) {
throw new Error('HAPI server unavailable');
}
return true;
})
.catch(error => {
console.warn('HAPI status check failed:', error.message);
validationContent.innerHTML = '<div class="alert alert-warning">HAPI server unavailable; using local validation.</div>';
return false;
});
}
// Sanitize text to prevent XSS
function sanitizeText(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Validate button handler
if (validateButton) {
validateButton.addEventListener('click', function() {
const packageName = packageNameInput.value;
const version = versionInput.value;
const sampleData = sampleInput.value.trim();
const includeDependencies = document.getElementById('{{ form.include_dependencies.id }}').checked;
if (!packageName || !version) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please select a package and version.</div>';
return;
}
if (!sampleData) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
return;
}
try {
JSON.parse(sampleData);
} catch (e) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
return;
}
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="text-center py-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Validating...</span></div></div>';
// Check HAPI status before validation
checkHapiStatus().then(() => {
console.log('Sending request to /api/validate-sample with:', {
package_name: packageName,
version: version,
include_dependencies: includeDependencies,
sample_data_length: sampleData.length
});
fetch('/api/validate-sample', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ form.csrf_token._value() }}'
},
body: JSON.stringify({
package_name: packageName,
version: version,
include_dependencies: includeDependencies,
sample_data: sampleData
})
})
.then(response => {
console.log('Server response status:', response.status, response.statusText);
if (!response.ok) {
return response.text().then(text => {
console.error('Server response text:', text.substring(0, 200) + (text.length > 200 ? '...' : ''));
throw new Error(`HTTP error ${response.status}: ${text.substring(0, 100)}...`);
});
}
return response.json();
})
.then(data => {
console.log('Validation response:', data);
validationContent.innerHTML = '';
let hasContent = false;
// Display profile information
if (data.results) {
const profileDiv = document.createElement('div');
profileDiv.className = 'mb-3';
const profiles = new Set();
Object.values(data.results).forEach(result => {
if (result.profile) profiles.add(result.profile);
});
if (profiles.size > 0) {
profileDiv.innerHTML = `
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
<ul>
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
</ul>
`;
validationContent.appendChild(profileDiv);
hasContent = true;
}
}
// Display errors
if (data.errors && data.errors.length > 0) {
hasContent = true;
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger';
errorDiv.innerHTML = `
<h5>Errors</h5>
<ul>
${data.errors.map(e => `<li>${sanitizeText(typeof e === 'string' ? e : e.message)}</li>`).join('')}
</ul>
`;
validationContent.appendChild(errorDiv);
}
// Display warnings
if (data.warnings && data.warnings.length > 0) {
hasContent = true;
const warningDiv = document.createElement('div');
warningDiv.className = 'alert alert-warning';
warningDiv.innerHTML = `
<h5>Warnings</h5>
<ul>
${data.warnings.map(w => {
const isMustSupport = w.includes('Must Support');
return `<li>${sanitizeText(typeof w === 'string' ? w : w.message)}${isMustSupport ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}</li>`;
}).join('')}
</ul>
`;
validationContent.appendChild(warningDiv);
}
// Detailed results with collapsible sections
if (data.results) {
hasContent = true;
const resultsDiv = document.createElement('div');
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
let index = 0;
for (const [resourceId, result] of Object.entries(data.results)) {
const collapseId = `result-collapse-${index++}`;
resultsDiv.innerHTML += `
<div class="card mb-2">
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
</div>
<div id="${collapseId}" class="collapse">
<div class="card-body">
${result.details && result.details.length > 0 ? `
<ul>
${result.details.map(d => `
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
${sanitizeText(d.issue)}
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
</li>
`).join('')}
</ul>
` : '<p>No additional details.</p>'}
</div>
</div>
</div>
`;
}
validationContent.appendChild(resultsDiv);
}
// Summary
const summaryDiv = document.createElement('div');
summaryDiv.className = 'alert alert-info';
summaryDiv.innerHTML = `
<h5>Summary</h5>
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
`;
validationContent.appendChild(summaryDiv);
hasContent = true;
if (!hasContent && data.valid) {
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
}
// Initialize Bootstrap collapse and tooltips
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
el.addEventListener('click', () => {
const target = document.querySelector(el.getAttribute('data-bs-target'));
target.classList.toggle('show');
});
});
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
new bootstrap.Tooltip(el);
});
})
.catch(error => {
console.error('Validation error:', error);
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
});
});
});
}
});
</script>
{% endblock %}