nomilo/public/scripts/records.js

329 lines
11 KiB
JavaScript

import { html, render, useState, useEffect } from './vendor/preact/standalone.js';
import { getRecords, createRecords } from './api.js';
const rdataInputProperties = {
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'},
Service: {label: 'Service', type: 'text'},
Protocol: {label: 'Protocole', type: 'text'},
Priority: {label: 'Priorité', type: 'number'},
Weight: {label: 'Poids', type: 'number'},
Port: {label: 'Port', type: 'number'},
Server: {label: 'Serveur', type: 'text'},
};
const realRecordDataConfig = {
'A': {
friendlyType: 'address',
fields: ['Address'],
},
'AAAA': {
friendlyType: 'address',
fields: ['Address'],
},
'CNAME': {
friendlyType: 'alias',
fields: ['Target'],
},
'SRV': {
friendlyType: 'service',
fields: [ 'Priority', 'Weight', 'Port', 'Server' ],
},
'NS': {
friendlyType: 'name_server',
fields: ['Target'],
},
'SOA': {
friendlyType: 'soa',
fields: ['MasterServerName', 'MaintainerName', 'Refresh', 'Retry', 'Expire', 'Minimum', 'Serial'],
},
};
function defaultBuildData(realRecordType) {
const defaultFields = Object.fromEntries(realRecordDataConfig[realRecordType].fields.map(field => [field, null]));
return (fields) => {
return {...defaultFields, ...fields, Type: realRecordType};
}
}
function defaultRecordToFields(realRecord) {
const type = realRecord.Type;
return realRecordDataConfig[type].fields.map(field => [field, realRecord[field]]);
}
function defaultGetName(name) {
return name;
}
function srvRecordToFields({ Name, Type, Class, ...fields }) {
const [ serviceName, protocol] = Name.split('.');
return {
Service: serviceName.replace(/^_/, ''),
Protocol: protocol.replace(/^_/, ''),
...fields
}
}
function srvGetName(originalName) {
const [_serviceName, _protocol, ...name] = originalName.split('.');
return name.join('.');
}
function buildAddressRecord(fields) {
const address = fields.Address || '';
if (address.indexOf('.') >= 0) {
fields.Type = 'A';
} else if (address.indexOf(':') >= 0) {
fields.Type = 'AAAA';
} else {
fields.Type = '';
}
return fields;
}
function buildServiceRecord({ Name, Service, Protocol, ...fields}) {
fields.Name = `_${Service}._${Protocol}.${Name}`;
fields.Type = 'SRV';
return fields;
}
const friendlyRecordDataConfig = {
'address': {
realRecordToFields: defaultRecordToFields,
fields: realRecordDataConfig['AAAA'].fields,
buildData: buildAddressRecord,
getName: defaultGetName,
},
'alias': {
realRecordToFields: defaultRecordToFields,
fields: realRecordDataConfig['CNAME'].fields,
buildData: defaultBuildData('CNAME'),
getName: defaultGetName,
},
'name_server': {
realRecordToFields: defaultRecordToFields,
fields: realRecordDataConfig['NS'].fields,
buildData: defaultBuildData('NS'),
getName: defaultGetName,
},
'soa': {
realRecordToFields: defaultRecordToFields,
fields: realRecordDataConfig['SOA'].fields,
buildData: defaultBuildData('SOA'),
getName: defaultGetName,
},
'service': {
realRecordToFields: srvRecordToFields,
fields: ['Service', 'Protocol', 'Priority', 'Weight', 'Port', 'Server'],
buildData: buildServiceRecord,
getName: srvGetName,
},
};
const recordTypeNames = {
'address': 'Adresse IP',
'service': 'Service',
'alias': 'Alias',
'name_server': 'Serveur de nom',
'soa': 'SOA',
}
/* Name to use with spf for example */
function getFriendlyTypeForRecord(name, type) {
return realRecordDataConfig[type].friendlyType;
}
function processRecords(records) {
return records.reduce((acc, record) => {
let type = getFriendlyTypeForRecord(record.Name, record.Type);
let name = friendlyRecordDataConfig[type].getName(record.Name);
if (!(name in acc)) {
acc[name] = {};
}
if (!(type in acc[name])) {
acc[name][type] = [];
}
acc[name][type].push(record);
return acc;
}, {});
}
function FriendlyRecord({type, record}) {
let keys = friendlyRecordDataConfig[type].realRecordToFields(record);
if (keys.length == 1) {
return html`<span>${keys[0][1]}</span>`;
} else {
return html`
<dl>
${keys.map(([name, value]) => {return html`<dt><span>${rdataInputProperties[name].label}</span></dt><dd>${value}</dd>`})}
</dl>
`;
}
}
function RecordsByName({ name, recordSets }) {
return html`
<article class="rrsets-per-name">
<h3 class="record-name">${name}</h4>
<div class="rrsets-per-type">
${Object.entries(recordSets).map(
([type, records]) => {
return html`
<article class="rrset-per-type">
<h4 class="record-type">${recordTypeNames[type]}</h4>
<ul class="rrset-rdata">
${records.map(record => html`<li><${FriendlyRecord} type=${type} record=${record}/></li>`)}
</ul>
</article>
`;
}
)}
</div>
</article>
`;
}
function RecordListFriendly({ zone }) {
const [records, setRecords] = useState({});
const [editable, setEditable] = useState(false);
useEffect(() => {
getRecords(zone)
.then((res) => setRecords(processRecords(res)));
}, [zone]);
return html`
${Object.entries(records).map(
([name, recordSets]) => {
return html`
<${RecordsByName} name=${name} recordSets=${recordSets}/>
`;
}
)}
`;
}
function NewRecordFormFriendly({ zone }) {
const defaultVaules = {Name: '', TTL: 3600, Class: 'IN'};
const [recordType, setRecordType] = useState(Object.keys(recordTypeNames)[0]);
const [recordData, setRecordData] = useState(defaultVaules);
const [realRecordData, setRealRecordData] = useState({});
const [realType, setRealType] = useState('');
const absoluteName = (name) => name ? `${name}.${zone}` : zone;
const setRecordDataFactory = (field) => {
return (e) => {
const newData = {...recordData};
newData[field] = e.target.type == 'number' ? Number(e.target.value) : e.target.value;
const newRealRecordData = friendlyRecordDataConfig[recordType].buildData({...newData, Class: 'IN', Name: absoluteName(newData.Name)})
setRecordData(newData);
setRealRecordData(newRealRecordData);
setRealType(newRealRecordData.Type);
}
}
const createNewRecord = (e) => {
e.preventDefault();
const newRecords = [realRecordData];
console.log(newRecords)
createRecords(zone, newRecords);
}
const resetData = (resetName = false) => {
setRealType('');
const newName = resetName ? defaultVaules.Name : recordData.Name;
setRecordData({ Name: newName, TTL: defaultVaules.TTL });
setRealRecordData({...defaultVaules, Name: absoluteName(newName)});
}
useEffect(() => resetData(true), []);
// TODO: Reset valeurs champs quand changement de type + "annuler" => 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`
<section class="new-record">
<header>
<h2>Nouvel enregistrement</h2>
</header>
<form>
<div class="form-row">
<div class="input-group">
<label for="domain">Domaine</label>
<div class="combined-input">
<input type="text" id="domain" name="domain" value=${recordData.Name} onInput=${setRecordDataFactory('Name')}/>
<span>.${ zone }</span>
</div>
</div>
<div class="input-group">
<label for="record_type">Type d'enregistrement</label>
<select id="record_type" name="record_type" onChange=${(e) => { setRecordType(e.target.value); resetData() }}>
${Object.entries(recordTypeNames).map(([type, name]) => html`<option value="${type}">${name}</option>`)}
</select>
</div>
<div>
${friendlyRecordDataConfig[recordType].fields.map(fieldName => html`
<div class="input-group">
<label for="${fieldName}">${rdataInputProperties[fieldName].label}</label>
<input id="${fieldName}" type="${rdataInputProperties[fieldName].type}" onInput=${setRecordDataFactory(fieldName)}></input>
</div>
`)}
</div>
<div class="input-group">
<label for="ttl">Durée dans le cache</label>
<input type="number" name="ttl" id="ttl" value=${recordData.TTL} onInput=${setRecordDataFactory('TTL')}/>
</div>
</div>
<article class="preview">
<h3>Prévisualisation des changements</h3>
<p>
<img src="/images/plus.svg" alt="Ajout" title="Ajout" class="addition"/>
<code class="addition">
${realRecordData.Name === zone ? '@' : realRecordData.Name} ${realRecordData.TTL} ${realRecordData.Class} ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''}
</code>
</p>
</article>
<div>
<input type="submit" onClick=${createNewRecord} value="Ajouter"/>
<button type="reset" onClick=${e => { resetData(true); e.preventDefault() }}>Annuler</button>
</div>
</form>
</section>
`;
}
function ZoneRecords({ zone }) {
return html`
<${NewRecordFormFriendly} zone=${zone}/>
<section>
<header>
<h2>Contenu de la zone</h2>
<button>Éditer la zone</button>
</header>
<div class="zone-content">
<${RecordListFriendly} zone=${zone} />
</div>
</section>
`;
}
export default function(element, { zone }) {
render(html`<${ZoneRecords} zone=${zone} />`, element);
};