Files
2026-08-16 21:18:45 -05:00

369 lines
17 KiB
HTML

{{define "content"}}
{{$z := .Data.Zone}}{{$csrf := .CSRF}}{{$f := .Data.Filter}}
{{$backTo := printf "/zones/%d" $z.ID}}
<nav aria-label="breadcrumb">
<ol class="breadcrumb small">
<li class="breadcrumb-item">
<a href="{{if eq (printf "%s" $z.Kind) "forward"}}/zones{{else}}/zones/reverse{{end}}">
{{$z.Kind.Label}} Zones
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{trimDot $z.Name}}</li>
</ol>
</nav>
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<h1 class="page-title mono">{{trimDot $z.Name}}</h1>
<p class="text-body-secondary mb-0">
{{if $z.Description}}{{$z.Description}} · {{end}}
serial {{$z.Serial}} · default TTL {{$z.DefaultTTL}}s ·
<span class="badge {{badgeFor $z.Enabled}}">{{statusWord $z.Enabled}}</span>
</p>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#recordModal"
data-record-new data-record-action="/zones/{{$z.ID}}/records">
<i class="bi bi-plus-lg me-1"></i>Add Record
</button>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-three-dots"></i> Zone
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/zones/{{$z.ID}}/edit"><i class="bi bi-pencil me-2"></i>Edit zone</a></li>
<li><a class="dropdown-item" href="/zones/{{$z.ID}}/export"><i class="bi bi-download me-2"></i>Export zone file</a></li>
<li><button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#importModal">
<i class="bi bi-upload me-2"></i>Import zone file</button></li>
<li><button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#cloneModal">
<i class="bi bi-copy me-2"></i>Clone zone</button></li>
</ul>
</div>
</div>
</div>
{{if .Data.Problems}}
<div class="alert alert-warning">
<div class="fw-semibold mb-2">
<i class="bi bi-exclamation-triangle me-1"></i>
{{len .Data.Problems}} record{{if ne (len .Data.Problems) 1}}s{{end}} in this zone could not be loaded and are not being served
</div>
<ul class="mb-0 small">
{{range .Data.Problems}}
<li><span class="mono">{{.Name}} {{.Type}}</span> — {{.Err}}</li>
{{end}}
</ul>
</div>
{{end}}
<div class="card" data-bulk-scope>
<div class="card-body">
<form method="get" data-autosubmit class="row g-2 align-items-center mb-3">
<div class="col-12 col-md-5 col-lg-4">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" name="q" value="{{$f.Search}}"
placeholder="Search records..." aria-label="Search records">
</div>
</div>
<div class="col-6 col-md-3 col-lg-2">
<select class="form-select form-select-sm" name="type" aria-label="Filter by record type">
<option value="">All types</option>
{{range .Data.TypesInUse}}
<option value="{{.}}" {{if eq . $f.Type}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</div>
<div class="col-6 col-md-3 col-lg-2">
<select class="form-select form-select-sm" name="status" aria-label="Filter by status">
<option value="">All statuses</option>
<option value="enabled" {{if eq $f.Enabled "enabled"}}selected{{end}}>Enabled</option>
<option value="disabled" {{if eq $f.Enabled "disabled"}}selected{{end}}>Disabled</option>
</select>
</div>
<div class="col-auto">
<noscript><button class="btn btn-sm btn-outline-secondary" type="submit">Apply</button></noscript>
{{if or $f.Search $f.Type $f.Enabled}}
<a href="{{$backTo}}" class="btn btn-sm btn-link">Clear</a>
{{end}}
</div>
</form>
<form method="post" action="/zones/{{$z.ID}}/records/bulk" id="bulkForm">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<input type="hidden" name="return_to" value="{{$backTo}}">
<div class="bulk-bar" data-bulk-bar>
<span class="fw-semibold"><span data-bulk-count>0</span> selected</span>
<div class="d-flex gap-2 ms-auto">
<button type="submit" name="action" value="enable" class="btn btn-sm btn-outline-success">
<i class="bi bi-check-lg me-1"></i>Enable
</button>
<button type="submit" name="action" value="disable" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-slash-circle me-1"></i>Disable
</button>
<button type="submit" name="action" value="delete" class="btn btn-sm btn-outline-danger js-confirm-bulk"
data-confirm="Delete the selected records? This cannot be undone.">
<i class="bi bi-trash me-1"></i>Delete
</button>
</div>
</div>
{{if .Data.Records}}
<div class="table-responsive">
<table class="table table-hover align-middle table-compact">
<thead>
<tr>
<th style="width:2.5rem;">
<input class="form-check-input" type="checkbox" data-bulk-all aria-label="Select all records">
</th>
<th>Name</th>
<th>Type</th>
<th>Value</th>
<th class="text-end">TTL</th>
<th>Status</th>
<th class="row-actions">Actions</th>
</tr>
</thead>
<tbody>
{{range .Data.Records}}
<tr {{if not .Enabled}}class="is-disabled"{{end}}>
<td>
<input class="form-check-input" type="checkbox" name="record_id" value="{{.ID}}"
data-bulk-item aria-label="Select record {{.Name}} {{.Type}}">
</td>
<td class="mono fw-semibold">{{.Name}}</td>
<td><span class="type-chip {{typeBadge .Type}}">{{.Type}}</span></td>
<td class="mono truncate-cell" title="{{.Data}}">{{.Data}}</td>
<td class="text-end">
{{if .TTL}}{{.TTL}}{{else}}<span class="text-body-secondary" title="Inherited from the zone">{{$z.DefaultTTL}}</span>{{end}}
</td>
<td><span class="badge {{badgeFor .Enabled}}">{{statusWord .Enabled}}</span></td>
<td class="row-actions">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal" data-bs-target="#recordModal"
data-record-action="/records/{{.ID}}/edit"
data-record-name="{{.Name}}"
data-record-rtype="{{.Type}}"
data-record-data="{{.Data}}"
data-record-ttl="{{if .TTL}}{{.TTL}}{{end}}"
data-record-comment="{{.Comment}}"
data-record-enabled="{{.Enabled}}"
data-record-values="{{json (rdataFields .Type .Data)}}"
title="Edit record">
<i class="bi bi-pencil"></i>
</button>
{{template "postform" dict
"Action" (printf "/records/%d/toggle" .ID) "CSRF" $csrf "ReturnTo" $backTo
"Fields" (dict "enabled" (boolstr (not .Enabled)))
"Icon" (toggleIcon .Enabled)
"Class" "btn btn-sm btn-outline-secondary"
"Title" (printf "%s this record" (toggleVerb .Enabled))}}
{{template "confirmform" dict
"Action" (printf "/records/%d/delete" .ID) "CSRF" $csrf "ReturnTo" $backTo
"Icon" "bi-trash"
"Message" (printf "Delete the %s record for %s?" .Type .Name)}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "pagination" dict "P" .Data.Pagination "Q" .Query}}
{{else if or $f.Search $f.Type $f.Enabled}}
{{template "empty" dict "Icon" "bi-search" "Title" "No records match those filters"
"Message" "Adjust or clear the filters above to see the rest of the zone."}}
{{else}}
{{template "empty" dict "Icon" "bi-list-columns-reverse" "Title" "This zone has no records yet"
"Message" "A zone needs at least an address record to be useful. The SOA and apex NS records are maintained for you automatically."}}
<div class="text-center pb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#recordModal"
data-record-new data-record-action="/zones/{{$z.ID}}/records">
<i class="bi bi-plus-lg me-1"></i>Add the first record
</button>
</div>
{{end}}
</form>
</div>
</div>
{{/* ---- Record editor modal ---- */}}
<div class="modal fade" id="recordModal" tabindex="-1" aria-hidden="true" aria-labelledby="recordModalLabel">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" id="recordForm" action="/zones/{{$z.ID}}/records">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<input type="hidden" name="return_to" value="{{$backTo}}">
<div class="modal-header">
<h5 class="modal-title" id="recordModalLabel">Add record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="recordEditor"
data-types="{{json .Data.RecordTypes}}" data-values="{}">
<div class="row g-3 mb-3">
<div class="col-12 col-md-6">
<label class="form-label" for="recordName">Name</label>
<div class="input-group">
<input type="text" class="form-control mono" id="recordName" name="name"
value="@" placeholder="@" required autocomplete="off">
<span class="input-group-text mono">.{{trimDot $z.Name}}</span>
</div>
<div class="form-text">Use <code>@</code> for the zone apex, or <code>*</code> for a wildcard.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="recordType">Type</label>
<select class="form-select" id="recordType" name="type" data-record-type>
<optgroup label="Common">
{{range .Data.CommonTypes}}
<option value="{{.Type}}">{{.Type}} — {{.Description}}</option>
{{end}}
</optgroup>
<optgroup label="All types">
{{range .Data.RecordTypes}}
{{if ne .Type "RAW"}}
<option value="{{.Type}}">{{.Type}} — {{.Description}}</option>
{{end}}
{{end}}
</optgroup>
</select>
</div>
</div>
<div class="row g-3" data-record-fields></div>
<div class="d-none" data-record-raw>
<label class="form-label" for="recordData">Record data</label>
<textarea class="form-control mono" id="recordData" name="data" rows="3"
placeholder="Rdata exactly as it appears in a zone file"></textarea>
<div class="form-text">
Enter the record data by hand. Unknown types use the RFC 3597 form,
for example <code>\# 4 0A0B0C0D</code>.
</div>
</div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" role="switch" id="recordAdvanced"
name="advanced" value="true" data-record-advanced>
<label class="form-check-label" for="recordAdvanced">
Advanced: enter record data by hand
</label>
</div>
<hr>
<div class="row g-3">
<div class="col-6 col-md-4">
<label class="form-label" for="recordTTL">TTL</label>
<input type="number" class="form-control" id="recordTTL" name="ttl"
placeholder="{{$z.DefaultTTL}}" min="1" max="604800">
<div class="form-text">Blank inherits the zone default.</div>
</div>
<div class="col-6 col-md-8">
<label class="form-label" for="recordComment">Comment</label>
<input type="text" class="form-control" id="recordComment" name="comment"
placeholder="Optional note for other administrators">
</div>
</div>
<div class="form-check form-switch mt-3">
<input type="hidden" name="enabled" value="false">
<input class="form-check-input" type="checkbox" role="switch" id="recordEnabled"
name="enabled" value="true" checked>
<label class="form-check-label" for="recordEnabled">Enabled</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save record</button>
</div>
</form>
</div>
</div>
</div>
{{/* ---- Import modal ---- */}}
<div class="modal fade" id="importModal" tabindex="-1" aria-hidden="true" aria-labelledby="importModalLabel">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form method="post" action="/zones/{{$z.ID}}/import" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<div class="modal-header">
<h5 class="modal-title" id="importModalLabel">Import a BIND zone file</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label" for="zoneFile">Zone file</label>
<input type="file" class="form-control" id="zoneFile" name="file" accept=".zone,.txt,.db,text/plain">
</div>
<div class="mb-3">
<label class="form-label" for="zonePaste">…or paste its contents</label>
<textarea class="form-control mono" id="zonePaste" name="content" rows="8"
placeholder="$ORIGIN {{$z.Name}}&#10;$TTL 3600&#10;@ IN A 192.0.2.10"></textarea>
</div>
<div class="mb-2">
<label class="form-label" for="importMode">If the zone already has records</label>
<select class="form-select" id="importMode" name="mode">
<option value="merge">Merge — keep existing records and add the imported ones</option>
<option value="replace">Replace — delete every existing record first</option>
</select>
</div>
<p class="form-text mb-0">
The whole file is checked before anything is written, so a syntax error
never leaves the zone half-imported. The SOA is read for its timers;
serial management stays with this server.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Import</button>
</div>
</form>
</div>
</div>
</div>
{{/* ---- Clone modal ---- */}}
<div class="modal fade" id="cloneModal" tabindex="-1" aria-hidden="true" aria-labelledby="cloneModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/zones/{{$z.ID}}/clone">
<input type="hidden" name="_csrf" value="{{$csrf}}">
<div class="modal-header">
<h5 class="modal-title" id="cloneModalLabel">Clone {{trimDot $z.Name}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label" for="cloneName">New zone name</label>
<input type="text" class="form-control mono" id="cloneName" name="name"
placeholder="example.net" required autocomplete="off">
</div>
<div class="mb-3">
<label class="form-label" for="cloneDescription">Description</label>
<input type="text" class="form-control" id="cloneDescription" name="description"
value="Cloned from {{trimDot $z.Name}}">
</div>
<p class="form-text mb-0">
Every record is copied. References to <span class="mono">{{trimDot $z.Name}}</span>
inside record data are rewritten to the new name so the clone is self-consistent.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Clone zone</button>
</div>
</form>
</div>
</div>
</div>
{{end}}