diff --git a/public/scripts/records.js b/public/scripts/records.js index aab7604..d427d6c 100644 --- a/public/scripts/records.js +++ b/public/scripts/records.js @@ -13,6 +13,12 @@ const rdataInputProperties = { 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 = { @@ -30,7 +36,7 @@ const realRecordDataConfig = { }, 'SRV': { friendlyType: 'service', - fields: [ /* TODO */ ], + fields: [ 'Priority', 'Weight', 'Port', 'Server' ], }, 'NS': { friendlyType: 'name_server', @@ -43,38 +49,84 @@ const realRecordDataConfig = { }; function defaultBuildData(realRecordType) { - const defaultFields = realRecordDataConfig[realRecordType].fields.map(field => [field, null]); + const defaultFields = Object.fromEntries(realRecordDataConfig[realRecordType].fields.map(field => [field, null])); return (fields) => { return {...defaultFields, ...fields, Type: realRecordType}; } } -function defaultRecordToField(realRecord) { +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: defaultRecordToField, + realRecordToFields: defaultRecordToFields, fields: realRecordDataConfig['AAAA'].fields, buildData: buildAddressRecord, + getName: defaultGetName, }, 'alias': { - realRecordToFields: defaultRecordToField, + realRecordToFields: defaultRecordToFields, fields: realRecordDataConfig['CNAME'].fields, buildData: defaultBuildData('CNAME'), + getName: defaultGetName, }, 'name_server': { - realRecordToFields: defaultRecordToField, + realRecordToFields: defaultRecordToFields, fields: realRecordDataConfig['CNAME'].fields, buildData: defaultBuildData('NS'), + getName: defaultGetName, }, 'soa': { - realRecordToFields: defaultRecordToField, + 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, }, }; @@ -87,11 +139,6 @@ const recordTypeNames = { 'soa': 'SOA', } -/* Type to use for SRV to derive name without port / service */ -function getNameForRecord(name, type) { - return name; -} - /* Name to use with _spf for example */ function getFriendlyTypeForRecord(name, type) { return realRecordDataConfig[type].friendlyType; @@ -99,8 +146,8 @@ function getFriendlyTypeForRecord(name, type) { function processRecords(records) { return records.reduce((acc, record) => { - let name = getNameForRecord(record.Name, record.Type); let type = getFriendlyTypeForRecord(record.Name, record.Type); + let name = friendlyRecordDataConfig[type].getName(record.Name); if (!(name in acc)) { acc[name] = {}; } @@ -112,13 +159,6 @@ function processRecords(records) { }, {}); } -function buildAddressRecord({ Address = ''}) { - return { - Type: Address.indexOf(':') > -1 ? 'AAAA' : 'A', - Address - } -} - function FriendlyRecord({type, record}) { let keys = friendlyRecordDataConfig[type].realRecordToFields(record); if (keys.length == 1) { @@ -154,8 +194,6 @@ function RecordsByName({ name, recordSets }) { `; } - - function RecordListFriendly({ zone }) { const [records, setRecords] = useState({}); const [editable, setEditable] = useState(false); @@ -176,19 +214,21 @@ function RecordListFriendly({ zone }) { `; } -function NewRecordFormFriendly({ zone, enabled, id, onCancel }) { +function NewRecordFormFriendly({ zone }) { + const defaultVaules = {Name: '', TTL: 3600, Class: 'IN'}; const [recordType, setRecordType] = useState(Object.keys(recordTypeNames)[0]); - const [recordData, setRecordData] = useState({}); + const [recordData, setRecordData] = useState(defaultVaules); const [realRecordData, setRealRecordData] = useState({}); const [realType, setRealType] = useState(''); - const [domain, setDomain] = useState(''); - const [ttl, setTTL] = useState(3600); + + const absoluteName = (name) => name ? `${name}.${zone}` : zone; const setRecordDataFactory = (field) => { return (e) => { const newData = {...recordData}; - newData[field] = e.target.value; - const newRealRecordData = friendlyRecordDataConfig[recordType].buildData(newData) + 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); @@ -197,75 +237,90 @@ function NewRecordFormFriendly({ zone, enabled, id, onCancel }) { const createNewRecord = (e) => { e.preventDefault(); - const newRecords = [{...realRecordData, Class: 'IN', TTL: ttl, Name: `${domain}.${zone}`}]; + const newRecords = [realRecordData]; console.log(newRecords) createRecords(zone, newRecords); } - // TODO: Reset valeurs champs quand changement de type => bound la valeur de l'input au state + 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` -
-

Nouvel enregistrement

-
-
- -
- setDomain(e.target.value)}/> - .${ zone } +
+
+

Nouvel enregistrement

+
+ +
+
+ +
+ + .${ zone } +
+
+
+ + +
+
+ ${friendlyRecordDataConfig[recordType].fields.map(fieldName => html` +
+ + +
+ `)} +
+
+ +
-
- - -
+
+

Prévisualisation des changements

+

+ Ajout + + ${realRecordData.Name === zone ? '@' : realRecordData.Name} ${realRecordData.TTL} ${realRecordData.Class} ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''} + +

+
- ${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(true); - - const onCancelNewRecord = (e) => { setAddNewRecord(false); e.preventDefault() }; - return html` -
-

Enregistrements

- - -
- <${NewRecordFormFriendly} zone=${zone} enabled=${addNewRecord} id="add-new-record-form" onCancel=${onCancelNewRecord}/> -
- <${RecordListFriendly} zone=${zone} /> -
+ <${NewRecordFormFriendly} zone=${zone}/> + +
+
+

Contenu de la zone

+ +
+ +
+ <${RecordListFriendly} zone=${zone} /> +
+
`; } diff --git a/public/styles/zone.css b/public/styles/zone.css index f34ab82..81a761b 100644 --- a/public/styles/zone.css +++ b/public/styles/zone.css @@ -35,6 +35,7 @@ header > :not(:last-of-type) { .zone-content article { display: flex; + flex-wrap: wrap; } .zone-content > article > div { @@ -78,26 +79,22 @@ header > :not(:last-of-type) { font-size: .7rem; } -form.new-record { +.new-record form { width: auto; } -form.new-record > div.form-row > div:not(:last-child) { +.new-record form > div.form-row > * { flex-grow: 1; - margin-right: 2ch; } -form.new-record > div.form-row { +.new-record form > div.form-row { display: flex; flex-wrap: wrap; + gap: 2ch; } -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)); +.new-record form label { + margin-top: .25rem; } form div.input-group { @@ -126,32 +123,52 @@ form.disabled { display: none; } -input[name^="ttl"] { - max-width: 10ch; -} - -form.new-record button, -form.new-record input[type="submit"] { +.new-record form button, +.new-record form input[type="submit"] { margin-right: 1ch; margin-top: .75rem; } -form.new-record h3 { - margin: 0; + +.new-record header { + margin-bottom: 0; } -form.new-record .preview { + +.new-record form .preview { margin: .5rem 0; border: 1px solid rgb(var(--color-primary)); } -form.new-record .preview pre { - margin: .5rem 0 .5rem 0; - padding: 0 .5rem; +.new-record form .preview p:first-of-type { + margin-top: .5rem; } -form.new-record .preview h4 { +.new-record form .preview p:last-of-type { + margin-bottom: .5rem; +} + +.new-record form .preview p { + display: flex; +} + +.new-record form .preview code { + padding: 0 .5rem; + flex: 1; +} + +.new-record form .preview img { + padding: 0 .25rem; + border-right: 1px solid #1b841b; +} + +.new-record form .preview .addition { + background: #d9fbd9; +} + +.new-record form .preview h3 { margin: 0; padding: .0rem .5rem 0 .5rem;; - border-bottom: 1px solid rgb(var(--color-primary)); font-size: 1rem; - font-weight: bold; + font-weight: normal; + background: rgb(var(--color-primary)); + color: var(--color-contrast) }