mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 00:40:00 +00:00
update major implementation v.0.05
This commit is contained in:
parent
9fba23c83b
commit
753cc4e6c1
@ -11,6 +11,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py .
|
||||
COPY services.py .
|
||||
COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
|
||||
# Ensure /tmp is writable as a fallback
|
||||
RUN mkdir -p /tmp && chmod 777 /tmp
|
||||
|
91
Dump/base.html.txt
Normal file
91
Dump/base.html.txt
Normal file
@ -0,0 +1,91 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('core.index') }}">{{ site_name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
{# FIX: Added check for request.endpoint #}
|
||||
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == 'core.index' else '' }}" aria-current="page" href="{{ url_for('core.index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
|
||||
</li>
|
||||
{% if module_menu_items %}
|
||||
{% for item in module_menu_items %}
|
||||
<li class="nav-item">
|
||||
{# FIX: Added check for request.endpoint #}
|
||||
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == item.endpoint else '' }}" href="{{ url_for(item.endpoint) }}">
|
||||
<i class="{{ item.icon | default('bi bi-box-seam me-1') }}"></i> {{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.role == 'admin' %}
|
||||
<li class="nav-item">
|
||||
{# FIX: Added check for request.endpoint #}
|
||||
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint.startswith('control_panel.') else '' }}" href="{{ url_for('control_panel.index') }}">
|
||||
<i class="bi bi-gear me-1"></i> Control Panel
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><span class="navbar-text me-2"><i class="bi bi-person-circle me-1"></i> {{ current_user.username }}</span></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.logout') }}"><i class="bi bi-box-arrow-right me-1"></i> Logout</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{# FIX: Added check for request.endpoint #}
|
||||
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == 'auth.login' else '' }}" href="{{ url_for('auth.login') }}"><i class="bi bi-box-arrow-in-right me-1"></i> Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-3 bg-light">
|
||||
<div class="container text-center">
|
||||
{% set current_year = now.year %} <span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
|
||||
{% block scripts %}
|
||||
{# Tooltip Initialization Script #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
16
Dump/index.html.txt
Normal file
16
Dump/index.html.txt
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %} {% block content %} <div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="https://placehold.co/72x57/0d6efd/white?text=PAS" alt="PAS Logo Placeholder" width="72" height="57">
|
||||
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
This is the starting point for our modular Patient Administration System.
|
||||
We will build upon this core framework by adding modules through the control panel.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<button type="button" class="btn btn-primary btn-lg px-4 gap-3">Primary button</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-lg px-4">Secondary</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
74
app.py
74
app.py
@ -65,26 +65,7 @@ class ProcessedIg(db.Model):
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template_string('''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FHIR IG Toolkit</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { text-align: center; padding: 50px; }
|
||||
.button { padding: 15px 30px; margin: 10px; font-size: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>FHIR IG Toolkit</h1>
|
||||
<p>Simple tool for importing and viewing FHIR Implementation Guides.</p>
|
||||
<a href="{{ url_for('import_ig') }}"><button class="button btn btn-primary">Import FHIR IG</button></a>
|
||||
<a href="{{ url_for('view_igs') }}"><button class="button btn btn-primary">View Downloaded IGs</button></a>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
return render_template('index.html', site_name='FHIRFLARE IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/import-ig', methods=['GET', 'POST'])
|
||||
def import_ig():
|
||||
@ -101,47 +82,7 @@ def import_ig():
|
||||
return redirect(url_for('view_igs'))
|
||||
except Exception as e:
|
||||
flash(f"Error downloading IG: {str(e)}", "error")
|
||||
return render_template_string('''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Import FHIR IG</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.form { max-width: 400px; margin: 20px auto; }
|
||||
.message { margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="mt-4">Import FHIR IG</h1>
|
||||
<form class="form" method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.package_name.label(class="form-label") }}
|
||||
{{ form.package_name(class="form-control") }}
|
||||
{% for error in form.package_name.errors %}<p class="text-danger message">{{ error }}</p>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.package_version.label(class="form-label") }}
|
||||
{{ form.package_version(class="form-control") }}
|
||||
{% for error in form.package_version.errors %}<p class="text-danger message">{{ error }}</p>{% endfor %}
|
||||
</div>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<p class="message text-{{ 'success' if category == 'success' else 'danger' }}">{{ message }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''', form=form)
|
||||
return render_template('import_ig.html', form=form, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/view-igs')
|
||||
def view_igs():
|
||||
@ -194,7 +135,8 @@ def view_igs():
|
||||
|
||||
return render_template('cp_downloaded_igs.html', packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors)
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
||||
site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/process-igs', methods=['POST'])
|
||||
def process_ig():
|
||||
@ -283,7 +225,8 @@ def view_ig(processed_ig_id):
|
||||
base_list = [t for t in processed_ig.resource_types_info if not t.get('is_profile')]
|
||||
examples_by_type = processed_ig.examples or {}
|
||||
return render_template('cp_view_processed_ig.html', title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
||||
processed_ig=processed_ig, profile_list=profile_list, base_list=base_list, examples_by_type=examples_by_type)
|
||||
processed_ig=processed_ig, profile_list=profile_list, base_list=base_list,
|
||||
examples_by_type=examples_by_type, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/get-structure')
|
||||
def get_structure_definition():
|
||||
@ -332,5 +275,10 @@ with app.app_context():
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
|
||||
# Add route to serve favicon
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
Binary file not shown.
BIN
static/FHIRFLARE.png
Normal file
BIN
static/FHIRFLARE.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 281 KiB |
26
templates/_form_helpers.html
Normal file
26
templates/_form_helpers.html
Normal file
@ -0,0 +1,26 @@
|
||||
{# app/templates/_form_helpers.html #}
|
||||
{% macro render_field(field, label_visible=true) %}
|
||||
<div class="form-group mb-3"> {# Add margin bottom for spacing #}
|
||||
{% if label_visible and field.label %}
|
||||
{{ field.label(class="form-label") }} {# Render label with Bootstrap class #}
|
||||
{% endif %}
|
||||
|
||||
{# Add is-invalid class if errors exist #}
|
||||
{% set css_class = 'form-control ' + kwargs.pop('class', '') %}
|
||||
{% if field.errors %}
|
||||
{% set css_class = css_class + ' is-invalid' %}
|
||||
{% endif %}
|
||||
|
||||
{# Render the field itself, passing any extra attributes #}
|
||||
{{ field(class=css_class, **kwargs) }}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
66
templates/base.html
Normal file
66
templates/base.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="bi bi-journal-arrow-down me-1"></i> Manage FHIR Packages</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="bi bi-download me-1"></i> Import IGs</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-3 bg-light">
|
||||
<div class="container text-center">
|
||||
{% set current_year = now.year %}
|
||||
<span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
65
templates/base.html.txt
Normal file
65
templates/base.html.txt
Normal file
@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="bi bi-journal-arrow-down me-1"></i> Manage FHIR Packages</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="bi bi-download me-1"></i> Import IGs</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-3 bg-light">
|
||||
<div class="container text-center">
|
||||
{% set current_year = now.year %}
|
||||
<span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
@ -1,32 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Manage FHIR Packages</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
{% 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">Welcome to {{ site_name }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
This is the starting Point for your Journey through the IG's
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Home</a>
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
@ -34,6 +30,7 @@
|
||||
<div class="card-body">
|
||||
{% if packages %}
|
||||
<div class="table-responsive">
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
|
||||
@ -42,12 +39,12 @@
|
||||
{% for pkg in packages %}
|
||||
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
|
||||
{% set is_duplicate = pkg.name in duplicate_names and duplicate_names[pkg.name]|length > 1 %}
|
||||
{% set group_color = group_colors[pkg.name] if is_duplicate else 'bg-warning' %}
|
||||
<tr {% if is_duplicate %}class="{{ group_color }} text-dark"{% endif %}>
|
||||
{% set group_color = group_colors[pkg.name] if is_duplicate else 'bg-light' %}
|
||||
<tr class="{% if is_duplicate %}{{ group_color }} text-dark{% endif %}">
|
||||
<td>
|
||||
<code>{{ pkg.name }}</code>
|
||||
{% if is_duplicate %}
|
||||
<span class="badge {{ group_color }} text-dark ms-1">Duplicate</span>
|
||||
<span class="badge bg-danger text-light ms-1">Duplicate</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ pkg.version }}</td>
|
||||
@ -58,12 +55,12 @@
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-gear"></i> Process</button>
|
||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@ -76,10 +73,13 @@
|
||||
<p class="mt-2 small text-muted">Duplicates detected:
|
||||
{% for name, versions in duplicate_groups.items() %}
|
||||
{% if versions|length > 1 %}
|
||||
<span class="badge {{ group_colors[name] }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
|
||||
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
|
||||
<span class="badge {{ group_color }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="mt-2 small text-muted">No duplicates detected.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted">No downloaded FHIR packages found.</p>
|
||||
@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
||||
<div class="card-body">
|
||||
{% if processed_list %}
|
||||
@ -102,7 +102,9 @@
|
||||
<tbody>
|
||||
{% for processed_ig in processed_list %}
|
||||
<tr>
|
||||
<td><code>{{ processed_ig.package_name }}</code></td>
|
||||
<td>
|
||||
<code>{{ processed_ig.package_name }}</code>
|
||||
</td>
|
||||
<td>{{ processed_ig.version }}</td>
|
||||
<td>
|
||||
{% set types_info = processed_ig.resource_types_info %}
|
||||
@ -121,7 +123,7 @@
|
||||
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
|
||||
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
||||
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-arrow-down-circle"></i> Unload</button>
|
||||
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@ -134,10 +136,12 @@
|
||||
<p class="text-muted">No packages recorded as processed yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
145
templates/cp_downloaded_igs.html.txt
Normal file
145
templates/cp_downloaded_igs.html.txt
Normal file
@ -0,0 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="https://placehold.co/72x57/0d6efd/white?text=PAS" alt="PAS Logo Placeholder" width="72" height="57">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
This is the starting point for our modular Patient Administration System.
|
||||
We will build upon this core framework by adding modules through the control panel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
|
||||
<div class="card-body">
|
||||
{% if packages %}
|
||||
<div class="table-responsive">
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pkg in packages %}
|
||||
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
|
||||
{% set is_duplicate = pkg.name in duplicate_names and duplicate_names[pkg.name]|length > 1 %}
|
||||
{% set group_color = group_colors[pkg.name] if is_duplicate else 'bg-light' %}
|
||||
<tr class="{% if is_duplicate %}{{ group_color }} text-dark{% endif %}">
|
||||
<td>
|
||||
<code>{{ pkg.name }}</code>
|
||||
{% if is_duplicate %}
|
||||
<span class="badge bg-danger text-light ms-1">Duplicate</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ pkg.version }}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if is_processed %}
|
||||
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if duplicate_groups %}
|
||||
<p class="mt-2 small text-muted">Duplicates detected:
|
||||
{% for name, versions in duplicate_groups.items() %}
|
||||
{% if versions|length > 1 %}
|
||||
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
|
||||
<span class="badge {{ group_color }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="mt-2 small text-muted">No duplicates detected.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted">No downloaded FHIR packages found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
||||
<div class="card-body">
|
||||
{% if processed_list %}
|
||||
<p class="mb-2"><small><span class="badge bg-warning text-dark border me-1">MS</span> = Contains Must Support Elements</small></p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr><th>Package Name</th><th>Version</th><th>Resource Types</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for processed_ig in processed_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ processed_ig.package_name }}</code>
|
||||
</td>
|
||||
<td>{{ processed_ig.version }}</td>
|
||||
<td>
|
||||
{% set types_info = processed_ig.resource_types_info %}
|
||||
{% if types_info %}<div class="d-flex flex-wrap gap-1">
|
||||
{% for type_info in types_info %}
|
||||
{% if type_info.must_support %}
|
||||
<span class="badge bg-warning text-dark border" title="Has Must Support">{{ type_info.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>{% else %}<small class="text-muted">N/A</small>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical btn-group-sm w-100">
|
||||
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
|
||||
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
||||
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No packages recorded as processed yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
@ -1,12 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
{% 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">{{ title }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
View details of the processed FHIR Implementation Guide.
|
||||
</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="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
|
||||
@ -50,6 +60,8 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No profiles defined.</em></p>
|
||||
{% endif %}
|
||||
{% if base_list %}
|
||||
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
|
||||
@ -68,6 +80,8 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No base resource types referenced.</em></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
|
||||
@ -132,255 +146,293 @@
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Debugging: Display raw data for verification -->
|
||||
<div class="mt-4">
|
||||
<details>
|
||||
<summary class="text-muted small">Debug: Raw Data (Click to Expand)</summary>
|
||||
<pre class="p-2 border bg-light">Resource Types Info: {{ processed_ig.resource_types_info | tojson | safe }}</pre>
|
||||
<pre class="p-2 border bg-light">Examples: {{ processed_ig.examples | tojson | safe }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
||||
const structureDisplay = document.getElementById('structure-content');
|
||||
const structureTitle = document.getElementById('structure-title');
|
||||
const structureLoading = document.getElementById('structure-loading');
|
||||
const exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
||||
const exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
||||
const exampleSelect = document.getElementById('example-select');
|
||||
const exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
||||
const exampleLoading = document.getElementById('example-loading');
|
||||
const exampleContentWrapper = document.getElementById('example-content-wrapper');
|
||||
const exampleFilename = document.getElementById('example-filename');
|
||||
const exampleContentRaw = document.getElementById('example-content-raw');
|
||||
const exampleContentJson = document.getElementById('example-content-json');
|
||||
const rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
||||
const rawStructureTitle = document.getElementById('raw-structure-title');
|
||||
const rawStructureContent = document.getElementById('raw-structure-content');
|
||||
const rawStructureLoading = document.getElementById('raw-structure-loading');
|
||||
try {
|
||||
const structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
||||
const structureDisplay = document.getElementById('structure-content');
|
||||
const structureTitle = document.getElementById('structure-title');
|
||||
const structureLoading = document.getElementById('structure-loading');
|
||||
const exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
||||
const exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
||||
const exampleSelect = document.getElementById('example-select');
|
||||
const exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
||||
const exampleLoading = document.getElementById('example-loading');
|
||||
const exampleContentWrapper = document.getElementById('example-content-wrapper');
|
||||
const exampleFilename = document.getElementById('example-filename');
|
||||
const exampleContentRaw = document.getElementById('example-content-raw');
|
||||
const exampleContentJson = document.getElementById('example-content-json');
|
||||
const rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
||||
const rawStructureTitle = document.getElementById('raw-structure-title');
|
||||
const rawStructureContent = document.getElementById('raw-structure-content');
|
||||
const rawStructureLoading = document.getElementById('raw-structure-loading');
|
||||
|
||||
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
||||
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
||||
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
||||
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
||||
|
||||
document.body.addEventListener('click', function(event) {
|
||||
const link = event.target.closest('.resource-type-link');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
// Debugging: Log initial data
|
||||
console.log("Examples Data:", examplesData);
|
||||
|
||||
const pkgName = link.dataset.packageName;
|
||||
const pkgVersion = link.dataset.packageVersion;
|
||||
const resourceType = link.dataset.resourceType;
|
||||
if (!pkgName || !pkgVersion || !resourceType) return;
|
||||
document.body.addEventListener('click', function(event) {
|
||||
const link = event.target.closest('.resource-type-link');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
|
||||
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||
const pkgName = link.dataset.packageName;
|
||||
const pkgVersion = link.dataset.packageVersion;
|
||||
const resourceType = link.dataset.resourceType;
|
||||
if (!pkgName || !pkgVersion || !resourceType) {
|
||||
console.error("Missing data attributes:", { pkgName, pkgVersion, resourceType });
|
||||
return;
|
||||
}
|
||||
|
||||
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
structureDisplay.innerHTML = '';
|
||||
rawStructureContent.textContent = '';
|
||||
structureLoading.style.display = 'block';
|
||||
rawStructureLoading.style.display = 'block';
|
||||
structureDisplayWrapper.style.display = 'block';
|
||||
rawStructureWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||
console.log("Fetching structure from:", structureFetchUrl);
|
||||
|
||||
fetch(structureFetchUrl)
|
||||
.then(response => response.json().then(data => ({ ok: response.ok, status: response.status, data })))
|
||||
.then(result => {
|
||||
if (!result.ok) throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||
populateExampleSelector(resourceType);
|
||||
})
|
||||
.catch(error => {
|
||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
rawStructureContent.textContent = `Error: ${error.message}`;
|
||||
})
|
||||
.finally(() => {
|
||||
structureLoading.style.display = 'none';
|
||||
rawStructureLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
||||
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
||||
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
if (availableExamples.length > 0) {
|
||||
availableExamples.forEach(filePath => {
|
||||
const filename = filePath.split('/').pop();
|
||||
const option = document.createElement('option');
|
||||
option.value = filePath;
|
||||
option.textContent = filename;
|
||||
exampleSelect.appendChild(option);
|
||||
});
|
||||
exampleSelectorWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'block';
|
||||
} else {
|
||||
exampleSelectorWrapper.style.display = 'none';
|
||||
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
structureDisplay.innerHTML = '';
|
||||
rawStructureContent.textContent = '';
|
||||
structureLoading.style.display = 'block';
|
||||
rawStructureLoading.style.display = 'block';
|
||||
structureDisplayWrapper.style.display = 'block';
|
||||
rawStructureWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if(exampleSelect) {
|
||||
exampleSelect.addEventListener('change', function(event) {
|
||||
const selectedFilePath = this.value;
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
if (!selectedFilePath) return;
|
||||
|
||||
const exampleParams = new URLSearchParams({
|
||||
package_name: "{{ processed_ig.package_name }}",
|
||||
package_version: "{{ processed_ig.version }}",
|
||||
filename: selectedFilePath
|
||||
});
|
||||
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
||||
exampleLoading.style.display = 'block';
|
||||
fetch(exampleFetchUrl)
|
||||
fetch(structureFetchUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
||||
}
|
||||
return response.text();
|
||||
console.log("Structure fetch response status:", response.status);
|
||||
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
||||
})
|
||||
.then(content => {
|
||||
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
||||
exampleContentRaw.textContent = content;
|
||||
try {
|
||||
const jsonContent = JSON.parse(content);
|
||||
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
||||
} catch (e) {
|
||||
exampleContentJson.textContent = 'Not valid JSON';
|
||||
}
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
.then(result => {
|
||||
if (!result.ok) throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||
console.log("Structure data received:", result.data);
|
||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||
populateExampleSelector(resourceType);
|
||||
})
|
||||
.catch(error => {
|
||||
exampleFilename.textContent = 'Error';
|
||||
exampleContentRaw.textContent = `Error: ${error.message}`;
|
||||
exampleContentJson.textContent = `Error: ${error.message}`;
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
console.error("Error fetching structure:", error);
|
||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
rawStructureContent.textContent = `Error: ${error.message}`;
|
||||
})
|
||||
.finally(() => {
|
||||
exampleLoading.style.display = 'none';
|
||||
structureLoading.style.display = 'none';
|
||||
rawStructureLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildTreeData(elements) {
|
||||
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||
const nodeMap = {'Root': treeRoot};
|
||||
elements.forEach(el => {
|
||||
const path = el.path;
|
||||
if (!path) return;
|
||||
const parts = path.split('.');
|
||||
let currentPath = '';
|
||||
let parentNode = treeRoot;
|
||||
for(let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
|
||||
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
|
||||
if (!nodeMap[currentPath]) {
|
||||
const newNode = { children: {}, element: null, name: part, path: currentPath };
|
||||
parentNode.children[part] = newNode;
|
||||
nodeMap[currentPath] = newNode;
|
||||
}
|
||||
if (i === parts.length - 1) {
|
||||
nodeMap[currentPath].element = el;
|
||||
}
|
||||
parentNode = nodeMap[currentPath];
|
||||
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||
console.log("Populating examples for:", resourceOrProfileIdentifier);
|
||||
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
||||
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
||||
console.log("Available examples:", availableExamples);
|
||||
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
if (availableExamples.length > 0) {
|
||||
availableExamples.forEach(filePath => {
|
||||
const filename = filePath.split('/').pop();
|
||||
const option = document.createElement('option');
|
||||
option.value = filePath;
|
||||
option.textContent = filename;
|
||||
exampleSelect.appendChild(option);
|
||||
});
|
||||
exampleSelectorWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'block';
|
||||
} else {
|
||||
exampleSelectorWrapper.style.display = 'none';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
}
|
||||
});
|
||||
return Object.values(treeRoot.children);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
|
||||
if (!node || !node.element) return '';
|
||||
const el = node.element;
|
||||
const path = el.path || 'N/A';
|
||||
const min = el.min !== undefined ? el.min : '';
|
||||
const max = el.max || '';
|
||||
const short = el.short || '';
|
||||
const definition = el.definition || '';
|
||||
const isMustSupport = mustSupportPathsSet.has(path);
|
||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></i>' : '';
|
||||
const hasChildren = Object.keys(node.children).length > 0;
|
||||
const collapseId = `collapse-${path.replace(/[\.\:]/g, '-')}`;
|
||||
const padding = level * 20;
|
||||
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||
let typeString = 'N/A';
|
||||
if (el.type && el.type.length > 0) {
|
||||
typeString = el.type.map(t => {
|
||||
let s = t.code || '';
|
||||
if (t.targetProfile && t.targetProfile.length > 0) {
|
||||
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
|
||||
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
|
||||
if (exampleSelect) {
|
||||
exampleSelect.addEventListener('change', function(event) {
|
||||
const selectedFilePath = this.value;
|
||||
console.log("Selected example file:", selectedFilePath);
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
if (!selectedFilePath) return;
|
||||
|
||||
const exampleParams = new URLSearchParams({
|
||||
package_name: "{{ processed_ig.package_name }}",
|
||||
package_version: "{{ processed_ig.version }}",
|
||||
filename: selectedFilePath
|
||||
});
|
||||
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
||||
console.log("Fetching example from:", exampleFetchUrl);
|
||||
exampleLoading.style.display = 'block';
|
||||
fetch(exampleFetchUrl)
|
||||
.then(response => {
|
||||
console.log("Example fetch response status:", response.status);
|
||||
if (!response.ok) {
|
||||
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(content => {
|
||||
console.log("Example content received:", content);
|
||||
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
||||
exampleContentRaw.textContent = content;
|
||||
try {
|
||||
const jsonContent = JSON.parse(content);
|
||||
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
||||
} catch (e) {
|
||||
console.error("Error parsing JSON:", e);
|
||||
exampleContentJson.textContent = 'Not valid JSON';
|
||||
}
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching example:", error);
|
||||
exampleFilename.textContent = 'Error';
|
||||
exampleContentRaw.textContent = `Error: ${error.message}`;
|
||||
exampleContentJson.textContent = `Error: ${error.message}`;
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
exampleLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildTreeData(elements) {
|
||||
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||
const nodeMap = { 'Root': treeRoot };
|
||||
elements.forEach(el => {
|
||||
const path = el.path;
|
||||
if (!path) return;
|
||||
const parts = path.split('.');
|
||||
let currentPath = '';
|
||||
let parentNode = treeRoot;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
|
||||
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
|
||||
if (!nodeMap[currentPath]) {
|
||||
const newNode = { children: {}, element: null, name: part, path: currentPath };
|
||||
parentNode.children[part] = newNode;
|
||||
nodeMap[currentPath] = newNode;
|
||||
}
|
||||
if (i === parts.length - 1) {
|
||||
nodeMap[currentPath].element = el;
|
||||
}
|
||||
parentNode = nodeMap[currentPath];
|
||||
}
|
||||
return s;
|
||||
}).join(' | ');
|
||||
}
|
||||
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||
let childrenHtml = '';
|
||||
if (hasChildren) {
|
||||
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
|
||||
Object.values(node.children).sort((a,b) => a.name.localeCompare(b.name)).forEach(childNode => {
|
||||
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, level + 1);
|
||||
});
|
||||
childrenHtml += `</ul>`;
|
||||
return Object.values(treeRoot.children);
|
||||
}
|
||||
let itemHtml = `<li class="${liClass}">`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||
if (hasChildren) {
|
||||
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||
}
|
||||
itemHtml += `</span><code class="fw-bold ms-1">${node.name}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
||||
let descriptionTooltipAttrs = '';
|
||||
if (definition) {
|
||||
const escapedDefinition = definition.replace(/"/g, '"').replace(/'/g, ''');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</div>`;
|
||||
itemHtml += `</div>`;
|
||||
itemHtml += childrenHtml;
|
||||
itemHtml += `</li>`;
|
||||
return itemHtml;
|
||||
}
|
||||
|
||||
function renderStructureTree(elements, mustSupportPaths) {
|
||||
if (!elements || elements.length === 0) {
|
||||
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
|
||||
return;
|
||||
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
|
||||
if (!node || !node.element) return '';
|
||||
const el = node.element;
|
||||
const path = el.path || 'N/A';
|
||||
const min = el.min !== undefined ? el.min : '';
|
||||
const max = el.max || '';
|
||||
const short = el.short || '';
|
||||
const definition = el.definition || '';
|
||||
const isMustSupport = mustSupportPathsSet.has(path);
|
||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></i>' : '';
|
||||
const hasChildren = Object.keys(node.children).length > 0;
|
||||
const collapseId = `collapse-${path.replace(/[\.\:]/g, '-')}`;
|
||||
const padding = level * 20;
|
||||
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||
let typeString = 'N/A';
|
||||
if (el.type && el.type.length > 0) {
|
||||
typeString = el.type.map(t => {
|
||||
let s = t.code || '';
|
||||
if (t.targetProfile && t.targetProfile.length > 0) {
|
||||
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
|
||||
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
|
||||
}
|
||||
return s;
|
||||
}).join(' | ');
|
||||
}
|
||||
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||
let childrenHtml = '';
|
||||
if (hasChildren) {
|
||||
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
|
||||
Object.values(node.children).sort((a, b) => a.name.localeCompare(b.name)).forEach(childNode => {
|
||||
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, level + 1);
|
||||
});
|
||||
childrenHtml += `</ul>`;
|
||||
}
|
||||
let itemHtml = `<li class="${liClass}">`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||
if (hasChildren) {
|
||||
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||
}
|
||||
itemHtml += `</span><code class="fw-bold ms-1">${node.name}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
||||
let descriptionTooltipAttrs = '';
|
||||
if (definition) {
|
||||
const escapedDefinition = definition
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes
|
||||
.replace(/"/g, '\\"') // Escape double quotes
|
||||
.replace(/'/g, "\\'") // Escape single quotes
|
||||
.replace(/\n/g, '\\n') // Escape newlines
|
||||
.replace(/\r/g, '\\r') // Escape carriage returns
|
||||
.replace(/\t/g, '\\t'); // Escape tabs
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</div>`;
|
||||
itemHtml += `</div>`;
|
||||
itemHtml += childrenHtml;
|
||||
itemHtml += `</li>`;
|
||||
return itemHtml;
|
||||
}
|
||||
const mustSupportPathsSet = new Set(mustSupportPaths || []);
|
||||
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</small></p>';
|
||||
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
||||
html += '<ul class="list-group list-group-flush">';
|
||||
buildTreeData(elements).sort((a,b) => a.name.localeCompare(b.name)).forEach(rootNode => {
|
||||
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
|
||||
});
|
||||
html += '</ul>';
|
||||
structureDisplay.innerHTML = html;
|
||||
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
|
||||
collapseEl.addEventListener('show.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
|
||||
|
||||
function renderStructureTree(elements, mustSupportPaths) {
|
||||
if (!elements || elements.length === 0) {
|
||||
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
|
||||
return;
|
||||
}
|
||||
const mustSupportPathsSet = new Set(mustSupportPaths || []);
|
||||
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</small></p>';
|
||||
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
||||
html += '<ul class="list-group list-group-flush">';
|
||||
buildTreeData(elements).sort((a, b) => a.name.localeCompare(b.name)).forEach(rootNode => {
|
||||
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
|
||||
});
|
||||
collapseEl.addEventListener('hide.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
|
||||
html += '</ul>';
|
||||
structureDisplay.innerHTML = html;
|
||||
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
});
|
||||
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
|
||||
collapseEl.addEventListener('show.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
|
||||
});
|
||||
collapseEl.addEventListener('hide.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("JavaScript error in cp_view_processed_ig.html:", e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
385
templates/cp_view_processed_ig.html.txt
Normal file
385
templates/cp_view_processed_ig.html.txt
Normal file
@ -0,0 +1,385 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "_form_helpers.html" import render_field %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Package List</a>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</div>
|
||||
|
||||
{% if processed_ig %}
|
||||
<div class="card">
|
||||
<div class="card-header">Package Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Package Name</dt>
|
||||
<dd class="col-sm-9"><code>{{ processed_ig.package_name }}</code></dd>
|
||||
<dt class="col-sm-3">Package Version</dt>
|
||||
<dd class="col-sm-9">{{ processed_ig.version }}</dd>
|
||||
<dt class="col-sm-3">Processed At</dt>
|
||||
<dd class="col-sm-9">{{ processed_ig.processed_date.strftime('%Y-%m-%d %H:%M:%S UTC') }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">Resource Types Found / Defined</div>
|
||||
<div class="card-body">
|
||||
{% if profile_list or base_list %}
|
||||
<p class="mb-2"><small><span class="badge bg-warning text-dark border me-1">MS</span> = Contains Must Support Elements</small></p>
|
||||
{% if profile_list %}
|
||||
<h6>Profiles Defined ({{ profile_list|length }}):</h6>
|
||||
<div class="d-flex flex-wrap gap-1 mb-3 resource-type-list">
|
||||
{% for type_info in profile_list %}
|
||||
<a href="#" class="resource-type-link text-decoration-none"
|
||||
data-package-name="{{ processed_ig.package_name }}"
|
||||
data-package-version="{{ processed_ig.version }}"
|
||||
data-resource-type="{{ type_info.name }}"
|
||||
aria-label="View structure for {{ type_info.name }}">
|
||||
{% if type_info.must_support %}
|
||||
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if base_list %}
|
||||
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
|
||||
<div class="d-flex flex-wrap gap-1 resource-type-list">
|
||||
{% for type_info in base_list %}
|
||||
<a href="#" class="resource-type-link text-decoration-none"
|
||||
data-package-name="{{ processed_ig.package_name }}"
|
||||
data-package-version="{{ processed_ig.version }}"
|
||||
data-resource-type="{{ type_info.name }}"
|
||||
aria-label="View structure for {{ type_info.name }}">
|
||||
{% if type_info.must_support %}
|
||||
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="structure-display-wrapper" class="mt-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Structure Definition for: <code id="structure-title"></code></span>
|
||||
<button type="button" class="btn-close" aria-label="Close" onclick="document.getElementById('structure-display-wrapper').style.display='none'; document.getElementById('example-display-wrapper').style.display='none'; document.getElementById('raw-structure-wrapper').style.display='none';"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="structure-loading" class="text-center py-3" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</div>
|
||||
<div id="structure-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="raw-structure-wrapper" class="mt-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">Raw Structure Definition for <code id="raw-structure-title"></code></div>
|
||||
<div class="card-body">
|
||||
<div id="raw-structure-loading" class="text-center py-3" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</div>
|
||||
<pre><code id="raw-structure-content" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="example-display-wrapper" class="mt-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">Examples for <code id="example-resource-type-title"></code></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3" id="example-selector-wrapper" style="display: none;">
|
||||
<label for="example-select" class="form-label">Select Example:</label>
|
||||
<select class="form-select form-select-sm" id="example-select"><option selected value="">-- Select --</option></select>
|
||||
</div>
|
||||
<div id="example-loading" class="text-center py-3" style="display: none;">
|
||||
<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</div>
|
||||
<div id="example-content-wrapper" style="display: none;">
|
||||
<h6 id="example-filename" class="mt-2 small text-muted"></h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Raw Content</h6>
|
||||
<pre><code id="example-content-raw" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Pretty-Printed JSON</h6>
|
||||
<pre><code id="example-content-json" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
||||
const structureDisplay = document.getElementById('structure-content');
|
||||
const structureTitle = document.getElementById('structure-title');
|
||||
const structureLoading = document.getElementById('structure-loading');
|
||||
const exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
||||
const exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
||||
const exampleSelect = document.getElementById('example-select');
|
||||
const exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
||||
const exampleLoading = document.getElementById('example-loading');
|
||||
const exampleContentWrapper = document.getElementById('example-content-wrapper');
|
||||
const exampleFilename = document.getElementById('example-filename');
|
||||
const exampleContentRaw = document.getElementById('example-content-raw');
|
||||
const exampleContentJson = document.getElementById('example-content-json');
|
||||
const rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
||||
const rawStructureTitle = document.getElementById('raw-structure-title');
|
||||
const rawStructureContent = document.getElementById('raw-structure-content');
|
||||
const rawStructureLoading = document.getElementById('raw-structure-loading');
|
||||
|
||||
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
||||
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
||||
|
||||
document.body.addEventListener('click', function(event) {
|
||||
const link = event.target.closest('.resource-type-link');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
|
||||
const pkgName = link.dataset.packageName;
|
||||
const pkgVersion = link.dataset.packageVersion;
|
||||
const resourceType = link.dataset.resourceType;
|
||||
if (!pkgName || !pkgVersion || !resourceType) return;
|
||||
|
||||
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||
|
||||
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||
structureDisplay.innerHTML = '';
|
||||
rawStructureContent.textContent = '';
|
||||
structureLoading.style.display = 'block';
|
||||
rawStructureLoading.style.display = 'block';
|
||||
structureDisplayWrapper.style.display = 'block';
|
||||
rawStructureWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
|
||||
fetch(structureFetchUrl)
|
||||
.then(response => response.json().then(data => ({ ok: response.ok, status: response.status, data })))
|
||||
.then(result => {
|
||||
if (!result.ok) throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||
populateExampleSelector(resourceType);
|
||||
})
|
||||
.catch(error => {
|
||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
rawStructureContent.textContent = `Error: ${error.message}`;
|
||||
})
|
||||
.finally(() => {
|
||||
structureLoading.style.display = 'none';
|
||||
rawStructureLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
||||
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
||||
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
if (availableExamples.length > 0) {
|
||||
availableExamples.forEach(filePath => {
|
||||
const filename = filePath.split('/').pop();
|
||||
const option = document.createElement('option');
|
||||
option.value = filePath;
|
||||
option.textContent = filename;
|
||||
exampleSelect.appendChild(option);
|
||||
});
|
||||
exampleSelectorWrapper.style.display = 'block';
|
||||
exampleDisplayWrapper.style.display = 'block';
|
||||
} else {
|
||||
exampleSelectorWrapper.style.display = 'none';
|
||||
exampleDisplayWrapper.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if(exampleSelect) {
|
||||
exampleSelect.addEventListener('change', function(event) {
|
||||
const selectedFilePath = this.value;
|
||||
exampleContentRaw.textContent = '';
|
||||
exampleContentJson.textContent = '';
|
||||
exampleFilename.textContent = '';
|
||||
exampleContentWrapper.style.display = 'none';
|
||||
if (!selectedFilePath) return;
|
||||
|
||||
const exampleParams = new URLSearchParams({
|
||||
package_name: "{{ processed_ig.package_name }}",
|
||||
package_version: "{{ processed_ig.version }}",
|
||||
filename: selectedFilePath
|
||||
});
|
||||
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
||||
exampleLoading.style.display = 'block';
|
||||
fetch(exampleFetchUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(content => {
|
||||
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
||||
exampleContentRaw.textContent = content;
|
||||
try {
|
||||
const jsonContent = JSON.parse(content);
|
||||
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
||||
} catch (e) {
|
||||
exampleContentJson.textContent = 'Not valid JSON';
|
||||
}
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
})
|
||||
.catch(error => {
|
||||
exampleFilename.textContent = 'Error';
|
||||
exampleContentRaw.textContent = `Error: ${error.message}`;
|
||||
exampleContentJson.textContent = `Error: ${error.message}`;
|
||||
exampleContentWrapper.style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
exampleLoading.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildTreeData(elements) {
|
||||
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||
const nodeMap = {'Root': treeRoot};
|
||||
elements.forEach(el => {
|
||||
const path = el.path;
|
||||
if (!path) return;
|
||||
const parts = path.split('.');
|
||||
let currentPath = '';
|
||||
let parentNode = treeRoot;
|
||||
for(let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
|
||||
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
|
||||
if (!nodeMap[currentPath]) {
|
||||
const newNode = { children: {}, element: null, name: part, path: currentPath };
|
||||
parentNode.children[part] = newNode;
|
||||
nodeMap[currentPath] = newNode;
|
||||
}
|
||||
if (i === parts.length - 1) {
|
||||
nodeMap[currentPath].element = el;
|
||||
}
|
||||
parentNode = nodeMap[currentPath];
|
||||
}
|
||||
});
|
||||
return Object.values(treeRoot.children);
|
||||
}
|
||||
|
||||
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
|
||||
if (!node || !node.element) return '';
|
||||
const el = node.element;
|
||||
const path = el.path || 'N/A';
|
||||
const min = el.min !== undefined ? el.min : '';
|
||||
const max = el.max || '';
|
||||
const short = el.short || '';
|
||||
const definition = el.definition || '';
|
||||
const isMustSupport = mustSupportPathsSet.has(path);
|
||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></i>' : '';
|
||||
const hasChildren = Object.keys(node.children).length > 0;
|
||||
const collapseId = `collapse-${path.replace(/[\.\:]/g, '-')}`;
|
||||
const padding = level * 20;
|
||||
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||
let typeString = 'N/A';
|
||||
if (el.type && el.type.length > 0) {
|
||||
typeString = el.type.map(t => {
|
||||
let s = t.code || '';
|
||||
if (t.targetProfile && t.targetProfile.length > 0) {
|
||||
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
|
||||
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
|
||||
}
|
||||
return s;
|
||||
}).join(' | ');
|
||||
}
|
||||
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||
let childrenHtml = '';
|
||||
if (hasChildren) {
|
||||
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
|
||||
Object.values(node.children).sort((a,b) => a.name.localeCompare(b.name)).forEach(childNode => {
|
||||
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, level + 1);
|
||||
});
|
||||
childrenHtml += `</ul>`;
|
||||
}
|
||||
let itemHtml = `<li class="${liClass}">`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||
if (hasChildren) {
|
||||
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||
}
|
||||
itemHtml += `</span><code class="fw-bold ms-1">${node.name}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
||||
let descriptionTooltipAttrs = '';
|
||||
if (definition) {
|
||||
const escapedDefinition = definition.replace(/"/g, '"').replace(/'/g, ''');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</div>`;
|
||||
itemHtml += `</div>`;
|
||||
itemHtml += childrenHtml;
|
||||
itemHtml += `</li>`;
|
||||
return itemHtml;
|
||||
}
|
||||
|
||||
function renderStructureTree(elements, mustSupportPaths) {
|
||||
if (!elements || elements.length === 0) {
|
||||
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
|
||||
return;
|
||||
}
|
||||
const mustSupportPathsSet = new Set(mustSupportPaths || []);
|
||||
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</small></p>';
|
||||
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
||||
html += '<ul class="list-group list-group-flush">';
|
||||
buildTreeData(elements).sort((a,b) => a.name.localeCompare(b.name)).forEach(rootNode => {
|
||||
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
|
||||
});
|
||||
html += '</ul>';
|
||||
structureDisplay.innerHTML = html;
|
||||
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
|
||||
collapseEl.addEventListener('show.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
|
||||
});
|
||||
collapseEl.addEventListener('hide.bs.collapse', event => {
|
||||
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
{% endblock content %} {# Main content block END #}
|
40
templates/import_ig.html
Normal file
40
templates/import_ig.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% 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">Import FHIR Implementation Guides</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Import new FHIR Implementation Guides to the system for viewing.
|
||||
</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">View Downloaded IGs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2><i class="bi bi-download me-2"></i>Import a New IG</h2>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" class="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field(form.package_name) }}
|
||||
{{ render_field(form.package_version) }}
|
||||
<div class="d-grid gap-2 d-sm-flex">
|
||||
{{ form.submit(class="btn btn-success") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
17
templates/index.html
Normal file
17
templates/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% 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="512" height="512">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Simple tool for importing and viewing FHIR Implementation Guides.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import FHIR IG</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user