New Engineering Layer tab on the engineer image page. A site uploads Engineering_Install_v<ver>.exe (keeps prereqs) or a .zip of the whole extra_engineering payload. The upload is streamed to disk, checked (MZ header, no unsafe zip paths) and swapped in; the replaced payload is archived (two kept) with a Restore button. Install-EngineeringLayer.ps1 stays repo-owned and now installs the newest Engineering_Install_v*.exe. KB: Part 4 becomes Site Content (enrollment package, Engineering Layer, Blancco site settings, Shop Floor apps) plus a new Adding Shop Floor Apps section for preinstall.json/postppkg.json. WJ examples removed.
135 lines
7.9 KiB
HTML
135 lines
7.9 KiB
HTML
{#
|
|
_image_header.html - shared header + tabs for every GE Aerospace image page.
|
|
|
|
CONTRACT (image-shell; used by the Overview, Drivers, Unattend and OS and
|
|
Packages tabs):
|
|
|
|
Use it as the first thing inside {% block content %}:
|
|
|
|
{% set header_actions %}
|
|
<button type="button" class="btn btn-success btn-sm" id="...">Save</button>
|
|
{% endset %}
|
|
{% set header_subtitle = 'Deploy/FlatUnattendW10.xml' %}
|
|
{% include "_image_header.html" %}
|
|
|
|
Variables it reads from the template context:
|
|
image_type required. Image key, e.g. "gea-standard".
|
|
active_tab optional. One of "overview", "drivers", "unattend", "os".
|
|
When missing it is worked out from request.endpoint
|
|
(image_tab_for_endpoint), so a route need not pass it.
|
|
header_actions optional. HTML for the right side of the title row
|
|
(Save / Review / Undo buttons). Build it with a
|
|
{% set header_actions %}...{% endset %} block.
|
|
header_subtitle optional. Plain text appended to the meta line
|
|
(file path, last save time, ...).
|
|
|
|
Globals it uses (context processor in app.py):
|
|
image_tabs services/image_pages.IMAGE_TABS: id, label, icon,
|
|
endpoint for each tab, in display order.
|
|
image_tab_url(t, tab) URL of a tab; use it for links and redirects
|
|
(in Python: services.image_pages.tab_url).
|
|
image_header_info(t) friendly_name, os_label, driver_count,
|
|
package_count, has_unattend.
|
|
|
|
Tab endpoints (change a row of IMAGE_TABS if a page moves):
|
|
overview images.overview GET /images/<t> views/images.py
|
|
drivers images_drivers.page GET /images/<t>/drivers views/images_drivers.py
|
|
unattend unattend_editor GET /images/<t>/unattend app.py
|
|
os images.os_packages GET /images/<t>/os views/images.py
|
|
Old URLs: /images/<t>/config redirects to the Overview (or ?tab=<id>).
|
|
|
|
The sidebar marks an image active whenever active_tab is set (or the
|
|
endpoint maps to a tab) and image_type matches, so any page that includes
|
|
this partial highlights the right sidebar entry.
|
|
|
|
It also loads static/image-pages.js and the shared review modal
|
|
(#imageReviewModal). For a "Review changes" step call
|
|
PxeImage.reviewAndSave({previewUrl, saveUrl, body, title})
|
|
where previewUrl answers {"files": [control_files.public(plan), ...]}
|
|
and saveUrl takes the same body plus "expect": {name: sha_before} and
|
|
answers {"ok": true} (it flashes; the page reloads) or {"error": "..."}.
|
|
PxeImage.renderDiff(el, files) draws the same diff anywhere.
|
|
#}
|
|
{% set _tab = active_tab if active_tab is defined and active_tab else image_tab_for_endpoint(request.endpoint) %}
|
|
{% set _hdr = image_header_info(image_type) %}
|
|
<style>
|
|
.image-head { margin-bottom: 20px; }
|
|
.image-head-top { display: flex; flex-wrap: wrap; gap: 12px 16px; justify-content: space-between; align-items: flex-end; margin-bottom: 14px; }
|
|
.image-head-title { flex: 1 1 20rem; min-width: 0; }
|
|
.image-head h1 { font-size: 28px; font-weight: 600; line-height: 1.25; margin: 2px 0 4px; letter-spacing: 0; }
|
|
.image-head .eyebrow-t { font-size: 13px; color: var(--muted); }
|
|
.image-head-meta { font-size: 12px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 2px 8px; align-items: center; }
|
|
.image-head-meta .sep { opacity: 0.5; }
|
|
.image-head .header-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
.image-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line-2); overflow-x: auto; scrollbar-width: none; margin: 0; padding: 0; list-style: none; }
|
|
.image-tabs a { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 42px; padding: 0 14px; font-size: 14px;
|
|
color: var(--text-2); text-decoration: none; white-space: nowrap; border-bottom: 3px solid transparent; margin-bottom: -1px; }
|
|
.image-tabs a:hover { color: var(--text); }
|
|
.image-tabs a[aria-current] { color: var(--text); font-weight: 600; border-bottom-color: var(--text); }
|
|
.image-tabs .tab-count { font-family: var(--font-mono); font-size: 12px; color: var(--muted); font-weight: 400; }
|
|
|
|
/* Unified diff (review step of every save) */
|
|
.diff-file { border: 1px solid var(--line-2); border-radius: 10px; margin-bottom: 1rem; overflow: hidden; }
|
|
.diff-file-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--line-2);
|
|
background: var(--surface-2); font-weight: 600; font-size: 13px; }
|
|
.diff-view { margin: 0; max-height: 55vh; overflow: auto; font-size: 12px; line-height: 1.45; font-family: var(--font-mono); background: var(--surface); }
|
|
.diff-view span { display: block; padding: 0 12px; white-space: pre-wrap; word-break: break-all; }
|
|
.diff-view .d-add { background: var(--add-bg); color: var(--add-text); }
|
|
.diff-view .d-del { background: var(--del-bg); color: var(--del-text); }
|
|
.diff-view .d-hunk { color: var(--accent); background: var(--surface-2); }
|
|
.diff-view .d-meta { color: var(--muted); }
|
|
</style>
|
|
|
|
<div class="image-head">
|
|
<div class="image-head-top">
|
|
<div class="image-head-title">
|
|
<div class="eyebrow-t">Image</div>
|
|
<h1>{{ _hdr.friendly_name | replace('GE Aerospace ', '') }}</h1>
|
|
<div class="image-head-meta">
|
|
<span class="mono">{{ image_type }}</span>
|
|
{% if _hdr.os_label %}<span class="sep">|</span><span>{{ _hdr.os_label }}</span>{% endif %}
|
|
{% if header_subtitle is defined and header_subtitle %}<span class="sep">|</span><span class="mono">{{ header_subtitle }}</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if header_actions is defined and header_actions %}
|
|
<div class="header-actions">{{ header_actions }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<nav aria-label="Image sections"><ul class="image-tabs">
|
|
{% for t in image_tabs if not t.images or image_type in t.images %}
|
|
<li><a href="{{ image_tab_url(image_type, t.id) }}"{% if t.id == _tab %} aria-current="page"{% endif %}>
|
|
{{ t.label }}
|
|
{% if t.id == 'drivers' %}<span class="tab-count" title="Entries in HardwareDriver.json">{{ _hdr.driver_count }}</span>
|
|
{% elif t.id == 'os' and _hdr.package_count %}<span class="tab-count" title="Entries in packages.json">{{ _hdr.package_count }}</span>
|
|
{% elif t.id == 'unattend' and not _hdr.has_unattend %}<span class="chip c-warn" style="font-size: 11px; padding: 1px 6px">missing</span>{% endif %}
|
|
</a></li>
|
|
{% endfor %}
|
|
</ul></nav>
|
|
</div>
|
|
|
|
{# Shared review step: filled by PxeImage.reviewAndSave (static/image-pages.js) #}
|
|
<div class="modal fade" id="imageReviewModal" tabindex="-1" aria-labelledby="imageReviewTitle">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="imageReviewTitle">Review changes</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-muted small mb-3" id="imageReviewIntro">
|
|
This is exactly what will be written. Each changed file is backed up next to itself
|
|
(<span class="mono">.bak-<date>-webui</span>) first, and the save can be undone from this page.
|
|
</p>
|
|
<div id="imageReviewBody"></div>
|
|
<div class="alert alert-danger d-none mb-0" id="imageReviewError"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn" data-bs-dismiss="modal">Back to editing</button>
|
|
<button type="button" class="ui-btn pri" id="imageReviewConfirm" disabled>Save and back up</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="{{ url_for('static', filename='image-pages.js') }}"></script>
|