369 lines
17 KiB
HTML
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}} $TTL 3600 @ 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}}
|