mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +00:00
303 lines
14 KiB
HTML
303 lines
14 KiB
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">FHIR API Explorer</h1>
|
|
<div class="col-lg-6 mx-auto">
|
|
<p class="lead mb-4">
|
|
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local HAPI or a custom server to explore resources or perform searches.
|
|
</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('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
|
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR UI Operations</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mt-4">
|
|
<div class="card">
|
|
<div class="card-header"><i class="bi bi-cloud-arrow-down me-2"></i>Send FHIR Request</div>
|
|
<div class="card-body">
|
|
<form id="fhirRequestForm" onsubmit="return false;">
|
|
{{ form.hidden_tag() }}
|
|
<div class="mb-3">
|
|
<label class="form-label">FHIR Server</label>
|
|
<div class="input-group">
|
|
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
|
<span id="toggleLabel">Use Local HAPI</span>
|
|
</button>
|
|
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
|
</div>
|
|
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="fhirPath" class="form-label">FHIR Path</label>
|
|
<input type="text" class="form-control" id="fhirPath" name="fhir_path" placeholder="e.g., Patient/wang-li" required aria-describedby="fhirPathHelp">
|
|
<small id="fhirPathHelp" class="form-text text-muted">Enter a resource path (e.g., Patient, Observation/example) or '_search' for search queries.</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Request Type</label>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<input type="radio" class="btn-check" name="method" id="get" value="GET" checked>
|
|
<label class="btn btn-outline-success" for="get"><span class="badge bg-success">GET</span></label>
|
|
|
|
<input type="radio" class="btn-check" name="method" id="post" value="POST">
|
|
<label class="btn btn-outline-primary" for="post"><span class="badge bg-primary">POST</span></label>
|
|
|
|
<input type="radio" class="btn-check" name="method" id="put" value="PUT">
|
|
<label class="btn btn-outline-warning" for="put"><span class="badge bg-warning text-dark">PUT</span></label>
|
|
|
|
<input type="radio" class="btn-check" name="method" id="delete" value="DELETE">
|
|
<label class="btn btn-outline-danger" for="delete"><span class="badge bg-danger">DELETE</span></label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3" id="requestBodyGroup" style="display: none;">
|
|
<div class="d-flex align-items-center mb-2">
|
|
<label for="requestBody" class="form-label me-2 mb-0">Request Body</label>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyRequestBody" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
|
</div>
|
|
<textarea class="form-control" id="requestBody" name="request_body" rows="6" placeholder="For POST: JSON resource (e.g., {'resourceType': 'Patient', ...}) or search params (e.g., name=John&birthdate=gt2000)"></textarea>
|
|
<div id="jsonError" class="text-danger mt-1" style="display: none;"></div>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" id="sendRequest">Send Request</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-4" id="responseCard" style="display: none;">
|
|
<div class="card-header">Response</div>
|
|
<div class="card-body">
|
|
<h5>Status: <span id="responseStatus" class="badge"></span></h5>
|
|
<div class="d-flex align-items-center mb-2">
|
|
<h6 class="me-2 mb-0">Headers</h6>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyResponseHeaders" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
|
</div>
|
|
<pre id="responseHeaders" class="border p-2 bg-light mb-3" style="max-height: 200px; overflow-y: auto;"></pre>
|
|
<div class="d-flex align-items-center mb-2">
|
|
<h6 class="me-2 mb-0">Body</h6>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyResponseBody" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
|
</div>
|
|
<pre id="responseBody" class="border p-2 bg-light" style="max-height: 400px; overflow-y: auto;"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const form = document.getElementById('fhirRequestForm');
|
|
const sendButton = document.getElementById('sendRequest');
|
|
const fhirPath = document.getElementById('fhirPath');
|
|
const requestBody = document.getElementById('requestBody');
|
|
const requestBodyGroup = document.getElementById('requestBodyGroup');
|
|
const jsonError = document.getElementById('jsonError');
|
|
const responseCard = document.getElementById('responseCard');
|
|
const responseStatus = document.getElementById('responseStatus');
|
|
const responseHeaders = document.getElementById('responseHeaders');
|
|
const responseBody = document.getElementById('responseBody');
|
|
const methodRadios = document.getElementsByName('method');
|
|
const toggleServerButton = document.getElementById('toggleServer');
|
|
const toggleLabel = document.getElementById('toggleLabel');
|
|
const fhirServerUrl = document.getElementById('fhirServerUrl');
|
|
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
|
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
|
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
|
let useLocalHapi = true;
|
|
|
|
// Show/hide request body
|
|
function updateRequestBodyVisibility() {
|
|
const selectedMethod = document.querySelector('input[name="method"]:checked').value;
|
|
requestBodyGroup.style.display = (selectedMethod === 'POST' || selectedMethod === 'PUT') ? 'block' : 'none';
|
|
if (selectedMethod !== 'POST' && selectedMethod !== 'PUT') {
|
|
requestBody.value = '';
|
|
jsonError.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Validate request body (JSON or form-encoded)
|
|
function validateRequestBody(method, path) {
|
|
if (!requestBody.value.trim()) {
|
|
requestBody.classList.remove('is-invalid');
|
|
jsonError.style.display = 'none';
|
|
return method === 'GET' ? null : '';
|
|
}
|
|
const isSearch = path.endsWith('_search');
|
|
if (method === 'POST' && isSearch) {
|
|
return requestBody.value; // Allow form-encoded for _search
|
|
}
|
|
try {
|
|
JSON.parse(requestBody.value);
|
|
requestBody.classList.remove('is-invalid');
|
|
jsonError.style.display = 'none';
|
|
return requestBody.value;
|
|
} catch (e) {
|
|
requestBody.classList.add('is-invalid');
|
|
jsonError.textContent = `Invalid JSON: ${e.message}`;
|
|
jsonError.style.display = 'block';
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Clean FHIR path
|
|
function cleanFhirPath(path) {
|
|
const cleaned = path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
|
console.log(`Cleaned path: ${path} -> ${cleaned}`);
|
|
return cleaned;
|
|
}
|
|
|
|
// Toggle server
|
|
function toggleServer() {
|
|
useLocalHapi = !useLocalHapi;
|
|
toggleLabel.textContent = useLocalHapi ? 'Use Local HAPI' : 'Use Custom URL';
|
|
fhirServerUrl.style.display = useLocalHapi ? 'none' : 'block';
|
|
fhirServerUrl.value = '';
|
|
console.log(`Server toggled: ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
|
}
|
|
|
|
// Copy to clipboard
|
|
async function copyToClipboard(text, button) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
button.innerHTML = '<i class="bi bi-check"></i>';
|
|
setTimeout(() => {
|
|
button.innerHTML = '<i class="bi bi-clipboard"></i>';
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Copy failed:', err);
|
|
alert('Failed to copy to clipboard');
|
|
}
|
|
}
|
|
|
|
// Bind toggle event
|
|
toggleServerButton.addEventListener('click', toggleServer);
|
|
|
|
// Bind copy events
|
|
copyRequestBodyButton.addEventListener('click', () => {
|
|
copyToClipboard(requestBody.value, copyRequestBodyButton);
|
|
});
|
|
|
|
copyResponseHeadersButton.addEventListener('click', () => {
|
|
copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton);
|
|
});
|
|
|
|
copyResponseBodyButton.addEventListener('click', () => {
|
|
copyToClipboard(responseBody.textContent, copyResponseBodyButton);
|
|
});
|
|
|
|
// Update visibility on method change
|
|
methodRadios.forEach(radio => {
|
|
radio.addEventListener('change', updateRequestBodyVisibility);
|
|
});
|
|
|
|
// Validate body on input
|
|
requestBody.addEventListener('input', () => validateRequestBody(
|
|
document.querySelector('input[name="method"]:checked').value,
|
|
fhirPath.value
|
|
));
|
|
|
|
// Send request
|
|
sendButton.addEventListener('click', async function() {
|
|
if (!fhirPath.value.trim()) {
|
|
fhirPath.classList.add('is-invalid');
|
|
console.log('Path validation failed: empty');
|
|
return;
|
|
}
|
|
fhirPath.classList.remove('is-invalid');
|
|
|
|
const method = document.querySelector('input[name="method"]:checked').value;
|
|
console.log(`Sending request: Method=${method}, Input Path=${fhirPath.value}, Server=${useLocalHapi ? 'Local HAPI' : fhirServerUrl.value}`);
|
|
|
|
const data = validateRequestBody(method, fhirPath.value);
|
|
if ((method === 'POST' || method === 'PUT') && data === null) {
|
|
console.log('Body validation failed');
|
|
sendButton.disabled = false;
|
|
sendButton.textContent = 'Send Request';
|
|
return;
|
|
}
|
|
|
|
sendButton.disabled = true;
|
|
sendButton.textContent = 'Sending...';
|
|
responseCard.style.display = 'none';
|
|
|
|
const cleanPath = cleanFhirPath(fhirPath.value);
|
|
let fetchUrl, headers = {
|
|
'Accept': 'application/fhir+json'
|
|
};
|
|
|
|
if (useLocalHapi) {
|
|
fetchUrl = `/fhir/${cleanPath}`;
|
|
if (method === 'POST' || method === 'PUT') {
|
|
headers['Content-Type'] = cleanPath.endsWith('_search') ? 'application/x-www-form-urlencoded' : 'application/fhir+json';
|
|
headers['X-CSRFToken'] = '{{ form.csrf_token._value() }}';
|
|
}
|
|
} else {
|
|
if (!fhirServerUrl.value.trim()) {
|
|
fhirServerUrl.classList.add('is-invalid');
|
|
console.log('Server URL validation failed: empty');
|
|
sendButton.disabled = false;
|
|
sendButton.textContent = 'Send Request';
|
|
return;
|
|
}
|
|
fhirServerUrl.classList.remove('is-invalid');
|
|
fetchUrl = `${fhirServerUrl.value.replace(/\/+$/, '')}/${cleanPath}`;
|
|
if (method === 'POST' || method === 'PUT') {
|
|
headers['Content-Type'] = cleanPath.endsWith('_search') ? 'application/x-www-form-urlencoded' : 'application/fhir+json';
|
|
}
|
|
}
|
|
|
|
console.log('Fetch details:', {
|
|
url: fetchUrl,
|
|
method,
|
|
headers,
|
|
body: data ? data.substring(0, 100) + '...' : null
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(fetchUrl, {
|
|
method: method,
|
|
headers: headers,
|
|
body: method === 'GET' ? undefined : data
|
|
});
|
|
console.log('Response status:', response.status, response.statusText);
|
|
|
|
const responseHeadersObj = {};
|
|
response.headers.forEach((value, key) => responseHeadersObj[key] = value);
|
|
const contentType = responseHeadersObj['content-type'] || '';
|
|
const text = await response.text();
|
|
console.log('Response headers:', responseHeadersObj);
|
|
console.log('Response body (first 200 chars):', text.substring(0, 200));
|
|
|
|
responseCard.style.display = 'block';
|
|
responseStatus.textContent = `${response.status} ${response.statusText}`;
|
|
responseStatus.className = `badge ${response.status < 300 ? 'bg-success' : response.status < 400 ? 'bg-info' : 'bg-danger'}`;
|
|
responseHeaders.textContent = JSON.stringify(responseHeadersObj, null, 2);
|
|
|
|
if (contentType.includes('application/fhir+json') || contentType.includes('application/json')) {
|
|
try {
|
|
responseBody.textContent = JSON.stringify(JSON.parse(text), null, 2);
|
|
} catch (e) {
|
|
console.warn('Failed to parse JSON:', e.message);
|
|
responseBody.textContent = text;
|
|
}
|
|
} else {
|
|
responseBody.textContent = text;
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch error:', error.name, error.message, error.stack);
|
|
responseCard.style.display = 'block';
|
|
responseStatus.textContent = 'Error';
|
|
responseStatus.className = 'badge bg-danger';
|
|
responseHeaders.textContent = '';
|
|
responseBody.textContent = `Error: ${error.message}`;
|
|
} finally {
|
|
sendButton.disabled = false;
|
|
sendButton.textContent = 'Send Request';
|
|
}
|
|
});
|
|
|
|
// Initialize visibility
|
|
updateRequestBodyVisibility();
|
|
});
|
|
</script>
|
|
{% endblock %} |