Added settings, proper record renaming
This commit is contained in:
parent
2ae93b6fd4
commit
7d7c0c9570
10 changed files with 444 additions and 85 deletions
86
dns.go
86
dns.go
|
|
@ -15,9 +15,9 @@ type DNSRecord struct {
|
||||||
TTL string
|
TTL string
|
||||||
Type string
|
Type string
|
||||||
ParsedValue string // not from RouterOS, here to not having to have the value logics in frontend too.
|
ParsedValue string // not from RouterOS, here to not having to have the value logics in frontend too.
|
||||||
|
MatchSubdomain string
|
||||||
|
|
||||||
Address string
|
routerosEntry DNSEntry
|
||||||
CNAME string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSEntry struct {
|
type DNSEntry struct {
|
||||||
|
|
@ -29,6 +29,12 @@ type DNSEntry struct {
|
||||||
|
|
||||||
Address string `json:"address,omitempty"`
|
Address string `json:"address,omitempty"`
|
||||||
CNAME string `json:"cname,omitempty"`
|
CNAME string `json:"cname,omitempty"`
|
||||||
|
ForwardTo string `json:"forward-to,omitempty"`
|
||||||
|
NS string `json:"ns,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
|
||||||
|
AddressList string `json:"address-list,omitempty"`
|
||||||
|
MatchSubdomain string `json:"match-subdomain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DomainPart struct {
|
type DomainPart struct {
|
||||||
|
|
@ -66,22 +72,52 @@ func SortDNSRecord(a, b DNSRecord) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r DNSRecord) String() string {
|
func NewDNSRecord(e DNSEntry) (r DNSRecord) {
|
||||||
|
r.routerosEntry = e
|
||||||
|
|
||||||
|
r.ID = e.ID
|
||||||
|
r.Disabled = e.Disabled
|
||||||
|
r.Name = e.Name
|
||||||
|
r.TTL = e.TTL
|
||||||
|
r.Type = e.Type
|
||||||
|
r.MatchSubdomain = e.MatchSubdomain
|
||||||
|
|
||||||
|
// Some routeros instances doesn't provide "type" when record is of type "A" :'(
|
||||||
|
if r.Type == "" {
|
||||||
|
r.Type = "A"
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ParsedValue = r.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (r DNSRecord) String() string { // {{{
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case "A", "AAAA":
|
case "A", "AAAA":
|
||||||
return r.Address
|
return r.routerosEntry.Address
|
||||||
|
|
||||||
case "CNAME":
|
case "CNAME":
|
||||||
return r.CNAME
|
return r.routerosEntry.CNAME
|
||||||
|
|
||||||
|
case "FWD":
|
||||||
|
return r.routerosEntry.ForwardTo
|
||||||
|
|
||||||
|
case "NS":
|
||||||
|
return r.routerosEntry.NS
|
||||||
|
|
||||||
|
case "NXDOMAIN":
|
||||||
|
return r.routerosEntry.AddressList
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
return r.routerosEntry.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("Implement type '%s'", r.Type)
|
return fmt.Sprintf("Implement type '%s'", r.Type)
|
||||||
}
|
} // }}}
|
||||||
|
func (r DNSRecord) Parts() int { // {{{
|
||||||
func (r DNSRecord) Parts() int {
|
|
||||||
return len(strings.Split(r.Name, "."))
|
return len(strings.Split(r.Name, "."))
|
||||||
}
|
} // }}}
|
||||||
|
func (r DNSRecord) Part(numParts int) string { // {{{
|
||||||
func (r DNSRecord) Part(numParts int) string {
|
|
||||||
splitName := strings.Split(r.Name, ".")
|
splitName := strings.Split(r.Name, ".")
|
||||||
slices.Reverse(splitName)
|
slices.Reverse(splitName)
|
||||||
|
|
||||||
|
|
@ -95,27 +131,39 @@ func (r DNSRecord) Part(numParts int) string {
|
||||||
|
|
||||||
slices.Reverse(parts)
|
slices.Reverse(parts)
|
||||||
return strings.Join(parts, ".")
|
return strings.Join(parts, ".")
|
||||||
}
|
} // }}}
|
||||||
|
func (r DNSRecord) NameReversed() string { // {{{
|
||||||
func (r DNSRecord) NameReversed() string {
|
|
||||||
parts := strings.Split(r.Name, ".")
|
parts := strings.Split(r.Name, ".")
|
||||||
slices.Reverse(parts)
|
slices.Reverse(parts)
|
||||||
return strings.Join(parts, ".")
|
return strings.Join(parts, ".")
|
||||||
}
|
} // }}}
|
||||||
|
func (r DNSRecord) toDNSEntry() (e DNSEntry) { // {{{
|
||||||
func (r DNSRecord) toDNSEntry() (e DNSEntry) {
|
|
||||||
e.ID = r.ID
|
e.ID = r.ID
|
||||||
e.Disabled = r.Disabled
|
e.Disabled = r.Disabled
|
||||||
e.Name = r.Name
|
e.Name = r.Name
|
||||||
e.TTL = r.TTL
|
e.TTL = r.TTL
|
||||||
e.Type = r.Type
|
e.Type = r.Type
|
||||||
|
e.MatchSubdomain = r.MatchSubdomain
|
||||||
|
|
||||||
switch(r.Type) {
|
switch r.Type {
|
||||||
case "A", "AAAA":
|
case "A", "AAAA":
|
||||||
e.Address = r.ParsedValue
|
e.Address = r.ParsedValue
|
||||||
|
|
||||||
case "CNAME":
|
case "CNAME":
|
||||||
e.CNAME = r.ParsedValue
|
e.CNAME = r.ParsedValue
|
||||||
|
|
||||||
|
case "FWD":
|
||||||
|
e.ForwardTo = r.ParsedValue
|
||||||
|
|
||||||
|
case "NS":
|
||||||
|
e.NS = r.ParsedValue
|
||||||
|
|
||||||
|
case "NXDOMAIN":
|
||||||
|
e.AddressList = r.ParsedValue
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
e.Text = r.ParsedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
} // }}}
|
||||||
|
|
|
||||||
13
main.go
13
main.go
|
|
@ -26,7 +26,7 @@ var (
|
||||||
device RouterosDevice
|
device RouterosDevice
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {// {{{
|
func init() { // {{{
|
||||||
initLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
|
initLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
|
||||||
|
|
||||||
confDir, err := os.UserConfigDir()
|
confDir, err := os.UserConfigDir()
|
||||||
|
|
@ -49,12 +49,14 @@ func init() {// {{{
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = initLogging(config)
|
logger = initLogging(config)
|
||||||
}// }}}
|
} // }}}
|
||||||
func initLogging(config Config) (*slog.Logger) {// {{{
|
func initLogging(config Config) *slog.Logger { // {{{
|
||||||
opts := slog.HandlerOptions{}
|
opts := slog.HandlerOptions{}
|
||||||
if flagDebug {
|
if flagDebug {
|
||||||
opts.Level = slog.LevelDebug
|
opts.Level = slog.LevelDebug
|
||||||
}
|
handler := slog.NewJSONHandler(os.Stdout, &opts)
|
||||||
|
return slog.New(handler)
|
||||||
|
} else {
|
||||||
handler := vlog.New(
|
handler := vlog.New(
|
||||||
os.Stdout,
|
os.Stdout,
|
||||||
opts,
|
opts,
|
||||||
|
|
@ -65,7 +67,8 @@ func initLogging(config Config) (*slog.Logger) {// {{{
|
||||||
config.Logging.Instance,
|
config.Logging.Instance,
|
||||||
)
|
)
|
||||||
return slog.New(handler)
|
return slog.New(handler)
|
||||||
}// }}}
|
}
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initLogger.Info("application", "version", VERSION)
|
initLogger.Info("application", "version", VERSION)
|
||||||
|
|
|
||||||
|
|
@ -106,23 +106,25 @@ func (dev *RouterosDevice) GetIdentity() (identity string, err error) { // {{{
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func (dev *RouterosDevice) StaticDNSEntries() (entries []*DNSRecord, err error) {
|
func (dev *RouterosDevice) StaticDNSEntries() (records []DNSRecord, err error) {
|
||||||
entries = []*DNSRecord{}
|
records = []DNSRecord{}
|
||||||
|
|
||||||
|
var routerosEntries []DNSEntry
|
||||||
var body []byte
|
var body []byte
|
||||||
body, err = dev.query("GET", "/ip/dns/static", []byte{})
|
body, err = dev.query("GET", "/ip/dns/static", []byte{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, &entries)
|
err = json.Unmarshal(body, &routerosEntries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range routerosEntries {
|
||||||
entry.ParsedValue = entry.String()
|
records = append(records, NewDNSRecord(entry))
|
||||||
}
|
}
|
||||||
|
logger.Info("FOO", "entry", records)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,64 @@ html {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
|
|
||||||
|
/* Boxed folders are a settings for the user. */
|
||||||
|
&.boxed-folders {
|
||||||
|
#records-tree {
|
||||||
|
.folder {
|
||||||
|
&>.label {
|
||||||
|
background-color: var(--label-background);
|
||||||
|
width: min-content;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--label-border);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.subfolders {
|
||||||
|
margin-left: 18px;
|
||||||
|
/* Default 10px */
|
||||||
|
padding-bottom: 8px;
|
||||||
|
min-height: 8px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
border-left: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.records {
|
||||||
|
margin-left: 18px;
|
||||||
|
/* Default 10px */
|
||||||
|
|
||||||
|
&>img {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#records-tree {
|
#records-tree {
|
||||||
|
|
@ -72,16 +126,6 @@ body {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
/*
|
|
||||||
background-color: var(--label-background);
|
|
||||||
width: min-content;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--label-border);
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
*/
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
|
@ -120,6 +164,17 @@ body {
|
||||||
.fqdn {
|
.fqdn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.subdomains {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.match-subdomains {
|
||||||
|
.subdomains {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.first-label {
|
.first-label {
|
||||||
color: var(--label-first);
|
color: var(--label-first);
|
||||||
|
|
@ -160,7 +215,6 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#record-dialog {
|
#record-dialog {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
|
|
@ -174,7 +228,15 @@ body {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input, select, button {
|
#settings-dialog {
|
||||||
font-size: 1em;
|
display: grid;
|
||||||
padding: 4px 8px;
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 6px 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
static/images/icon_settings.svg
Normal file
67
static/images/icon_settings.svg
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="20.861649"
|
||||||
|
height="18.000528"
|
||||||
|
viewBox="0 0 5.5196444 4.7626399"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.4 (e7c3feb, 2024-10-09)"
|
||||||
|
sodipodi:docname="icon_settings.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="-61"
|
||||||
|
inkscape:cy="153"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="2190"
|
||||||
|
inkscape:window-height="1404"
|
||||||
|
inkscape:window-x="1463"
|
||||||
|
inkscape:window-y="16"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d6d6d6"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-109.05253,-110.63344)">
|
||||||
|
<title
|
||||||
|
id="title1">account-cog</title>
|
||||||
|
<path
|
||||||
|
d="m 111.16928,110.63344 a 1.0583333,1.0583333 0 0 0 -1.05833,1.05833 1.0583333,1.0583333 0 0 0 1.05833,1.05833 1.0583333,1.0583333 0 0 0 1.05834,-1.05833 1.0583333,1.0583333 0 0 0 -1.05834,-1.05833 m 1.85209,2.11666 c -0.0344,0 -0.0635,0.0238 -0.0688,0.0556 l -0.0503,0.34925 c -0.0794,0.0344 -0.1561,0.0767 -0.22489,0.12435 l -0.32808,-0.13229 c -0.0291,0 -0.0635,0 -0.082,0.0344 l -0.26458,0.45773 c -0.0159,0.0291 -0.0106,0.0635 0.0159,0.0847 l 0.28045,0.21696 c -0.005,0.045 -0.008,0.0873 -0.008,0.13229 0,0.045 0.003,0.0873 0.008,0.13229 l -0.28045,0.21696 c -0.0238,0.0212 -0.0317,0.0556 -0.0159,0.0847 l 0.26458,0.45773 c 0.0159,0.0344 0.0503,0.0344 0.082,0.0344 l 0.32808,-0.13229 c 0.0688,0.0476 0.14287,0.0926 0.22489,0.12435 l 0.0503,0.34925 c 0.005,0.0318 0.0318,0.0556 0.0688,0.0556 h 0.52916 c 0.0291,0 0.0582,-0.0238 0.0635,-0.0556 l 0.0503,-0.34925 c 0.0794,-0.0344 0.15081,-0.0767 0.22225,-0.12435 l 0.32543,0.13229 c 0.0344,0 0.0688,0 0.0873,-0.0344 l 0.26458,-0.45773 c 0.0159,-0.0291 0.008,-0.0635 -0.0159,-0.0847 l -0.2831,-0.21696 c 0.005,-0.045 0.0106,-0.0873 0.0106,-0.13229 0,-0.045 -0.003,-0.0873 -0.0106,-0.13229 l 0.28046,-0.21696 c 0.0238,-0.0212 0.0317,-0.0556 0.0159,-0.0847 l -0.26458,-0.45773 c -0.0159,-0.0344 -0.0503,-0.0344 -0.0847,-0.0344 l -0.32544,0.13229 c -0.0714,-0.0476 -0.14287,-0.0926 -0.22489,-0.12435 l -0.0503,-0.34925 c -0.003,-0.0318 -0.0318,-0.0556 -0.0609,-0.0556 h -0.52916 m -1.85209,0.52917 c -1.16945,0 -2.11666,0.4736 -2.11666,1.05833 v 0.52917 h 2.56116 a 1.8520833,1.8520833 0 0 1 -0.17991,-0.79375 1.8520833,1.8520833 0 0 1 0.16933,-0.76994 c -0.14023,-0.0159 -0.28575,-0.0238 -0.43392,-0.0238 m 2.11667,0.39688 c 0.2196,0 0.39688,0.17727 0.39688,0.39687 0,0.2196 -0.17728,0.39688 -0.39688,0.39688 -0.22225,0 -0.39687,-0.17728 -0.39687,-0.39688 0,-0.2196 0.17727,-0.39687 0.39687,-0.39687 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.264583" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -1,28 +1,56 @@
|
||||||
|
import { MessageBus } from '@mbus'
|
||||||
|
|
||||||
export class Application {
|
export class Application {
|
||||||
constructor(records) {// {{{
|
constructor(records) {// {{{
|
||||||
|
window._mbus = new MessageBus()
|
||||||
|
this.settings = new Settings()
|
||||||
this.records = this.parseRecords(records)
|
this.records = this.parseRecords(records)
|
||||||
this.folders = this.createFolders()
|
this.topFolder = new Folder(this, 'root')
|
||||||
|
this.recordsTree = null
|
||||||
|
this.settingsIcon = null
|
||||||
|
|
||||||
|
this.renderFolders()
|
||||||
this.render()
|
this.render()
|
||||||
}// }}}
|
}// }}}
|
||||||
parseRecords(recordsData) {// {{{
|
parseRecords(recordsData) {// {{{
|
||||||
const records = recordsData.map(d => new Record(d))
|
const records = recordsData.map(d => new Record(d))
|
||||||
return records
|
return records
|
||||||
}// }}}
|
}// }}}
|
||||||
createFolders() {// {{{
|
|
||||||
this.records.sort(this.sortRecords)
|
|
||||||
const topFolder = new Folder(this, 'root')
|
|
||||||
|
|
||||||
|
// cleanFolders removes all records from all folders.
|
||||||
|
// renderFolders can then put moved records in the correct
|
||||||
|
// (or newly created) folders again when record names are updated.
|
||||||
|
cleanFolders(folder) {// {{{
|
||||||
|
if (folder === undefined)
|
||||||
|
folder = this.topFolder
|
||||||
|
|
||||||
|
folder.records = []
|
||||||
|
folder.subfolders.forEach((folder, _label) => {
|
||||||
|
this.cleanFolders(folder)
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
renderFolders() {// {{{
|
||||||
|
this.records.sort(this.sortRecords)
|
||||||
|
|
||||||
|
|
||||||
|
// rec: for example www.google.com
|
||||||
for (const rec of this.records) {
|
for (const rec of this.records) {
|
||||||
|
// com.google (reverse and remove wwww)
|
||||||
const labels = rec.labels().reverse().slice(0, -1)
|
const labels = rec.labels().reverse().slice(0, -1)
|
||||||
|
|
||||||
let currFolder = topFolder
|
// Start each record from the top and iterate through all its labels
|
||||||
|
// except the first one since that would be the actual record.
|
||||||
|
let currFolder = this.topFolder
|
||||||
let accFolderLabels = []
|
let accFolderLabels = []
|
||||||
for (const i in labels) {
|
for (const i in labels) {
|
||||||
const label = labels[i]
|
const label = labels[i]
|
||||||
|
|
||||||
|
// The accumulated name is used to create each folder as the record progresses.
|
||||||
accFolderLabels.push(label)
|
accFolderLabels.push(label)
|
||||||
const accFolderName = accFolderLabels.map(v => v).reverse().join('.')
|
const accFolderName = accFolderLabels.map(v => v).reverse().join('.')
|
||||||
|
|
||||||
|
// A new folder is created only when it doesn't exist
|
||||||
|
// to be able to update them when necessary.
|
||||||
let folder = currFolder.subfolders.get(label)
|
let folder = currFolder.subfolders.get(label)
|
||||||
if (folder === undefined) {
|
if (folder === undefined) {
|
||||||
folder = new Folder(this, accFolderName)
|
folder = new Folder(this, accFolderName)
|
||||||
|
|
@ -30,12 +58,10 @@ export class Application {
|
||||||
}
|
}
|
||||||
currFolder = folder
|
currFolder = folder
|
||||||
|
|
||||||
// Add the record to the innermost folder
|
|
||||||
}
|
}
|
||||||
|
// Add the record to the innermost folder
|
||||||
currFolder.addRecord(rec)
|
currFolder.addRecord(rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
return topFolder
|
|
||||||
}// }}}
|
}// }}}
|
||||||
sortFolders(a, b) {// {{{
|
sortFolders(a, b) {// {{{
|
||||||
const aLabels = a.labels().reverse()
|
const aLabels = a.labels().reverse()
|
||||||
|
|
@ -67,15 +93,31 @@ export class Application {
|
||||||
return 0
|
return 0
|
||||||
}// }}}
|
}// }}}
|
||||||
render() {// {{{
|
render() {// {{{
|
||||||
const tree = document.getElementById('records-tree')
|
if (this.recordsTree == null) {
|
||||||
tree.replaceChildren()
|
this.recordsTree = document.createElement('div')
|
||||||
|
this.recordsTree.id = 'records-tree'
|
||||||
|
|
||||||
|
this.settingsIcon = document.createElement('img')
|
||||||
|
this.settingsIcon.id = 'settings-icon'
|
||||||
|
this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
|
||||||
|
this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
|
||||||
|
|
||||||
|
document.body.appendChild(this.recordsTree)
|
||||||
|
document.body.appendChild(this.settingsIcon)
|
||||||
|
}
|
||||||
|
//this.recordsTree.replaceChildren()
|
||||||
|
|
||||||
// Top root folder doesn't have to be shown.
|
// Top root folder doesn't have to be shown.
|
||||||
const folders = Array.from(this.folders.subfolders.values())
|
const folders = Array.from(this.topFolder.subfolders.values())
|
||||||
folders.sort(this.sortFolders)
|
folders.sort(this.sortFolders)
|
||||||
|
|
||||||
for (const folder of folders)
|
for (const folder of folders)
|
||||||
tree.append(folder.toElement())
|
this.recordsTree.append(folder.toElement())
|
||||||
|
|
||||||
|
// Subscribe to settings update since the elements they will change
|
||||||
|
// exists now.
|
||||||
|
_mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail))
|
||||||
|
this.setBoxedFolders(this.settings.get('boxed_folders'))
|
||||||
|
|
||||||
}// }}}
|
}// }}}
|
||||||
handlerTop(event) {// {{{
|
handlerTop(event) {// {{{
|
||||||
|
|
@ -125,6 +167,17 @@ export class Application {
|
||||||
|
|
||||||
|
|
||||||
}// }}}
|
}// }}}
|
||||||
|
handlerSettingsUpdated({ key, value }) {// {{{
|
||||||
|
if (key == 'boxed_folders') {
|
||||||
|
this.setBoxedFolders(value)
|
||||||
|
}
|
||||||
|
}// }}}
|
||||||
|
setBoxedFolders(state) {// {{{
|
||||||
|
if (state)
|
||||||
|
document.body.classList.add('boxed-folders')
|
||||||
|
else
|
||||||
|
document.body.classList.remove('boxed-folders')
|
||||||
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Folder {
|
class Folder {
|
||||||
|
|
@ -250,6 +303,9 @@ class Record {
|
||||||
value() {// {{{
|
value() {// {{{
|
||||||
return this.data.ParsedValue
|
return this.data.ParsedValue
|
||||||
}// }}}
|
}// }}}
|
||||||
|
matchSubdomain() {// {{{
|
||||||
|
return this.data.MatchSubdomain === 'true'
|
||||||
|
}// }}}
|
||||||
labels() {// {{{
|
labels() {// {{{
|
||||||
return this.name().split('.')
|
return this.name().split('.')
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
@ -263,6 +319,17 @@ class Record {
|
||||||
new RecordDialog(this).show()
|
new RecordDialog(this).show()
|
||||||
}// }}}
|
}// }}}
|
||||||
set(key, value) {// {{{
|
set(key, value) {// {{{
|
||||||
|
if (key == 'Name') {
|
||||||
|
if (value.slice(0, 2) == '*.') {
|
||||||
|
this.data['Name'] = value.slice(2)
|
||||||
|
this.data['MatchSubdomain'] = 'true'
|
||||||
|
} else {
|
||||||
|
this.data['Name'] = value
|
||||||
|
this.data['MatchSubdomain'] = 'false'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.data[key] = value
|
this.data[key] = value
|
||||||
}// }}}
|
}// }}}
|
||||||
render() {// {{{
|
render() {// {{{
|
||||||
|
|
@ -280,8 +347,9 @@ class Record {
|
||||||
this.divSeparator.classList.add('separator')
|
this.divSeparator.classList.add('separator')
|
||||||
|
|
||||||
this.divFQDN.innerHTML = `
|
this.divFQDN.innerHTML = `
|
||||||
<span class="first-label">${this.labels()[0]}</span>
|
<span class="subdomains">*.</span>
|
||||||
<span class="rest-label">${this.labels().slice(1).join('.')}</span>
|
<span class="first-label"></span>
|
||||||
|
<span class="rest-label"></span>
|
||||||
`
|
`
|
||||||
|
|
||||||
this.divFQDN.addEventListener('click', event => {
|
this.divFQDN.addEventListener('click', event => {
|
||||||
|
|
@ -299,6 +367,17 @@ class Record {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FQDN is updated.
|
||||||
|
if (this.matchSubdomain())
|
||||||
|
this.divFQDN.classList.add('match-subdomains')
|
||||||
|
else
|
||||||
|
this.divFQDN.classList.remove('match-subdomains')
|
||||||
|
|
||||||
|
const fl = this.labels()[0]
|
||||||
|
const rl = this.labels().slice(1).join('.')
|
||||||
|
this.divFQDN.querySelector('.first-label').innerText = fl
|
||||||
|
this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : ''
|
||||||
|
|
||||||
this.divType.innerText = this.type()
|
this.divType.innerText = this.type()
|
||||||
this.divValue.innerText = this.value()
|
this.divValue.innerText = this.value()
|
||||||
|
|
||||||
|
|
@ -315,6 +394,9 @@ class Record {
|
||||||
alert(json.Error)
|
alert(json.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_app.cleanFolders()
|
||||||
|
_app.renderFolders()
|
||||||
|
_app.render()
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +418,10 @@ class RecordDialog {
|
||||||
<option>A</option>
|
<option>A</option>
|
||||||
<option>AAAA</option>
|
<option>AAAA</option>
|
||||||
<option>CNAME</option>
|
<option>CNAME</option>
|
||||||
|
<option>FWD</option>
|
||||||
|
<option>NS</option>
|
||||||
|
<option>NXDOMAIN</option>
|
||||||
|
<option>TXT</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div>Value</div>
|
<div>Value</div>
|
||||||
|
|
@ -350,7 +436,11 @@ class RecordDialog {
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
if (this.record.matchSubdomain())
|
||||||
|
this.dlg.querySelector('.name').value = '*.' + this.record.name()
|
||||||
|
else
|
||||||
this.dlg.querySelector('.name').value = this.record.name()
|
this.dlg.querySelector('.name').value = this.record.name()
|
||||||
|
|
||||||
this.dlg.querySelector('.type').value = this.record.type()
|
this.dlg.querySelector('.type').value = this.record.type()
|
||||||
this.dlg.querySelector('.value').value = this.record.value()
|
this.dlg.querySelector('.value').value = this.record.value()
|
||||||
this.dlg.querySelector('.ttl').value = this.record.ttl();
|
this.dlg.querySelector('.ttl').value = this.record.ttl();
|
||||||
|
|
@ -380,3 +470,63 @@ class RecordDialog {
|
||||||
this.dlg.close()
|
this.dlg.close()
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
constructor() {// {{{
|
||||||
|
this.settings = new Map([
|
||||||
|
['boxed_folders', true],
|
||||||
|
])
|
||||||
|
|
||||||
|
// Read any configured settings from local storage, but keeping default value
|
||||||
|
// if not set.
|
||||||
|
this.settings.forEach((_v, key) => {
|
||||||
|
const configuredValue = localStorage.getItem(key)
|
||||||
|
if (configuredValue !== null)
|
||||||
|
this.settings.set(key, JSON.parse(configuredValue))
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
set(key, value) {// {{{
|
||||||
|
this.settings.set(key, value)
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
_mbus.dispatch('settings_updated', { key, value })
|
||||||
|
}// }}}
|
||||||
|
get(key) {// {{{
|
||||||
|
return this.settings.get(key)
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsDialog {
|
||||||
|
constructor(app) {// {{{
|
||||||
|
this.application = app
|
||||||
|
|
||||||
|
this.dlg = null
|
||||||
|
this.elBoxedFolders = null
|
||||||
|
}// }}}
|
||||||
|
show() {// {{{
|
||||||
|
this.dlg = document.createElement('dialog')
|
||||||
|
this.dlg.id = 'settings-dialog'
|
||||||
|
|
||||||
|
this.dlg.innerHTML = `
|
||||||
|
<input type="checkbox" id="boxed-folders"> <label for="boxed-folders">Boxed folders</label>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="save">Save</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const boxedFolders = this.application.settings.get('boxed_folders')
|
||||||
|
this.elBoxedFolders = this.dlg.querySelector('#boxed-folders')
|
||||||
|
this.elBoxedFolders.checked = boxedFolders
|
||||||
|
|
||||||
|
// Event listeners are connected.
|
||||||
|
this.dlg.querySelector('.save').addEventListener('click', () => this.save())
|
||||||
|
this.dlg.addEventListener('close', () => this.dlg.remove())
|
||||||
|
|
||||||
|
// Can't show a dialog that doesn't exist in DOM.
|
||||||
|
document.body.appendChild(this.dlg)
|
||||||
|
this.dlg.showModal()
|
||||||
|
}// }}}
|
||||||
|
save() {// {{{
|
||||||
|
this.application.settings.set('boxed_folders', this.elBoxedFolders.checked)
|
||||||
|
this.dlg.close()
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
|
||||||
29
static/js/mbus.mjs
Normal file
29
static/js/mbus.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export class MessageBus {
|
||||||
|
constructor() {
|
||||||
|
this.bus = new EventTarget()
|
||||||
|
this.counter = 0
|
||||||
|
this.fnmap = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(eventName, fn) {
|
||||||
|
this.counter++
|
||||||
|
this.bus.addEventListener(eventName, fn)
|
||||||
|
this.fnmap.set(this.counter, { eventName, fn })
|
||||||
|
return this.counter
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(mappedID) {
|
||||||
|
const mapped = this.fnmap.get(mappedID)
|
||||||
|
if (mapped === undefined) {
|
||||||
|
console.warn('unsubscribe, no such mapped ID', mappedID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.fnmap.delete(mappedID)
|
||||||
|
this.bus.removeEventListener(mapped.eventName, mapped.fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(eventName, data) {
|
||||||
|
console.debug('mbus', eventName, data)
|
||||||
|
this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@mbus": "/js/{{ .Data.VERSION }}/mbus.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,4 @@ window._app = new Application({{ .Data.DNSRecords }})
|
||||||
|
|
||||||
<h1>{{ .Data.Identity }}</h1>
|
<h1>{{ .Data.Identity }}</h1>
|
||||||
|
|
||||||
<div id="records-tree">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []*DNSRecord
|
var entries []DNSRecord
|
||||||
entries, err = device.StaticDNSEntries()
|
entries, err = device.StaticDNSEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue