wip: add new record

main
Hannaeko 2023-02-25 02:53:10 +01:00
parent e15ad04c64
commit 960942c47f
4 changed files with 294 additions and 162 deletions

View File

@ -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,
};

View File

@ -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`<span>${keys[0][1]}</span>`;
} 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`
<form class="new-record">
<div>
<form class="new-record ${enabled ? '' : 'disabled'}" id=${id}>
<h3>Nouvel enregistrement</h3>
<div class="form-row">
<div class="input-group">
<label for="domain">Domaine</label>
<div>
<input type="text" id="domain"/>
<div class="combined-input">
<input type="text" id="domain" name="domain" onChange=${e => setDomain(e.target.value)}/>
<span>.${ zone }</span>
</div>
</div>
<div>
<div class="input-group">
<label for="record_type">Type d'enregistrement</label>
<select id="record_type">
<option>Adresse IP</option>
<option>Alias</option>
<select id="record_type" name="record_type" onChange=${e => { setRecordType(e.target.value); setRealType(''); setRecordData({}); setRealRecordData({})}}>
${Object.entries(recordTypeNames).map(([type, name]) => html`<option value="${type}">${name}</option>`)}
</select>
</div>
<div>
<input type="text"/>
${friendlyRecordDataConfig[recordType].fields.map(fieldName => html`
<div class="input-group">
<label for="${fieldName}">${rdataInputProperties[fieldName].label}</label>
<input id="${fieldName}" type="${rdataInputProperties[fieldName].type}" onChange=${setRecordDataFactory(fieldName)}></input>
</div>
`)}
</div>
<div class="input-group">
<label for="ttl">TTL</label>
<input type="number" name="ttl" id="ttl" value="${ttl}" onChange=${e => setTTL(e.target.value)}/>
</div>
</div>
<article class="preview">
<h4>Prévisualisation de l'enregistrement</h3>
<pre>
${domain == '' ? '@' : domain + '.' + zone} ${ttl} IN ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''}
</pre>
</article>
<div>
<input type="submit" value="Enregistrer"/>
<button role="button">Anuler</button>
<input type="submit" onClick=${createNewRecord} value="Ajouter"/>
<button role="button" onClick=${onCancel}>Anuler</button>
</div>
</form>
`;
}
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`
<header>
<h2>Enregistrements</h2>
<button onClick=${() => setNewRecords([...newRecords, NewRecordFormFriendly({ zone })])}>Ajouter un enregistrement</button>
<button onClick=${() => setAddNewRecord(true)} aria-controls="add-new-record-form" aria-expanded=${addNewRecord} disabled=${addNewRecord}>Ajouter un enregistrement</button>
<button>Éditer la zone</button>
</header>
${newRecords}
<${NewRecordFormFriendly} zone=${zone} enabled=${addNewRecord} id="add-new-record-form" onCancel=${onCancelNewRecord}/>
<div class="zone-content">
<${RecordListFriendly} zone=${zone} />
</div>
`;
}

View File

@ -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;
}

View File

@ -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;
}