Webapp redesign phase 6: the remaining pages
Restyled to the same row pattern (one line, details in an expandable body, one action plus a menu, tokens for light and dark): - hardware coverage, audit log (family chips, destructive actions in red, client-side filters, first 300 rows), Clonezilla backups, enrollment packages (active package marked and undeletable, MCL flags with a visible legend) - image Overview, OS and packages, Unattend (sticky save bar), Import - Dell, iPXE, Blancco and BPRT update pages; Dell history links to the rollback review instead of rolling back on one click - boot menu editor (tabs, rows with a menu, lint chips that jump to the line, tokens instead of hard-coded dark colours) and the imaging session detail page Fixes found on the way: - BPRT batch Activate never asked to confirm: a hidden input named 'confirm' shadowed window.confirm in the inline handler, so the form submitted without a prompt. It now calls window.confirm. - Backup upload refused when a backup with that name exists, unless 'Replace the existing backup' is ticked (it overwrote silently). - app.js: em dashes in comments replaced (plain ASCII).
This commit is contained in:
@@ -1,270 +1,305 @@
|
||||
{% extends "base.html" %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}{{ friendly_name }} - OS and Packages{% endblock %}
|
||||
|
||||
{# OS and Packages tab. Each list is a <table> whose rows are drawn as one
|
||||
line plus an expandable body, because static/app.js removes a row with
|
||||
.remove-row by its closest <tr> and renumbers .order-num in the tbody.
|
||||
Edits are staged in the page; the save bar's Review and save posts only
|
||||
the sections that changed (views/images.py os_preview / os_save). #}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.osp-table td { vertical-align: top; white-space: normal; }
|
||||
.osp-table td.num { width: 40px; color: var(--text-light); font-weight: 600; text-align: center; }
|
||||
.osp-table .wrap { overflow-wrap: anywhere; }
|
||||
.osp-table .sub { font-size: 12px; color: var(--text-light); }
|
||||
.osp-table .form-control-sm { min-width: 10rem; }
|
||||
.osp-table tr.row-new td { background: rgba(65, 129, 255, 0.06); }
|
||||
#ospModelTable td { vertical-align: middle; }
|
||||
.osp-note { font-size: 13px; color: var(--text-light); padding: 0.75rem 1.25rem; border-top: 1px solid var(--border); }
|
||||
.osp-explain { font-size: 13.5px; }
|
||||
.osp-explain p:last-child { margin-bottom: 0; }
|
||||
.dirty-flag { font-size: 12px; color: var(--warning-dark); display: none; align-items: center; gap: 0.35rem; }
|
||||
[data-theme="dark"] .dirty-flag { color: var(--warning); }
|
||||
.dirty .dirty-flag { display: inline-flex; }
|
||||
.empty-row td { text-align: center; color: var(--text-light); padding: 1.25rem; }
|
||||
.osp-list, .osp-list tbody, .osp-list tr, .osp-list td { display: block; }
|
||||
.osp-list { width: 100%; border-collapse: collapse; margin: 0; }
|
||||
.osp-list td { padding: 0; }
|
||||
.osp-list tr.row-new > td { background: var(--edit-soft); }
|
||||
.osp-phead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 16px 24px 12px; }
|
||||
.osp-phead h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
.osp-phead .sp { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
||||
.osp-note { font-size: 13px; color: var(--text-2); padding: 12px 24px; border-top: 1px solid var(--line); line-height: 1.55; }
|
||||
.osp-list td.osp-empty { padding: 20px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); font-size: 14px; }
|
||||
.osp-head.og, .osp-head.pg, .osp-head.mg { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px !important; padding-bottom: 6px !important; }
|
||||
.osp-list .num { color: var(--muted); font-size: 13px; }
|
||||
.osp-list .chips { display: flex; gap: 6px; flex-wrap: nowrap; min-width: 0; }
|
||||
.osp-list .name { font-weight: 600; }
|
||||
|
||||
/* Operating systems */
|
||||
.og { display: grid; grid-template-columns: 26px minmax(0, 1fr) 54px 150px 190px minmax(0, 1fr) 14px; align-items: center;
|
||||
gap: 0 14px; padding: 11px 76px 11px 24px; font-size: 14px; min-height: 24px; }
|
||||
/* Packages */
|
||||
.pg { display: grid; grid-template-columns: 26px minmax(0, 1.2fr) minmax(0, 1fr) 64px 82px 92px 14px; align-items: center;
|
||||
gap: 0 14px; padding: 11px 130px 11px 24px; font-size: 14px; min-height: 24px; }
|
||||
/* Hardware models (inputs on the row, no body) */
|
||||
.mg { display: grid; grid-template-columns: 26px minmax(0, 1fr) minmax(0, 1.3fr) 190px 84px; align-items: center;
|
||||
gap: 0 14px; padding: 8px 24px; font-size: 14px; }
|
||||
.mg .field { width: 100%; min-width: 0; height: 32px; font-size: 13px; }
|
||||
.mg .ents { display: flex; align-items: center; gap: 8px; white-space: nowrap; min-width: 0; }
|
||||
.mg > :last-child { justify-self: end; }
|
||||
|
||||
.obody { padding: 2px 24px 16px 64px; font-size: 13px; }
|
||||
.obody dl { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 5px 16px; }
|
||||
.obody dt { color: var(--muted); font-weight: 400; }
|
||||
.obody dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
.osp-list .act { display: flex; align-items: center; gap: 10px; }
|
||||
.osp-list .act .form-switch { margin: 0; padding-left: 2.5em; min-height: 0; display: flex; align-items: center; }
|
||||
.osp-list .act .form-check-input { margin-top: 0; cursor: pointer; }
|
||||
|
||||
.osp-models > summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 16px 24px; }
|
||||
.osp-models > summary h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
.osp-models .explain { padding: 0 24px 14px; font-size: 13px; color: var(--text-2); line-height: 1.6; max-width: 980px; }
|
||||
.osp-models .explain p { margin: 0 0 8px; }
|
||||
.osp-intro { margin: 0; font-size: 14px; color: var(--text-2); line-height: 1.6; max-width: 980px; }
|
||||
|
||||
.savebar { position: sticky; bottom: 12px; z-index: 50; display: none; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 12px 12px 12px 20px; border-radius: 12px; background: var(--savebar-bg); border: 1px solid var(--savebar-edge);
|
||||
color: var(--savebar-text); box-shadow: 0 8px 24px rgba(0,0,0,.25); }
|
||||
.dirty .savebar { display: flex; }
|
||||
.savebar .ui-btn { background: transparent; color: var(--savebar-text); border-color: #8E9AAB; }
|
||||
.savebar .ui-btn.pri { background: var(--savebar-pri); border-color: var(--savebar-pri); color: var(--on-savebar-pri); }
|
||||
.savebar .dirty-flag { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; }
|
||||
.savebar .dirty-flag .dot { background: var(--act); }
|
||||
|
||||
@container (max-width: 1000px) {
|
||||
.og { grid-template-columns: 26px minmax(0, 1fr) 150px 190px 14px; }
|
||||
.og .c-id, .og .c-dest { display: none; }
|
||||
.pg { grid-template-columns: 26px minmax(0, 1fr) 82px 92px 14px; }
|
||||
.pg .c-file, .pg .c-id { display: none; }
|
||||
.mg { grid-template-columns: 26px minmax(0, 1fr) minmax(0, 1fr) auto 84px; }
|
||||
.mg .ents .sub { display: none; }
|
||||
.obody { padding-left: 24px; }
|
||||
}
|
||||
@container (max-width: 600px) {
|
||||
.og, .pg { grid-template-columns: 22px auto minmax(0, 1fr) 14px; padding: 11px 56px 11px 16px; gap: 6px 10px; }
|
||||
.pg { padding-right: 108px; }
|
||||
.og > .tip:not(.c-dest), .pg > .tip:not(.c-file) { grid-column: 2 / 4; grid-row: 1; }
|
||||
.og .c-st, .pg .c-en { grid-column: 2; grid-row: 2; }
|
||||
.og .c-wim, .pg .c-disk { grid-column: 3; grid-row: 2; flex-wrap: wrap; }
|
||||
.og .chev, .pg .chev { grid-column: 4; grid-row: 1; }
|
||||
.osp-head { display: none; }
|
||||
.osp-list .ui-row > .act { right: 8px; gap: 4px; }
|
||||
.mg { grid-template-columns: 22px minmax(0, 1fr) auto; padding: 10px 16px; gap: 6px 10px; }
|
||||
.mg .c-model { grid-column: 2; grid-row: 1; }
|
||||
.mg .c-fam { grid-column: 2 / -1; grid-row: 2; }
|
||||
.mg .ents { grid-column: 2; grid-row: 3; padding: 0; }
|
||||
.mg > :last-child { grid-column: 3; grid-row: 1; }
|
||||
.obody { padding: 2px 16px 14px; }
|
||||
.osp-phead, .osp-note, .osp-models > summary, .osp-models .explain { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set header_actions %}
|
||||
<span class="dirty-flag" id="ospDirty"><i class="bi bi-circle-fill" style="font-size:8px;"></i> Unsaved changes</span>
|
||||
{% if last_save %}
|
||||
<form method="POST" action="{{ url_for('images.os_undo', image_type=image_type) }}" class="d-inline"
|
||||
onsubmit="return confirm('Put back {{ last_save.files | map(attribute='name') | join(', ') }} as they were before the save of {{ last_save.ts | local_dt }}?');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm" title="Last save: {{ last_save.ts | local_dt }}">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Undo last save
|
||||
</button>
|
||||
<button type="submit" class="ui-btn" title="Last save: {{ last_save.ts | local_dt }}">Undo last save</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="sub">Undo last save: nothing saved from this page yet</span>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-success btn-sm" id="ospReview" disabled>
|
||||
<i class="bi bi-file-earmark-diff"></i> Review and save
|
||||
</button>
|
||||
{% endset %}
|
||||
{% set header_subtitle = 'Deploy/Control' %}
|
||||
{% include "_image_header.html" %}
|
||||
|
||||
<p class="text-muted osp-explain">
|
||||
<div class="ui-stack" id="ospPage">
|
||||
<p class="osp-intro">
|
||||
What PESetup puts on the disk besides drivers: the Windows image from
|
||||
<span class="mono">OperatingSystem.json</span>, then the offline servicing packages from
|
||||
<span class="mono">packages.json</span> (both in <span class="mono">Deploy/Control</span>).
|
||||
Edits here are staged until <strong>Review and save</strong>, which shows the exact diff of every file first.
|
||||
<span class="mono">packages.json</span>. Edits are staged until <strong>Review and save</strong>,
|
||||
which shows the exact diff of every file first.
|
||||
</p>
|
||||
|
||||
{# ==================== Operating system ==================== #}
|
||||
<div class="card" data-section="operating_systems">
|
||||
<div class="card-header">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-windows"></i> Operating system
|
||||
<span class="badge badge-secondary">{{ oses | length }}</span>
|
||||
</span>
|
||||
<span class="small text-muted fw-normal mono">OperatingSystem.json</span>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table mb-0 osp-table" id="ospOsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Windows</th>
|
||||
<th>osId</th>
|
||||
<th>Status</th>
|
||||
<th>install.wim</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in oses %}
|
||||
{% set osv = r.osv %}
|
||||
<tr data-json='{{ r.entry | tojson_ordered }}'>
|
||||
<td class="num order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<strong>{{ osv.productName or osv.marketingName or 'Windows' }}</strong>
|
||||
<div class="sub">{{ osv.marketingName }}{% if osv.versionNumber %} | version {{ osv.versionNumber }}{% endif %}{% if osv.buildNumber %} | build {{ osv.buildNumber }}{% endif %}</div>
|
||||
</td>
|
||||
<td class="mono">{{ osv.id }}</td>
|
||||
<td class="text-nowrap">
|
||||
{% if osv.isActive %}<span class="badge badge-success">Active</span>{% else %}<span class="badge badge-secondary">Inactive</span>{% endif %}
|
||||
{% if os_selection is not none and osv.id | string == os_selection | string %}<span class="badge badge-primary" title="user_selections.json OperatingSystemSelection">Selected</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="mono wrap">{{ r.dest or '(no DestinationDir)' }}</div>
|
||||
<div class="sub">
|
||||
{% if r.wim.exists %}<span class="badge badge-success">On disk</span> {{ r.wim.size | filesizeformat }}, modified {{ r.wim.mtime | local_dt }}
|
||||
{% else %}<span class="badge badge-danger">install.wim not on disk</span>{% endif %}
|
||||
| index {{ osv.wimindex if osv.wimindex is defined else (r.entry.wimindex if r.entry.wimindex is defined else '-') }}
|
||||
{% if osv.wim and osv.wim.size and r.wim.exists and osv.wim.size != r.wim.size %}
|
||||
<span class="badge badge-warning" title="size in OperatingSystem.json: {{ osv.wim.size }}">size differs from the JSON</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this entry">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if oses %}style="display:none"{% endif %}><td colspan="6">No operating system entries. PESetup has nothing to apply.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="panel ui-list" data-section="operating_systems" aria-labelledby="osp-os-h">
|
||||
<div class="osp-phead">
|
||||
<h2 id="osp-os-h">Operating system</h2>
|
||||
{{ ui.chip(oses | length, 'neutral') }}
|
||||
<span class="sp"><span class="sub mono">OperatingSystem.json</span></span>
|
||||
</div>
|
||||
<div class="og osp-head" aria-hidden="true"><span>#</span><span>Windows</span><span class="c-id">osId</span><span class="c-st">Status</span><span class="c-wim">install.wim</span><span class="c-dest">Folder</span><span></span></div>
|
||||
<table class="osp-list" id="ospOsTable">
|
||||
<tbody>
|
||||
{% for r in oses %}
|
||||
{% set osv = r.osv %}
|
||||
{% set idx = osv.wimindex if osv.wimindex is defined else (r.entry.wimindex if r.entry.wimindex is defined else '-') %}
|
||||
{% set size_differs = osv.wim and osv.wim.size and r.wim.exists and osv.wim.size != r.wim.size %}
|
||||
<tr data-json='{{ r.entry | tojson_ordered }}'><td class="ui-row">
|
||||
<details>
|
||||
<summary class="og">
|
||||
<span class="num order-num">{{ loop.index }}</span>
|
||||
<span class="tip" data-tip="{{ osv.productName or osv.marketingName or 'Windows' }}{% if osv.versionNumber %} {{ osv.versionNumber }}{% endif %}"><span class="trunc"><span class="name">{{ osv.productName or osv.marketingName or 'Windows' }}</span> <span class="muted">{{ osv.versionNumber }}</span></span></span>
|
||||
<span class="mono c-id">{{ osv.id }}</span>
|
||||
<span class="chips c-st">
|
||||
{% if osv.isActive %}{{ ui.chip('Active', 'ok') }}{% else %}{{ ui.chip('Inactive', 'off') }}{% endif %}
|
||||
{% if os_selection is not none and osv.id | string == os_selection | string %}{{ ui.chip('Selected', 'w11', 'user_selections.json OperatingSystemSelection') }}{% endif %}
|
||||
</span>
|
||||
<span class="chips c-wim">{% if r.wim.exists %}{{ ui.chip('On disk', 'ok') }}{% else %}{{ ui.chip('Not on disk', 'err') }}{% endif %}{% if size_differs %}{{ ui.chip('Size differs', 'warn') }}{% endif %}</span>
|
||||
<span class="tip right c-dest" data-tip="{{ r.dest or '(no DestinationDir)' }}"><span class="trunc mono sub">{{ r.dest or '(no DestinationDir)' }}</span></span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="obody"><dl>
|
||||
<dt>Windows</dt><dd>{{ osv.productName or '-' }}{% if osv.marketingName %} ({{ osv.marketingName }}){% endif %}</dd>
|
||||
<dt>Version</dt><dd>{{ osv.versionNumber or '-' }}{% if osv.buildNumber %}, build {{ osv.buildNumber }}{% endif %}, osId <span class="mono">{{ osv.id }}</span></dd>
|
||||
<dt>DestinationDir</dt><dd class="mono">{{ r.dest or '(no DestinationDir)' }}</dd>
|
||||
<dt>install.wim</dt><dd>{% if r.wim.exists %}On disk, {{ r.wim.size | filesizeformat }}, modified {{ r.wim.mtime | local_dt }}{% else %}<strong>install.wim not on disk</strong>{% endif %}, image index {{ idx }}</dd>
|
||||
{% if size_differs %}<dt>Size</dt><dd>Differs from the JSON: OperatingSystem.json says {{ osv.wim.size | filesizeformat }} ({{ osv.wim.size }} bytes)</dd>{% endif %}
|
||||
</dl></div>
|
||||
</details>
|
||||
<div class="act">
|
||||
{% call ui.more('operating system ' ~ loop.index) %}<button type="button" class="remove-row del">Remove entry</button>{% endcall %}
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if oses %}style="display:none"{% endif %}><td class="osp-empty">No operating system entries. PESetup has nothing to apply.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="osp-note">
|
||||
PESetup applies the image index of <span class="mono">install.wim</span> from the entry's folder
|
||||
(<span class="mono">*destinationdir*</span> is this image's share).
|
||||
{% if os_selection is not none %}The entry marked Selected is the one the media was built for
|
||||
(<span class="mono">OperatingSystemSelection = {{ os_selection }}</span>).{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ==================== Packages ==================== #}
|
||||
<div class="card" data-section="packages">
|
||||
<div class="card-header">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-box-seam"></i> Packages
|
||||
<span class="badge badge-secondary">{{ packages | length }}</span>
|
||||
<section class="panel ui-list" data-section="packages" aria-labelledby="osp-pk-h">
|
||||
<div class="osp-phead">
|
||||
<h2 id="osp-pk-h">Packages</h2>
|
||||
{{ ui.chip(packages | length, 'neutral') }}
|
||||
<span class="sp">
|
||||
<span class="sub mono">packages.json</span>
|
||||
<button type="button" class="ui-btn" data-bs-toggle="modal" data-bs-target="#packageUploadModal">Upload file</button>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<span class="small text-muted fw-normal mono align-self-center">packages.json</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal" data-bs-target="#packageUploadModal">
|
||||
<i class="bi bi-upload"></i> Upload file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table mb-0 osp-table" id="ospPkgTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Package</th>
|
||||
<th>File</th>
|
||||
<th>osId</th>
|
||||
<th>Enabled</th>
|
||||
<th>On disk</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in packages %}
|
||||
{% set p = r.entry %}
|
||||
<tr data-json='{{ p | tojson_ordered }}'>
|
||||
<td class="num order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<strong>{{ p.name or '(no name)' }}</strong>
|
||||
{% if p.comment %}<div class="sub">{{ p.comment }}</div>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="mono wrap">{{ p.fileName or p.FileName or '' }}</div>
|
||||
<div class="sub mono wrap">{{ p.destinationDir or p.DestinationDir or '' }}</div>
|
||||
</td>
|
||||
<td class="mono">{{ p.osId }}</td>
|
||||
<td>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input class="form-check-input pkg-enabled" type="checkbox" role="switch"
|
||||
aria-label="Enabled" {% if p.enabled %}checked{% endif %}>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{% if r.file.exists %}<span class="badge badge-success">Yes</span><div class="sub">{{ r.file.size | filesizeformat }}</div>
|
||||
{% else %}<span class="badge badge-danger">No</span>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this entry">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if packages %}style="display:none"{% endif %}><td colspan="7">No packages configured.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pg osp-head" aria-hidden="true"><span>#</span><span>Package</span><span class="c-file">File</span><span class="c-id">osId</span><span class="c-en">Enabled</span><span class="c-disk">On disk</span><span></span></div>
|
||||
<table class="osp-list" id="ospPkgTable">
|
||||
<tbody>
|
||||
{% for r in packages %}
|
||||
{% set p = r.entry %}
|
||||
{% set fname = p.fileName or p.FileName or '' %}
|
||||
{% set pdest = p.destinationDir or p.DestinationDir or '' %}
|
||||
<tr data-json='{{ p | tojson_ordered }}'><td class="ui-row">
|
||||
<details>
|
||||
<summary class="pg">
|
||||
<span class="num order-num">{{ loop.index }}</span>
|
||||
<span class="tip" data-tip="{{ p.name or '(no name)' }}"><span class="trunc name">{{ p.name or '(no name)' }}</span></span>
|
||||
<span class="tip c-file" data-tip="{{ fname }}"><span class="trunc mono sub">{{ fname }}</span></span>
|
||||
<span class="mono c-id">{{ p.osId }}</span>
|
||||
<span class="c-en pkg-state">{% if p.enabled %}{{ ui.chip('Enabled', 'ok') }}{% else %}{{ ui.chip('Disabled', 'off') }}{% endif %}</span>
|
||||
<span class="c-disk">{% if r.file.exists %}{{ ui.chip('On disk', 'ok') }}{% else %}{{ ui.chip('Missing', 'err') }}{% endif %}</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="obody"><dl>
|
||||
{% if p.comment %}<dt>Comment</dt><dd>{{ p.comment }}</dd>{% endif %}
|
||||
<dt>File</dt><dd class="mono">{{ fname or '-' }}</dd>
|
||||
<dt>DestinationDir</dt><dd class="mono">{{ pdest or '-' }}</dd>
|
||||
<dt>On disk</dt><dd>{% if r.file.exists %}Yes, {{ r.file.size | filesizeformat }}{% else %}<strong>No</strong>: the file is not in that folder{% endif %}</dd>
|
||||
<dt>osId</dt><dd class="mono">{{ p.osId }}</dd>
|
||||
</dl></div>
|
||||
</details>
|
||||
<div class="act">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input pkg-enabled" type="checkbox" role="switch"
|
||||
aria-label="Enabled: {{ p.name or fname }}" {% if p.enabled %}checked{% endif %}>
|
||||
</div>
|
||||
{% call ui.more('package ' ~ loop.index) %}<button type="button" class="remove-row del">Remove entry</button>{% endcall %}
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if packages %}style="display:none"{% endif %}><td class="osp-empty">No packages configured.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="osp-note">
|
||||
A package is applied when it is enabled and its osId list contains the OS being installed.
|
||||
Removing an entry here leaves the file on disk.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ==================== Hardware models (user_selections.json) ==================== #}
|
||||
<div class="card" data-section="hardware_models">
|
||||
<div class="card-header">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-cpu"></i> Hardware Models
|
||||
<span class="badge badge-secondary">{{ models | length }}</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<span class="small text-muted fw-normal mono align-self-center">Tools/user_selections.json</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="ospAddModel" {% if not has_selections %}disabled{% endif %}>
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body osp-explain border-bottom">
|
||||
<p class="mb-2">
|
||||
<strong>Media build selection, not used while imaging.</strong>
|
||||
{% set n_none = models | selectattr('fam.entries', 'equalto', 0) | list | length %}
|
||||
<details class="panel ui-list osp-models" data-section="hardware_models" {% if not models %}open{% endif %}>
|
||||
<summary>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
<h2>Hardware models</h2>
|
||||
{{ ui.chip(models | length, 'neutral') }}
|
||||
{% if n_none %}{{ ui.chip(n_none ~ ' with no driver entry', 'warn') }}{% endif %}
|
||||
<span class="sub">Media build selection, not used while imaging</span>
|
||||
</summary>
|
||||
<div class="explain">
|
||||
<p>
|
||||
Media Creator Lite saved this list when the media was built: the driver families (Family ID) whose packs it
|
||||
downloaded, and the OS it built for. <span class="mono">scripts/Download-Drivers.ps1</span> and
|
||||
<span class="mono">Upload-Image.ps1</span> read it to choose which driver packs and OS to fetch and upload.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<p>
|
||||
PESetup never reads this file: at imaging it matches the PC's WMI model against
|
||||
<a href="{{ image_tab_url(image_type, 'drivers') }}">HardwareDriver.json</a> alone, so adding a model here does not
|
||||
give a PC drivers. The Driver entries column counts map entries with the same <span class="mono">family</span>.
|
||||
give a PC drivers. Driver entries counts map entries with the same <span class="mono">family</span>.
|
||||
</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center">
|
||||
<span class="sub mono">Tools/user_selections.json</span>
|
||||
<button type="button" class="ui-btn" id="ospAddModel" {% if not has_selections %}disabled{% endif %}>Add model</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table mb-0 osp-table" id="ospModelTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Model</th>
|
||||
<th>Family ID</th>
|
||||
<th>Driver entries</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in models %}
|
||||
<tr data-json='{{ r.m | tojson_ordered }}'>
|
||||
<td class="num order-num">{{ loop.index }}</td>
|
||||
<td><input type="text" class="form-control form-control-sm" data-field="Model" value="{{ r.m.Model }}" aria-label="Model"></td>
|
||||
<td><input type="text" class="form-control form-control-sm" data-field="Id" value="{{ r.m.Id }}" aria-label="Family ID"></td>
|
||||
<td class="text-nowrap">
|
||||
<div class="mg osp-head" aria-hidden="true"><span>#</span><span class="c-model">Model</span><span class="c-fam">Family ID</span><span class="ents">Driver entries</span><span></span></div>
|
||||
<table class="osp-list" id="ospModelTable">
|
||||
<tbody>
|
||||
{% for r in models %}
|
||||
<tr data-json='{{ r.m | tojson_ordered }}'><td class="ui-row">
|
||||
<div class="mg">
|
||||
<span class="num order-num">{{ loop.index }}</span>
|
||||
<input type="text" class="field c-model" data-field="Model" value="{{ r.m.Model }}" aria-label="Model">
|
||||
<input type="text" class="field c-fam" data-field="Id" value="{{ r.m.Id }}" aria-label="Family ID">
|
||||
<span class="ents tip right"{% if r.fam.entries %} data-tip="{{ r.fam.on_disk }} of {{ r.fam.entries }} zips on disk"{% endif %}>
|
||||
{% if r.fam.entries %}
|
||||
<span class="badge {{ 'badge-success' if r.fam.on_disk == r.fam.entries else 'badge-warning' }}">{{ r.fam.entries }} entr{{ 'ies' if r.fam.entries != 1 else 'y' }}</span>
|
||||
<div class="sub">{{ r.fam.on_disk }} zip{{ 's' if r.fam.on_disk != 1 }} on disk</div>
|
||||
{{ ui.chip(r.fam.entries ~ ' entr' ~ ('ies' if r.fam.entries != 1 else 'y'), 'ok' if r.fam.on_disk == r.fam.entries else 'warn') }}
|
||||
<span class="sub">{{ r.fam.on_disk }} zip{{ 's' if r.fam.on_disk != 1 }} on disk</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">None</span>
|
||||
{{ ui.chip('None', 'off') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this model">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if models %}style="display:none"{% endif %}><td colspan="5">{% if has_selections %}No hardware models selected.{% else %}No user_selections.json for this image.{% endif %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
<button type="button" class="ui-btn ghost sm remove-row" aria-label="Remove model {{ r.m.Model }}">Remove</button>
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="empty-row" {% if models %}style="display:none"{% endif %}><td class="osp-empty">{% if has_selections %}No hardware models selected.{% else %}No user_selections.json for this image.{% endif %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<div class="savebar" role="region" aria-label="Unsaved changes">
|
||||
<span class="dirty-flag" id="ospDirty" role="status"><span class="dot" aria-hidden="true"></span> Unsaved changes</span>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button type="button" class="ui-btn" id="ospDiscard">Discard</button>
|
||||
<button type="button" class="ui-btn pri" id="ospReview" disabled>Review and save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Package upload modal (unchanged from the old Configuration page) -->
|
||||
<div class="modal fade" id="packageUploadModal" tabindex="-1">
|
||||
<!-- Package upload modal -->
|
||||
<div class="modal fade" id="packageUploadModal" tabindex="-1" aria-labelledby="pkgUploadTitle">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" action="{{ url_for('images_packages_upload', image_type=image_type) }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Upload package</h3>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h3 class="modal-title" id="pkgUploadTitle">Upload package</h3>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-field">
|
||||
<label class="form-label">Package file</label>
|
||||
<input type="file" class="form-control" name="package_file" required>
|
||||
<div class="form-text">Lands in <code>{{ image_type }}/Deploy/Packages/</code>.</div>
|
||||
<label class="form-label" for="pkgUploadFile">Package file</label>
|
||||
<input type="file" class="form-control" name="package_file" id="pkgUploadFile" required>
|
||||
<div class="form-text">Lands in <span class="mono">{{ image_type }}/Deploy/Packages/</span>.</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Destination directory (optional)</label>
|
||||
<input type="text" class="form-control mono" name="destination_dir"
|
||||
<label class="form-label" for="pkgUploadDest">Destination directory (optional)</label>
|
||||
<input type="text" class="form-control mono" name="destination_dir" id="pkgUploadDest"
|
||||
placeholder="*destinationdir*\Packages">
|
||||
<div class="form-text">Setting this also appends an entry to <code>packages.json</code>.</div>
|
||||
<div class="form-text">Setting this also appends an entry to <span class="mono">packages.json</span>.</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="overwrite" value="1" id="pkgOverwriteCheck">
|
||||
@@ -272,8 +307,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Upload</button>
|
||||
<button type="button" class="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="ui-btn pri">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -289,11 +324,13 @@
|
||||
var dirty = {};
|
||||
var reviewBtn = document.getElementById('ospReview');
|
||||
var head = document.querySelector('.image-head');
|
||||
var page = document.getElementById('ospPage');
|
||||
|
||||
function markDirty(section) {
|
||||
dirty[section] = true;
|
||||
reviewBtn.disabled = false;
|
||||
if (head) head.classList.add('dirty');
|
||||
if (page) page.classList.add('dirty');
|
||||
}
|
||||
|
||||
function rowsOf(tableId) {
|
||||
@@ -315,7 +352,15 @@
|
||||
document.querySelectorAll('[data-section]').forEach(function (card) {
|
||||
var section = card.getAttribute('data-section');
|
||||
card.addEventListener('input', function () { markDirty(section); });
|
||||
card.addEventListener('change', function () { markDirty(section); });
|
||||
card.addEventListener('change', function (e) {
|
||||
markDirty(section);
|
||||
// Keep the row's Enabled chip in step with its switch.
|
||||
if (e.target.classList.contains('pkg-enabled')) {
|
||||
var st = e.target.closest('tr').querySelector('.pkg-state');
|
||||
if (st) st.innerHTML = e.target.checked
|
||||
? '<span class="chip c-ok">Enabled</span>' : '<span class="chip c-off">Disabled</span>';
|
||||
}
|
||||
});
|
||||
card.addEventListener('click', function (e) {
|
||||
// app.js removes the row and renumbers (delegated on document, so it
|
||||
// runs after this); refresh the empty row once it is gone.
|
||||
@@ -334,11 +379,13 @@
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = 'row-new';
|
||||
tr.innerHTML =
|
||||
'<td class="num order-num"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" data-field="Model" placeholder="Model name" aria-label="Model"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" data-field="Id" placeholder="Driver family ID" aria-label="Family ID"></td>' +
|
||||
'<td><span class="badge badge-info">New</span></td>' +
|
||||
'<td class="text-end"><button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this model"><i class="bi bi-trash"></i></button></td>';
|
||||
'<td class="ui-row"><div class="mg">' +
|
||||
'<span class="num order-num"></span>' +
|
||||
'<input type="text" class="field c-model" data-field="Model" placeholder="Model name" aria-label="Model">' +
|
||||
'<input type="text" class="field c-fam" data-field="Id" placeholder="Driver family ID" aria-label="Family ID">' +
|
||||
'<span class="ents"><span class="chip c-edit">New</span></span>' +
|
||||
'<button type="button" class="ui-btn ghost sm remove-row" aria-label="Remove this model">Remove</button>' +
|
||||
'</div></td>';
|
||||
tbody.insertBefore(tr, tbody.querySelector('.empty-row'));
|
||||
rowsOf('ospModelTable').forEach(function (r, i) { r.querySelector('.order-num').textContent = i + 1; });
|
||||
refreshEmpty(tbody.closest('table'));
|
||||
@@ -391,6 +438,15 @@
|
||||
});
|
||||
});
|
||||
|
||||
var discardBtn = document.getElementById('ospDiscard');
|
||||
if (discardBtn) {
|
||||
discardBtn.addEventListener('click', function () {
|
||||
if (!confirm('Discard the unsaved changes on this page?')) return;
|
||||
dirty = {};
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
if (Object.keys(dirty).length) { e.preventDefault(); e.returnValue = ''; }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user