2023-02-22 23:02:49 +00:00
|
|
|
import { html, render, useState, useEffect } from './vendor/preact/standalone.js';
|
2022-04-29 17:13:03 +00:00
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
import { getRecords, createRecords } from './api.js';
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2022-04-29 17:13:03 +00:00
|
|
|
|
2022-04-29 02:29:10 +00:00
|
|
|
const rdataInputProperties = {
|
2023-02-25 01:53:10 +00:00
|
|
|
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'},
|
2022-04-29 16:04:12 +00:00
|
|
|
};
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
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'),
|
|
|
|
},
|
2023-02-22 23:02:49 +00:00
|
|
|
};
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
const recordTypeNames = {
|
|
|
|
'address': 'Adresse IP',
|
|
|
|
'service': 'Service',
|
|
|
|
'alias': 'Alias',
|
|
|
|
'name_server': 'Serveur de nom',
|
|
|
|
'soa': 'SOA',
|
|
|
|
}
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
/* Type to use for SRV to derive name without port / service */
|
2023-02-22 23:02:49 +00:00
|
|
|
function getNameForRecord(name, type) {
|
|
|
|
return name;
|
|
|
|
}
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
/* Name to use with _spf for example */
|
|
|
|
function getFriendlyTypeForRecord(name, type) {
|
|
|
|
return realRecordDataConfig[type].friendlyType;
|
2022-04-29 02:29:10 +00:00
|
|
|
}
|
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
function processRecords(records) {
|
|
|
|
return records.reduce((acc, record) => {
|
|
|
|
let name = getNameForRecord(record.Name, record.Type);
|
2023-02-25 01:53:10 +00:00
|
|
|
let type = getFriendlyTypeForRecord(record.Name, record.Type);
|
2023-02-22 23:02:49 +00:00
|
|
|
if (!(name in acc)) {
|
|
|
|
acc[name] = {};
|
|
|
|
}
|
|
|
|
if (!(type in acc[name])) {
|
|
|
|
acc[name][type] = [];
|
|
|
|
}
|
|
|
|
acc[name][type].push(record);
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
}
|
2023-02-25 01:53:10 +00:00
|
|
|
|
|
|
|
function buildAddressRecord({ Address = ''}) {
|
|
|
|
return {
|
|
|
|
Type: Address.indexOf(':') > -1 ? 'AAAA' : 'A',
|
|
|
|
Address
|
|
|
|
}
|
2022-04-29 02:29:10 +00:00
|
|
|
}
|
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
function FriendlyRecord({type, record}) {
|
2023-02-25 01:53:10 +00:00
|
|
|
let keys = friendlyRecordDataConfig[type].realRecordToFields(record);
|
2023-02-22 23:02:49 +00:00
|
|
|
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>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
function RecordsByName({ name, recordSets }) {
|
2022-04-29 02:33:00 +00:00
|
|
|
return html`
|
2023-02-22 23:02:49 +00:00
|
|
|
<article>
|
|
|
|
<h3>${name}</h4>
|
|
|
|
<div>
|
|
|
|
${Object.entries(recordSets).map(
|
|
|
|
([type, records]) => {
|
|
|
|
return html`
|
|
|
|
<article>
|
|
|
|
<h4>${recordTypeNames[type]}</h4>
|
|
|
|
<ul>
|
|
|
|
${records.map(record => html`<li><${FriendlyRecord} type=${type} record=${record}/></li>`)}
|
|
|
|
</ul>
|
|
|
|
</article>
|
|
|
|
`;
|
2022-04-29 16:04:12 +00:00
|
|
|
}
|
2023-02-22 23:02:49 +00:00
|
|
|
)}
|
2022-04-29 16:04:12 +00:00
|
|
|
</div>
|
2023-02-22 23:02:49 +00:00
|
|
|
</article>
|
2022-04-29 02:33:00 +00:00
|
|
|
`;
|
2022-04-29 02:29:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
|
|
|
|
function RecordListFriendly({ zone }) {
|
|
|
|
const [records, setRecords] = useState({});
|
|
|
|
const [editable, setEditable] = useState(false);
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2022-04-29 02:33:00 +00:00
|
|
|
useEffect(() => {
|
|
|
|
getRecords(zone)
|
2023-02-22 23:02:49 +00:00
|
|
|
.then((res) => setRecords(processRecords(res)));
|
|
|
|
}, [zone]);
|
|
|
|
|
|
|
|
return html`
|
|
|
|
${Object.entries(records).map(
|
|
|
|
([name, recordSets]) => {
|
|
|
|
return html`
|
|
|
|
<${RecordsByName} name=${name} recordSets=${recordSets}/>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
)}
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-02-25 01:53:10 +00:00
|
|
|
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
|
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
return html`
|
2023-02-25 01:53:10 +00:00
|
|
|
<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 class="combined-input">
|
|
|
|
<input type="text" id="domain" name="domain" onChange=${e => setDomain(e.target.value)}/>
|
|
|
|
<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); setRealType(''); setRecordData({}); setRealRecordData({})}}>
|
|
|
|
${Object.entries(recordTypeNames).map(([type, name]) => html`<option value="${type}">${name}</option>`)}
|
|
|
|
</select>
|
|
|
|
</div>
|
2023-02-22 23:02:49 +00:00
|
|
|
<div>
|
2023-02-25 01:53:10 +00:00
|
|
|
${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)}/>
|
2023-02-22 23:02:49 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-02-25 01:53:10 +00:00
|
|
|
<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>
|
2023-02-22 23:02:49 +00:00
|
|
|
<div>
|
2023-02-25 01:53:10 +00:00
|
|
|
<input type="submit" onClick=${createNewRecord} value="Ajouter"/>
|
|
|
|
<button role="button" onClick=${onCancel}>Anuler</button>
|
2023-02-22 23:02:49 +00:00
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
`;
|
|
|
|
}
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2023-02-22 23:02:49 +00:00
|
|
|
function ZoneRecords({ zone }) {
|
2023-02-25 01:53:10 +00:00
|
|
|
const [addNewRecord, setAddNewRecord] = useState(true);
|
|
|
|
|
|
|
|
const onCancelNewRecord = (e) => { setAddNewRecord(false); e.preventDefault() };
|
2022-04-29 02:29:10 +00:00
|
|
|
|
2022-04-29 02:33:00 +00:00
|
|
|
return html`
|
2023-02-22 23:02:49 +00:00
|
|
|
<header>
|
|
|
|
<h2>Enregistrements</h2>
|
2023-02-25 01:53:10 +00:00
|
|
|
<button onClick=${() => setAddNewRecord(true)} aria-controls="add-new-record-form" aria-expanded=${addNewRecord} disabled=${addNewRecord}>Ajouter un enregistrement</button>
|
2023-02-22 23:02:49 +00:00
|
|
|
<button>Éditer la zone</button>
|
|
|
|
</header>
|
2023-02-25 01:53:10 +00:00
|
|
|
<${NewRecordFormFriendly} zone=${zone} enabled=${addNewRecord} id="add-new-record-form" onCancel=${onCancelNewRecord}/>
|
|
|
|
<div class="zone-content">
|
|
|
|
<${RecordListFriendly} zone=${zone} />
|
|
|
|
</div>
|
2022-04-29 02:33:00 +00:00
|
|
|
`;
|
2022-04-29 02:29:10 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 17:13:03 +00:00
|
|
|
export default function(element, { zone }) {
|
2023-02-22 23:02:49 +00:00
|
|
|
render(html`<${ZoneRecords} zone=${zone} />`, element);
|
2022-04-29 17:13:03 +00:00
|
|
|
};
|