Zone page: links to the same zone in the other views
The header says "also in VIEW" for every zone of the same name in another view, linked or not, in the view's colour, keeping the open tab where the other zone has it. The zone API gains in_views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a36c54ce60
commit
4cc2ab2287
7 files changed
+58
-2
No files matched your search
@@ -8,6 +8,10 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- A zone's page links to the same zone in the other views ("also in
|
||||||
|
external"), in each view's colour, keeping the tab you are on. It
|
||||||
|
works for zones that aren't linked too, and the zone API's new
|
||||||
|
`in_views` lists them.
|
||||||
- `jarl.service` has the alias `jarld.service`, so `systemctl restart
|
- `jarl.service` has the alias `jarld.service`, so `systemctl restart
|
||||||
jarld` works too. The alias is linked when the unit is enabled; the
|
jarld` works too. The alias is linked when the unit is enabled; the
|
||||||
package relinks a unit enabled by an older version on upgrade.
|
package relinks a unit enabled by an older version on upgrade.
|
||||||
|
|||||||
+4
-1
@@ -363,7 +363,10 @@ In a linked zone ([views.md](views.md#linked-zones)), records carry
|
|||||||
(`PATCH /records/{id}`) and setting (`PUT .../rrsets/...`) take `"local":
|
(`PATCH /records/{id}`) and setting (`PUT .../rrsets/...`) take `"local":
|
||||||
true` (this view only, an override) or `false` (shared; on an update or a
|
true` (this view only, an override) or `false` (shared; on an update or a
|
||||||
set it makes this view's version every view's). Zones carry `link_group`
|
set it makes this view's version every view's). Zones carry `link_group`
|
||||||
and `linked_with` (the other views).
|
and `linked_with` (the other views). Every zone in a view carries
|
||||||
|
`in_views`: the zones of the same name in the other views, linked or not
|
||||||
|
and of any type, in view order (`[{"id": 289, "view": "external", "type":
|
||||||
|
"primary", "linked": true}]`).
|
||||||
|
|
||||||
| Method | Path | Body |
|
| Method | Path | Body |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ shows one (it needs `tsig:write` and is audited). Key names starting with
|
|||||||
`jarl-view-` are reserved, and the view keys can be used in your own
|
`jarl-view-` are reserved, and the view keys can be used in your own
|
||||||
options (`allow-transfer { key jarl-view-external; }`).
|
options (`allow-transfer { key jarl-view-external; }`).
|
||||||
|
|
||||||
|
**Moving between views.** A zone's page lists the other views that have a
|
||||||
|
zone of the same name ("also in external", in each view's colour). Each
|
||||||
|
one links straight to that zone, on the tab you are on.
|
||||||
|
|
||||||
## Linked zones
|
## Linked zones
|
||||||
|
|
||||||
Most split-horizon zones are the same in every view apart from a few
|
Most split-horizon zones are the same in every view apart from a few
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type Zone struct {
|
|||||||
LinkedWith []string `json:"linked_with,omitempty"`
|
LinkedWith []string `json:"linked_with,omitempty"`
|
||||||
// Links are the zones this one is linked with, with their views.
|
// Links are the zones this one is linked with, with their views.
|
||||||
Links []ZoneLink `json:"links,omitempty"`
|
Links []ZoneLink `json:"links,omitempty"`
|
||||||
|
// InViews are the zones of this name in the other views (any type),
|
||||||
|
// in view order: for going straight to one.
|
||||||
|
InViews []ZoneLink `json:"in_views,omitempty"`
|
||||||
// OtherViews are the other views with a primary zone of this name
|
// OtherViews are the other views with a primary zone of this name
|
||||||
// (linked or not): what GET /zones/{zone}/compare compares.
|
// (linked or not): what GET /zones/{zone}/compare compares.
|
||||||
OtherViews []string `json:"other_views,omitempty"`
|
OtherViews []string `json:"other_views,omitempty"`
|
||||||
@@ -628,6 +631,9 @@ type BlockRule struct {
|
|||||||
type ZoneLink struct {
|
type ZoneLink struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
View string `json:"view"`
|
View string `json:"view"`
|
||||||
|
// Type and Linked are set in Zone.InViews.
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Linked bool `json:"linked,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncDNSSummary is what the dashboard says about encrypted DNS.
|
// EncDNSSummary is what the dashboard says about encrypted DNS.
|
||||||
|
|||||||
@@ -35,3 +35,17 @@ func TestDNSSECWhereDSGoes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A zone lists the zones of its name in the other views.
|
||||||
|
func TestZoneInViews(t *testing.T) {
|
||||||
|
e := newAPIEnv(t)
|
||||||
|
e.do("POST", "/views", e.token, map[string]any{"name": "internal", "match_clients": []string{"10.0.0.0/8"}}, 201)
|
||||||
|
e.do("POST", "/views", e.token, map[string]any{"name": "external"}, 201)
|
||||||
|
e.do("POST", "/zones", e.token, map[string]any{"name": "adbraun.lol", "view": "internal", "nameservers": []string{"ns1.example.net."}}, 201)
|
||||||
|
e.do("POST", "/zones", e.token, map[string]any{"name": "adbraun.lol", "view": "external", "nameservers": []string{"ns1.example.net."}}, 201)
|
||||||
|
z := e.do("GET", "/zones/adbraun.lol?view=internal", e.token, nil, 200)
|
||||||
|
in, _ := z["in_views"].([]any)
|
||||||
|
if len(in) != 1 || in[0].(map[string]any)["view"] != "external" || in[0].(map[string]any)["type"] != "primary" {
|
||||||
|
t.Errorf("in_views = %v", z["in_views"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,15 @@ func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if z.ViewID != 0 {
|
||||||
|
others := st.ZonesNamed(z.Name)
|
||||||
|
st.SortByView(others)
|
||||||
|
for _, o := range others {
|
||||||
|
if o.ID != z.ID {
|
||||||
|
out.InViews = append(out.InViews, api.ZoneLink{ID: o.ID, View: st.ViewName(o), Type: o.Type, Linked: z.LinkGroup != 0 && o.LinkGroup == z.LinkGroup})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if pf, ok := dnsname.PrefixFromReverse(z.Name); ok {
|
if pf, ok := dnsname.PrefixFromReverse(z.Name); ok {
|
||||||
out.Prefix = pf.String()
|
out.Prefix = pf.String()
|
||||||
if z.Type == model.ZonePrimary {
|
if z.Type == model.ZonePrimary {
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import { registrarTab } from "./registrar.js";
|
|||||||
|
|
||||||
let renderSeq = 0;
|
let renderSeq = 0;
|
||||||
|
|
||||||
|
// tabIn is the "/tab" part of the address of zone o (an in_views entry)
|
||||||
|
// that keeps the tab open now, when o has that tab.
|
||||||
|
function tabIn(o, tab) {
|
||||||
|
if (!tab || tab === "registrar") return "";
|
||||||
|
if (["records", "file", "dnssec", "compare"].includes(tab) && o.type !== "primary") return "";
|
||||||
|
if (tab === "servers" && !["primary", "secondary", "mirror"].includes(o.type)) return "";
|
||||||
|
return "/" + tab;
|
||||||
|
}
|
||||||
|
|
||||||
export async function render(el, params, query) {
|
export async function render(el, params, query) {
|
||||||
const id = params.id;
|
const id = params.id;
|
||||||
const [zone, views] = await Promise.all([get("/zones/" + id, draftQuery()), get("/views")]);
|
const [zone, views] = await Promise.all([get("/zones/" + id, draftQuery()), get("/views")]);
|
||||||
@@ -33,12 +42,19 @@ export async function render(el, params, query) {
|
|||||||
badge(zone.type, "accent"), " ",
|
badge(zone.type, "accent"), " ",
|
||||||
zone.pending ? [pendingBadge(zone.pending), " "] : null,
|
zone.pending ? [pendingBadge(zone.pending), " "] : null,
|
||||||
zone.view ? ["view ", viewLink(view, zone.view, "#/zones/" + id + (params.tab ? "/" + params.tab : "")), " · "] : null,
|
zone.view ? ["view ", viewLink(view, zone.view, "#/zones/" + id + (params.tab ? "/" + params.tab : "")), " · "] : null,
|
||||||
|
zone.in_views ? [h("span", { title: "The zone " + zone.display_name + " in another view" }, "also in ",
|
||||||
|
zone.in_views.map((o, i) => {
|
||||||
|
const v = views.find((x) => x.name === o.view);
|
||||||
|
return [i ? ", " : "", h("a", { href: "#/zones/" + o.id + tabIn(o, params.tab), class: v && v.color ? "vc-" + v.color : null,
|
||||||
|
title: "Go to " + zone.display_name + " in view " + o.view + (o.type !== zone.type ? " (" + o.type + ")" : "") + (o.linked ? ", linked with this one" : "") }, o.view),
|
||||||
|
o.type !== zone.type ? h("span", { class: "muted" }, " (" + o.type + ")") : null];
|
||||||
|
})), " · "] : null,
|
||||||
zone.type === "primary" ? "serial " + zone.soa.serial + " · " : null,
|
zone.type === "primary" ? "serial " + zone.soa.serial + " · " : null,
|
||||||
zone.prefix ? h("span", { class: "mono" }, zone.prefix + " · ") : null,
|
zone.prefix ? h("span", { class: "mono" }, zone.prefix + " · ") : null,
|
||||||
zone.auto_ptr ? [h("a", { href: "#/zones/" + id + "/settings", title: "Adding, changing, disabling or deleting an A or AAAA record does the same to its PTR record" }, badge("PTRs kept in step", "ok")), " "] : null,
|
zone.auto_ptr ? [h("a", { href: "#/zones/" + id + "/settings", title: "Adding, changing, disabling or deleting an A or AAAA record does the same to its PTR record" }, badge("PTRs kept in step", "ok")), " "] : null,
|
||||||
zone.dnssec_policy ? [h("a", { href: "#/zones/" + id + "/dnssec" }, badge(zone.dnssec_policy === "insecure" ? "DNSSEC: turning off" : "DNSSEC", zone.dnssec_policy === "insecure" ? "warn" : "ok")), " "] : null,
|
zone.dnssec_policy ? [h("a", { href: "#/zones/" + id + "/dnssec" }, badge(zone.dnssec_policy === "insecure" ? "DNSSEC: turning off" : "DNSSEC", zone.dnssec_policy === "insecure" ? "warn" : "ok")), " "] : null,
|
||||||
zone.links ? [h("span", { class: "badge accent" }, "linked with ",
|
zone.links ? [h("span", { class: "badge accent" }, "linked with ",
|
||||||
zone.links.map((l, i) => [i ? ", " : "", h("a", { href: "#/zones/" + l.id + (params.tab ? "/" + params.tab : ""), title: "The zone in view " + l.view }, l.view)])), " "] : null,
|
zone.links.map((l, i) => [i ? ", " : "", h("a", { href: "#/zones/" + l.id + tabIn({ type: zone.type }, params.tab), title: "The zone in view " + l.view }, l.view)])), " "] : null,
|
||||||
zone.zone_file ? h("span", { class: "mono small" }, zone.zone_file) : null)),
|
zone.zone_file ? h("span", { class: "mono small" }, zone.zone_file) : null)),
|
||||||
h("div", { class: "actions" },
|
h("div", { class: "actions" },
|
||||||
zone.disabled
|
zone.disabled
|
||||||
|
|||||||
Reference in new issue
Block a user