From 960942c47fe779444f56c529375be52ed917c1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Sat, 25 Feb 2023 02:53:10 +0100 Subject: [PATCH] wip: add new record --- public/scripts/api.js | 22 ++++ public/scripts/records.js | 218 +++++++++++++++++++++++++++----------- public/styles/main.css | 117 ++++++++------------ public/styles/zone.css | 99 ++++++++++++----- 4 files changed, 294 insertions(+), 162 deletions(-) diff --git a/public/scripts/api.js b/public/scripts/api.js index 6074991..a1b560a 100644 --- a/public/scripts/api.js +++ b/public/scripts/api.js @@ -12,11 +12,33 @@ function apiGet(url) { }); } +function apiPost(url, data) { + return fetch(`${baseUrl}/${url}`, { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + .then(res => { + if (!res.ok) { + // do something here + throw new Error('Not ok'); + } + return res.json(); + }); +} + function getRecords(zone) { return apiGet(`zones/${zone}/records`); } +function createRecords(zone, record) { + return apiPost(`zones/${zone}/records`, record); +} + export { getRecords, + createRecords, }; diff --git a/public/scripts/records.js b/public/scripts/records.js index 3e65d50..aab7604 100644 --- a/public/scripts/records.js +++ b/public/scripts/records.js @@ -1,29 +1,84 @@ import { html, render, useState, useEffect } from './vendor/preact/standalone.js'; -import { getRecords } from './api.js'; +import { getRecords, createRecords } from './api.js'; const rdataInputProperties = { - Address: {label: 'adresse :', type: 'text'}, - Serial: {label: 'serial :', type: 'number'}, - Minimum: {label: 'minimum :', type: 'number'}, - Retry: {label: 'nouvelle tentative :', type: 'number'}, - Refresh: {label: 'actualisation :', type: 'number'}, - MaintainerName: {label: 'contact :', type: 'text'}, - MasterServerName: {label: 'serveur primaire :', type: 'text'}, - Expire: {label: 'expiration :', type: 'number'}, - Target: {label: 'cible :', type: 'text'}, + Address: {label: 'Adresse', type: 'text'}, + Serial: {label: 'Numéro de série', type: 'number'}, + Minimum: {label: 'Minimum', type: 'number'}, + Retry: {label: 'Nouvelle tentative', type: 'number'}, + Refresh: {label: 'Actualisation', type: 'number'}, + MaintainerName: {label: 'Contact', type: 'text'}, + MasterServerName: {label: 'Serveur primaire', type: 'text'}, + Expire: {label: 'Expiration', type: 'number'}, + Target: {label: 'Cible', type: 'text'}, }; -const recordTypes = { - 'A': 'address', - 'AAAA': 'address', - 'SRV': 'service', - 'CNAME': 'alias', - 'NS': 'name_server', - 'SOA': 'soa', +const realRecordDataConfig = { + 'A': { + friendlyType: 'address', + fields: ['Address'], + }, + 'AAAA': { + friendlyType: 'address', + fields: ['Address'], + }, + 'CNAME': { + friendlyType: 'alias', + fields: ['Target'], + }, + 'SRV': { + friendlyType: 'service', + fields: [ /* TODO */ ], + }, + 'NS': { + friendlyType: 'name_server', + fields: ['Target'], + }, + 'SOA': { + friendlyType: 'soa', + fields: ['MasterServerName', 'MaintainerName', 'Refresh', 'Retry', 'Expire', 'Minimum', 'Serial'], + }, }; +function defaultBuildData(realRecordType) { + const defaultFields = realRecordDataConfig[realRecordType].fields.map(field => [field, null]); + return (fields) => { + return {...defaultFields, ...fields, Type: realRecordType}; + } +} + +function defaultRecordToField(realRecord) { + const type = realRecord.Type; + return realRecordDataConfig[type].fields.map(field => [field, realRecord[field]]); +} + + +const friendlyRecordDataConfig = { + 'address': { + realRecordToFields: defaultRecordToField, + fields: realRecordDataConfig['AAAA'].fields, + buildData: buildAddressRecord, + }, + 'alias': { + realRecordToFields: defaultRecordToField, + fields: realRecordDataConfig['CNAME'].fields, + buildData: defaultBuildData('CNAME'), + }, + 'name_server': { + realRecordToFields: defaultRecordToField, + fields: realRecordDataConfig['CNAME'].fields, + buildData: defaultBuildData('NS'), + }, + 'soa': { + realRecordToFields: defaultRecordToField, + fields: realRecordDataConfig['SOA'].fields, + buildData: defaultBuildData('SOA'), + }, +}; + + const recordTypeNames = { 'address': 'Adresse IP', 'service': 'Service', @@ -32,18 +87,20 @@ const recordTypeNames = { 'soa': 'SOA', } +/* Type to use for SRV to derive name without port / service */ function getNameForRecord(name, type) { return name; } -function getTypeForRecord(name, type) { - return recordTypes[type]; +/* Name to use with _spf for example */ +function getFriendlyTypeForRecord(name, type) { + return realRecordDataConfig[type].friendlyType; } function processRecords(records) { return records.reduce((acc, record) => { let name = getNameForRecord(record.Name, record.Type); - let type = getTypeForRecord(record.Name, record.Type); + let type = getFriendlyTypeForRecord(record.Name, record.Type); if (!(name in acc)) { acc[name] = {}; } @@ -54,26 +111,16 @@ function processRecords(records) { return acc; }, {}); } -const recordsKeys = { - 'address': (record) => { return [[ 'Address', record.Address ]] }, - 'service': (record) => { /* TODO */ }, - 'alias': (record) => { return [[ 'Target', record.Target ]] }, - 'name_server': (record) => { return [[ 'Target', record.Target ]] }, - 'soa': (record) => { - return [ - [ 'MasterServerName', record.MasterServerName ], - [ 'MaintainerName', record.MaintainerName ], - [ 'Refresh', record.Refresh ], - [ 'Retry', record.Retry ], - [ 'Expire', record.Expire ], - [ 'Minimum', record.Minimum ], - [ 'Serial', record.Serial ], - ] - }, + +function buildAddressRecord({ Address = ''}) { + return { + Type: Address.indexOf(':') > -1 ? 'AAAA' : 'A', + Address + } } function FriendlyRecord({type, record}) { - let keys = recordsKeys[type](record); + let keys = friendlyRecordDataConfig[type].realRecordToFields(record); if (keys.length == 1) { return html`${keys[0][1]}`; } else { @@ -122,7 +169,6 @@ function RecordListFriendly({ zone }) { ${Object.entries(records).map( ([name, recordSets]) => { return html` - <${RecordsByName} name=${name} recordSets=${recordSets}/> `; } @@ -130,46 +176,96 @@ function RecordListFriendly({ zone }) { `; } -function NewRecordFormFriendly({ zone }) { +function NewRecordFormFriendly({ zone, enabled, id, onCancel }) { + const [recordType, setRecordType] = useState(Object.keys(recordTypeNames)[0]); + const [recordData, setRecordData] = useState({}); + const [realRecordData, setRealRecordData] = useState({}); + const [realType, setRealType] = useState(''); + const [domain, setDomain] = useState(''); + const [ttl, setTTL] = useState(3600); + + const setRecordDataFactory = (field) => { + return (e) => { + const newData = {...recordData}; + newData[field] = e.target.value; + const newRealRecordData = friendlyRecordDataConfig[recordType].buildData(newData) + setRecordData(newData); + setRealRecordData(newRealRecordData); + setRealType(newRealRecordData.Type); + } + } + + const createNewRecord = (e) => { + e.preventDefault(); + const newRecords = [{...realRecordData, Class: 'IN', TTL: ttl, Name: `${domain}.${zone}`}]; + console.log(newRecords) + createRecords(zone, newRecords); + } + + // TODO: Reset valeurs champs quand changement de type => bound la valeur de l'input au state + // TODO: Dans le cas où un domain est dans le RDATA mettre le domaine absolue dans la preview + // TODO: Déplacer preview dans son component, faire une vue en "diff" et l'appeler "prévisualisation des changements" + // TODO: Validation des données client et serveur + return html` -
-
- + +

Nouvel enregistrement

+
+
+ +
+ setDomain(e.target.value)}/> + .${ zone } +
+
+
+ + +
- - .${ zone } + ${friendlyRecordDataConfig[recordType].fields.map(fieldName => html` +
+ + +
+ `)} +
+
+ + setTTL(e.target.value)}/>
+
+

Prévisualisation de l'enregistrement

+
+                    ${domain == '' ? '@' : domain + '.' + zone} ${ttl} IN ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''}
+                
+
- - -
-
- -
-
- - + +
`; } function ZoneRecords({ zone }) { - const [addNewRecord, setAddNewRecord] = useState(false); - const [newRecords, setNewRecords] = useState([]); + const [addNewRecord, setAddNewRecord] = useState(true); + + const onCancelNewRecord = (e) => { setAddNewRecord(false); e.preventDefault() }; return html`

Enregistrements

- +
- ${newRecords} - <${RecordListFriendly} zone=${zone} /> + <${NewRecordFormFriendly} zone=${zone} enabled=${addNewRecord} id="add-new-record-form" onCancel=${onCancelNewRecord}/> +
+ <${RecordListFriendly} zone=${zone} /> +
`; } diff --git a/public/styles/main.css b/public/styles/main.css index 21d300c..775adb2 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -4,12 +4,13 @@ body { min-width: 100vw; margin: 0; font-family: sans-serif; + line-height: 1.5; } :root { - --color-primary: #5e0c97; - --color-hightlight-1: #ffd4ba; - --color-hightlight-2: #dd39dd; + --color-primary: 94, 12, 151; + --color-hightlight-1: 255, 212, 186; + --color-hightlight-2: 208, 44, 167; --color-contrast: white; } @@ -26,7 +27,7 @@ h1 { } a { - color: var(--color-primary); + color: rgb(var(--color-primary)); text-decoration: none; position: relative; padding-bottom: .3em; @@ -36,18 +37,13 @@ a::after { content: ""; display: block; width: 100%; - border-bottom: 2px solid var(--color-hightlight-2); + border-bottom: 2px solid rgb(var(--color-primary)); position: absolute; bottom: .1em; transition: .2s; } a:hover::after { - content: ""; - display: block; - width: 100%; - border-bottom: 2px solid var(--color-hightlight-2); - position: absolute; bottom: .3em; } @@ -61,6 +57,14 @@ p.feedback.error { color: #710000; } +select, +input { + border: 1px solid rgb(var(--color-primary));; + border-radius: 0; + background: var(--color-contrast); +} + +select, button, input { padding: .35rem .35rem; @@ -69,25 +73,35 @@ input { button, input[type="submit"] { - background: var(--color-primary); + background: rgb(var(--color-primary)); color: var(--color-contrast); - border-left: 5px solid var(--color-hightlight-1); - border-top: 5px solid var(--color-hightlight-1); - border-right: 5px solid var(--color-hightlight-2); - border-bottom: 5px solid var(--color-hightlight-2); + border-left: 5px solid rgb(var(--color-hightlight-1)); + border-top: 5px solid rgb(var(--color-hightlight-1)); + border-right: 5px solid rgb(var(--color-hightlight-2)); + border-bottom: 5px solid rgb(var(--color-hightlight-2)); } -button:hover, -input[type="submit"]:hover { - background: #7a43a1; +button:hover:not([disabled]), +input[type="submit"]:hover:not([disabled]) { + background: rgba(var(--color-primary), .8); } -button:active , -input[type="submit"]:active { +button:active:not([disabled]) , +input[type="submit"]:active:not([disabled]) { border-left: 5px solid var(--color-hightlight-2); border-top: 5px solid var(--color-hightlight-2); - border-right: 5px solid var(--color-hightlight-1); - border-bottom: 5px solid var(--color-hightlight-1); + border-right: 5px solid rgb(var(--color-hightlight-1)); + border-bottom: 5px solid rgb(var(--color-hightlight-1)); +} + +button[disabled], +input[type="submit"][disabled] { + background: rgba(var(--color-primary), .75); + border-left-color: rgba(var(--color-hightlight-1), .75); + border-top-color: rgba(var(--color-hightlight-1), .75); + border-right-color: rgba(var(--color-hightlight-2), .75); + border-bottom-color: rgba(var(--color-hightlight-2), .75); + cursor: not-allowed; } form input[type="submit"] { @@ -105,18 +119,23 @@ form { } nav.main { - background: var(--color-primary); + background: rgb(var(--color-primary)); min-width: 25ch; display: flex; flex: 0; padding: 1rem; - border-right: 5px solid var(--color-hightlight-2); + border-right: 5px solid rgb(var(--color-hightlight-2)); } nav.main a { color: var(--color-contrast); } +nav.main a::after { + border-bottom: 2px solid var(--color-contrast); +} + + nav.main a img { filter: invert(100%); width: 1.4em; @@ -137,53 +156,3 @@ nav.main ul li { nav.main ul ul { margin-left: 1rem; } - -zone-content table { - border-collapse: collapse; - width: 100%; -} - -zone-content .rdata { - display: flex; - flex-wrap: wrap; -} - -zone-content th, .zone-content td { - font-weight: normal; - text-align: left; - vertical-align: top; - padding: 0.25rem; -} - -zone-content thead { - background: #ccb9ff; - color: #39004d; -} - -zone-content tbody tr:nth-child(even) td { - background: #ccb9ff3d; -} - -zone-content tbody tr .rdata dt, -zone-content tbody tr .rdata label { - display: inline-block; - padding: 0.1em 0.5em; - background: #cecece; - font-size: 0.7rem; - border-radius: 0.5em; - margin-right: 0.1rem; -} -zone-content tbody tr .rdata dd { - margin: 0; -} -zone-content tbody tr .rdata dl { - display: flex; - flex-wrap: wrap; - margin: 0; -} - -zone-content tbody tr .rdata div { - margin: 0.1rem 0.5rem 0.1rem 0; - display: flex; - align-items: baseline; -} diff --git a/public/styles/zone.css b/public/styles/zone.css index 0157ede..f34ab82 100644 --- a/public/styles/zone.css +++ b/public/styles/zone.css @@ -26,52 +26,52 @@ header > :not(:last-of-type) { margin-right: 2ch; } -zone-content h3, zone-content h4 { +.zone-content h3, .zone-content h4 { margin: 0; font-weight: normal; font-size: 1rem; width: 30%; } -zone-content article { +.zone-content article { display: flex; } -zone-content > article > div { +.zone-content > article > div { flex-grow: 1; } -zone-content > article { +.zone-content > article { margin: .5rem 0; position: relative; } -zone-content > article:not(:last-of-type) { - border-bottom: 2px solid var(--color-hightlight-2); +.zone-content > article:not(:last-of-type) { + border-bottom: 2px solid rgb(var(--color-hightlight-2)); } -zone-content article > *{ +.zone-content article > *{ margin-right: 2ch; } -zone-content article ul { +.zone-content article ul { padding: 0; margin: 0; list-style-type: none; } -zone-content article dl { +.zone-content article dl { display: grid; grid-template: auto / max-content 1fr; } -zone-content article dd { +.zone-content article dd { margin: 0; } -zone-content article dt span { +.zone-content article dt span { display: inline-block; - background-color: var(--color-hightlight-1); + background-color: rgb(var(--color-hightlight-1)); padding: 0.1em 0.5em; border-radius: 0.5em; margin-right: 0.1rem; @@ -79,34 +79,79 @@ zone-content article dt span { } form.new-record { - display: flex; - flex-direction: row; + width: auto; } -form.new-record > div { - display: flex; - flex-direction: column; +form.new-record > div.form-row > div:not(:last-child) { + flex-grow: 1; margin-right: 2ch; } -form.new-record > div:first-child { - width: 30%; +form.new-record > div.form-row { + display: flex; + flex-wrap: wrap; } -form.new-record > div:first-child span { +form.new-record > div.form-row > div:first-child { + min-width: 30%; +} + +form.new-record > div.form-row > div:nth-child(2) { + min-width: calc( .3 * (70% - 4ch)); +} + +form div.input-group { + display: flex; + flex-direction: column; +} + +form div.combined-input { + display: flex; +} + +form div.combined-input input { + height: min-content; flex-grow: 1; +} + +form div.combined-input span { font-size: .8rem; + padding: .35rem; + border: 1px solid rgb(var(--color-primary));; + border-left: none; + background: rgba(var(--color-hightlight-2),.2); } -form.new-record > div:nth-child(2) { - width: calc( .3 * (70% - 4ch)); +form.disabled { + display: none; } - -form.new-record > div:nth-child(2) select { - flex-grow: 1; +input[name^="ttl"] { + max-width: 10ch; } -form.new-record > div:nth-child(3) { - flex: 1; +form.new-record button, +form.new-record input[type="submit"] { + margin-right: 1ch; + margin-top: .75rem; +} +form.new-record h3 { + margin: 0; +} +form.new-record .preview { + margin: .5rem 0; + border: 1px solid rgb(var(--color-primary)); +} + +form.new-record .preview pre { + margin: .5rem 0 .5rem 0; + padding: 0 .5rem; +} + +form.new-record .preview h4 { + margin: 0; + padding: .0rem .5rem 0 .5rem;; + border-bottom: 1px solid rgb(var(--color-primary)); + font-size: 1rem; + font-weight: bold; }