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,164 +1,151 @@
|
||||
{% extends "base.html" %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}Image Import - PXE Server Manager{% endblock %}
|
||||
|
||||
{# Image import: two panels (network upload folder, USB drive), then the
|
||||
images one line each. Submitting asks for confirmation (upload-folder
|
||||
sources are MOVED, same-name files are overwritten) and then disables the
|
||||
button with a spinner while the import runs. #}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.imp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); gap: 22px; align-items: start; }
|
||||
.imp-panel { padding: 20px 24px 22px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.imp-panel h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
.imp-panel .lead { margin: 2px 0 0; font-size: 13px; color: var(--muted); }
|
||||
.imp-panel form { display: flex; flex-direction: column; gap: 16px; margin: 0; }
|
||||
.imp-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.imp-field label { font-size: 13px; font-weight: 600; color: var(--text-2); }
|
||||
.imp-field .field { width: 100%; }
|
||||
.imp-field .sub { line-height: 1.5; }
|
||||
.imp-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.imp-empty { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; font-size: 14px; color: var(--text-2); line-height: 1.6; }
|
||||
.imp-empty p { margin: 0; }
|
||||
|
||||
.ig { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr) 92px 112px auto; align-items: center; gap: 0 14px;
|
||||
padding: 11px 24px; font-size: 14px; border-top: 1px solid var(--line); }
|
||||
.ig.ihead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; border-top: 0; }
|
||||
.ig .name { font-weight: 600; }
|
||||
.ig a.ui-btn { justify-self: end; }
|
||||
.imp-phead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 16px 24px 10px; }
|
||||
.imp-phead h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
@container (max-width: 720px) {
|
||||
.ig { grid-template-columns: minmax(0, 1fr) auto auto; padding: 11px 16px; gap: 6px 10px; }
|
||||
.ig .c-path { grid-column: 1 / -1; grid-row: 2; }
|
||||
.ig .c-un { display: none; }
|
||||
.ig.ihead { display: none; }
|
||||
.imp-phead { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
@media (max-width: 600px) { .imp-panel { padding: 16px; } }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-box-arrow-in-down"></i> Image Import</h1>
|
||||
{{ ui.head('Image import', 'Images', "Copy WinPE deployment content into an image's Deploy folder, from the network upload share or a USB drive.") }}
|
||||
|
||||
<div class="ui-stack">
|
||||
<div class="imp-cols">
|
||||
|
||||
<!-- Network upload import -->
|
||||
<section class="panel imp-panel" aria-labelledby="imp-up-h">
|
||||
<div>
|
||||
<h2 id="imp-up-h">From the network upload share</h2>
|
||||
<p class="lead">Files copied over SMB to <span class="mono">\\172.16.9.1\image-upload</span>. They are moved, not copied.</p>
|
||||
</div>
|
||||
{% if upload_sources %}
|
||||
<form method="POST" id="uploadImportForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="imp-field">
|
||||
<label for="uploadSource">Source</label>
|
||||
<select class="field" name="source" id="uploadSource" required>
|
||||
<option value="">Select an upload folder</option>
|
||||
{% for src in upload_sources %}
|
||||
<option value="{{ src }}">{{ src }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="imp-field">
|
||||
<label for="uploadTarget">Target image</label>
|
||||
<select class="field" name="target" id="uploadTarget" required>
|
||||
<option value="">Select the image to import into</option>
|
||||
{% for it in image_types %}
|
||||
<option value="{{ it }}">{{ friendly_names[it] }} ({{ it }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="sub">Content goes into the image's Deploy folder. Out-of-box Drivers are pooled once in a shared folder and linked from each image to save disk space.</span>
|
||||
</div>
|
||||
<div class="notice"><span><strong>The files are moved out of the upload folder</strong>, so the source is used up.
|
||||
Files with the same name in the image are overwritten. Large images take several minutes.</span></div>
|
||||
<div class="imp-actions">
|
||||
<button type="submit" class="ui-btn pri" id="uploadImportBtn">Start import</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="imp-empty">
|
||||
{{ ui.chip('Nothing uploaded', 'off') }}
|
||||
<p>Map <span class="mono">\\172.16.9.1\image-upload</span> on your Windows PC and copy the Deploy folder contents there, then refresh.</p>
|
||||
<button type="button" class="ui-btn" onclick="location.reload()">Refresh</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- USB import -->
|
||||
<section class="panel imp-panel" aria-labelledby="imp-usb-h">
|
||||
<div>
|
||||
<h2 id="imp-usb-h">From a USB drive</h2>
|
||||
<p class="lead">A drive mounted under <span class="mono">/mnt/</span> or <span class="mono">/media/</span>. Files are copied.</p>
|
||||
</div>
|
||||
{% if usb_mounts %}
|
||||
<form method="POST" id="importForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="imp-field">
|
||||
<label for="source">Source (USB mount point)</label>
|
||||
<select class="field" name="source" id="source" required>
|
||||
<option value="">Select a mounted USB drive</option>
|
||||
{% for mount in usb_mounts %}
|
||||
<option value="{{ mount }}">{{ mount }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="sub">The drive holding the WinPE deployment content.</span>
|
||||
</div>
|
||||
<div class="imp-field">
|
||||
<label for="target">Target image</label>
|
||||
<select class="field" name="target" id="target" required>
|
||||
<option value="">Select the image to import into</option>
|
||||
{% for it in image_types %}
|
||||
<option value="{{ it }}">{{ friendly_names[it] }} ({{ it }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="sub">Content goes into the image's Deploy folder.</span>
|
||||
</div>
|
||||
<div class="notice"><span>Files with the same name in the image are overwritten. Large images take several minutes.</span></div>
|
||||
<div class="imp-actions">
|
||||
<button type="submit" class="ui-btn pri" id="importBtn">Start import</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="imp-empty">
|
||||
{{ ui.chip('No USB drive', 'off') }}
|
||||
<p>No mounted USB drive was found under <span class="mono">/mnt/</span> or <span class="mono">/media/</span>. Mount one and refresh.</p>
|
||||
<button type="button" class="ui-btn" onclick="location.reload()">Refresh</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<!-- Network Upload Import -->
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-hdd-network"></i> Import from Network Upload</div>
|
||||
{% if upload_sources %}
|
||||
<form method="POST" id="uploadImportForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-field">
|
||||
<label for="uploadSource" class="form-label">Source</label>
|
||||
<select class="form-select" name="source" id="uploadSource" required>
|
||||
<option value="">-- Select upload source --</option>
|
||||
{% for src in upload_sources %}
|
||||
<option value="{{ src }}">{{ src }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Files uploaded via SMB to <code class="mono">\\172.16.9.1\image-upload</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="uploadTarget" class="form-label">Target Image Type</label>
|
||||
<select class="form-select" name="target" id="uploadTarget" required>
|
||||
<option value="">-- Select target image --</option>
|
||||
{% for it in image_types %}
|
||||
<option value="{{ it }}">{{ friendly_names[it] }} ({{ it }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Content will be copied into the Deploy directory. Shared resources
|
||||
(Out-of-box Drivers) are stored once and linked across all image types.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled
|
||||
into a shared directory and symlinked for each image type to save disk space.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
||||
same names will be overwritten. This operation may take several minutes for large
|
||||
images.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" id="uploadImportBtn">
|
||||
<i class="bi bi-play-fill"></i> Start Import
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-cloud-slash" style="font-size:2rem;color:var(--text-light);"></i>
|
||||
<h5 class="mt-3" style="color:var(--text-light);">No Upload Content Found</h5>
|
||||
<p class="mb-0" style="color:var(--text-light);">
|
||||
Map <code class="mono">\\172.16.9.1\image-upload</code> on your Windows PC and copy
|
||||
the Deploy directory contents there.
|
||||
</p>
|
||||
<button class="pxe-btn pxe-btn-ghost pxe-btn-sm mt-3" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- USB Import -->
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-usb-drive"></i> Import from USB Drive</div>
|
||||
{% if usb_mounts %}
|
||||
<form method="POST" id="importForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-field">
|
||||
<label for="source" class="form-label">Source (USB Mount Point)</label>
|
||||
<select class="form-select" name="source" id="source" required>
|
||||
<option value="">-- Select a mounted USB drive --</option>
|
||||
{% for mount in usb_mounts %}
|
||||
<option value="{{ mount }}">{{ mount }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Select the mounted USB drive containing the WinPE deployment content.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="target" class="form-label">Target Image Type</label>
|
||||
<select class="form-select" name="target" id="target" required>
|
||||
<option value="">-- Select target image --</option>
|
||||
{% for it in image_types %}
|
||||
<option value="{{ it }}">{{ friendly_names[it] }} ({{ it }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Content will be copied into the Deploy directory for this image type.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
||||
same names will be overwritten. This operation may take several minutes for large
|
||||
images.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" id="importBtn">
|
||||
<i class="bi bi-play-fill"></i> Start Import
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-usb-drive" style="font-size:2rem;color:var(--text-light);"></i>
|
||||
<h5 class="mt-3" style="color:var(--text-light);">No USB Drives Detected</h5>
|
||||
<p class="mb-0" style="color:var(--text-light);">
|
||||
No mounted USB drives were found under <code class="mono">/mnt/</code> or <code class="mono">/media/</code>.<br>
|
||||
Mount a USB drive and refresh this page.
|
||||
</p>
|
||||
<button class="pxe-btn pxe-btn-ghost pxe-btn-sm mt-3" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-list-check"></i> Current Image Status</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Content</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for img in images %}
|
||||
<tr>
|
||||
<td>{{ img.friendly_name }}</td>
|
||||
<td>
|
||||
{% if img.has_content %}
|
||||
<span class="badge badge-success">Present</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Empty</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<section class="panel ui-list" aria-labelledby="imp-st-h">
|
||||
<div class="imp-phead"><h2 id="imp-st-h">Images</h2>{{ ui.chip(images | length, 'neutral') }}</div>
|
||||
<div class="ig ihead" aria-hidden="true"><span>Image</span><span class="c-path">Deploy folder</span><span>Content</span><span class="c-un">Unattend</span><span></span></div>
|
||||
{% for img in images %}
|
||||
<div class="ig">
|
||||
<span class="tip" data-tip="{{ img.friendly_name }}"><span class="trunc name">{{ img.friendly_name }}</span></span>
|
||||
<span class="tip c-path" data-tip="{{ img.deploy_path }}"><span class="trunc mono sub">{{ img.image_type }}/Deploy</span></span>
|
||||
<span>{% if img.has_content %}{{ ui.chip('Present', 'ok') }}{% else %}{{ ui.chip('Empty', 'off') }}{% endif %}</span>
|
||||
<span class="c-un">{% if img.has_unattend %}{{ ui.chip('Unattend', 'neutral') }}{% else %}<span class="sub">No unattend</span>{% endif %}</span>
|
||||
{% if img.image_type in image_types %}<a class="ui-btn sm" href="{{ image_tab_url(img.image_type, 'overview') }}" aria-label="Open {{ img.friendly_name }}">Open</a>{% else %}<span></span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -181,7 +168,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!confirm(msg)) { e.preventDefault(); return; }
|
||||
var btn = form.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Importing...';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Importing...';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user