wip better records management ui

This commit is contained in:
Hannaeko 2023-02-23 00:02:49 +01:00
parent 9fec0cc643
commit d0df3584c8
6 changed files with 273 additions and 96 deletions

View file

@ -1,117 +1,178 @@
import { html, Component, render, createContext, useState, useEffect } from './vendor/preact/standalone.js'; import { html, render, useState, useEffect } from './vendor/preact/standalone.js';
import { getRecords } from './api.js'; import { getRecords } from './api.js';
const rdataInputProperties = { const rdataInputProperties = {
Address: {label: 'adresse', type: 'text'}, Address: {label: 'adresse :', type: 'text'},
Serial: {label: 'serial', type: 'number'}, Serial: {label: 'serial :', type: 'number'},
Minimum: {label: 'minimum', type: 'number'}, Minimum: {label: 'minimum :', type: 'number'},
Retry: {label: 'nouvelle tentative', type: 'number'}, Retry: {label: 'nouvelle tentative :', type: 'number'},
Refresh: {label: 'actualisation', type: 'number'}, Refresh: {label: 'actualisation :', type: 'number'},
MaintainerName: {label: 'contact', type: 'text'}, MaintainerName: {label: 'contact :', type: 'text'},
MasterServerName: {label: 'serveur primaire', type: 'text'}, MasterServerName: {label: 'serveur primaire :', type: 'text'},
Expire: {label: 'expiration', type: 'number'}, Expire: {label: 'expiration :', type: 'number'},
Target: {label: 'cible', type: 'text'}, Target: {label: 'cible :', type: 'text'},
}; };
const Editable = createContext(false); const recordTypes = {
'A': 'address',
'AAAA': 'address',
'SRV': 'service',
'CNAME': 'alias',
'NS': 'name_server',
'SOA': 'soa',
};
const recordTypeNames = {
function RDataInput({ name, value = '', index = 0 }) { 'address': 'Adresse IP',
const {label, type} = rdataInputProperties[name] || {label: name, type: 'text'}; 'service': 'Service',
'alias': 'Alias',
return html` 'name_server': 'Serveur de nom',
<${Editable.Consumer}> 'soa': 'SOA',
${
(editable) => {
if (editable) {
return html`
<div>
<label for=record_${index}_${name}>${label}:</label>
<input id=record_${index}_${name} type=${type} value=${value} />
</div>
`;
} else {
return html`
<div>
<dt>${label}:</dt>
<dd>${value}</dd>
</div>
`;
}
}
}
<//>
`;
} }
function RData({ rdata, index }) { function getNameForRecord(name, type) {
const { Address: address } = rdata; return name;
return Object.entries(rdata).map(([name, value]) => html`<${RDataInput} name=${name} value=${value} index=${index} />`);
} }
function getTypeForRecord(name, type) {
return recordTypes[type];
}
function Record({name, ttl, type, rdata, index = 0}) { function processRecords(records) {
return records.reduce((acc, record) => {
let name = getNameForRecord(record.Name, record.Type);
let type = getTypeForRecord(record.Name, record.Type);
if (!(name in acc)) {
acc[name] = {};
}
if (!(type in acc[name])) {
acc[name][type] = [];
}
acc[name][type].push(record);
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 FriendlyRecord({type, record}) {
let keys = recordsKeys[type](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` return html`
<tr> <article>
<td class=domain>${name}</div> <h3>${name}</h4>
<td class=type>${type}</div> <div>
<td class=ttl>${ttl}</div> ${Object.entries(recordSets).map(
<td class=rdata> ([type, records]) => {
<${Editable.Consumer}> return html`
${ <article>
(editable) => { <h4>${recordTypeNames[type]}</h4>
if (editable) { <ul>
return html`<${RData} rdata=${rdata} index=${index}/>` ${records.map(record => html`<li><${FriendlyRecord} type=${type} record=${record}/></li>`)}
} else { </ul>
return html`<dl><${RData} rdata=${rdata} index=${index}/></dl>` </article>
} `;
}
} }
)}
<//>
</div> </div>
</tr> </article>
`; `;
} }
function RecordList({ zone }) {
const [records, setRecords] = useState([]);
const [editable, setEditable] = useState(false);
const toggleEdit = () => setEditable(!editable);
function RecordListFriendly({ zone }) {
const [records, setRecords] = useState({});
const [editable, setEditable] = useState(false);
useEffect(() => { useEffect(() => {
getRecords(zone) getRecords(zone)
.then((res) => setRecords(res)); .then((res) => setRecords(processRecords(res)));
}, []); }, [zone]);
return html` return html`
<${Editable.Provider} value=${editable}> ${Object.entries(records).map(
<button onclick=${toggleEdit}>${ editable ? 'Save' : 'Edit'}</button> ([name, recordSets]) => {
<table> return html`
<thead>
<tr> <${RecordsByName} name=${name} recordSets=${recordSets}/>
<th>Nom</th> `;
<th>Type</th> }
<th>TTL</th> )}
<th>Données</th> `;
</tr> }
</thead>
<tbody> function NewRecordFormFriendly({ zone }) {
${records.map( return html`
({Name, Class, TTL, Type, ...rdata}, index) => { <form class="new-record">
return html`<${Record} name=${Name} ttl=${TTL} type=${Type} rdata=${rdata} index=${index}/>` <div>
} <label for="domain">Domaine</label>
)} <div>
</tbody> <input type="text" id="domain"/>
</ul> <span>.${ zone }</span>
<//> </div>
</div>
<div>
<label for="record_type">Type d'enregistrement</label>
<select id="record_type">
<option>Adresse IP</option>
<option>Alias</option>
</select>
</div>
<div>
<input type="text"/>
</div>
<div>
<input type="submit" value="Enregistrer"/>
<button role="button">Anuler</button>
</div>
</form>
`;
}
function ZoneRecords({ zone }) {
const [addNewRecord, setAddNewRecord] = useState(false);
const [newRecords, setNewRecords] = useState([]);
return html`
<header>
<h2>Enregistrements</h2>
<button onClick=${() => setNewRecords([...newRecords, NewRecordFormFriendly({ zone })])}>Ajouter un enregistrement</button>
<button>Éditer la zone</button>
</header>
${newRecords}
<${RecordListFriendly} zone=${zone} />
`; `;
} }
export default function(element, { zone }) { export default function(element, { zone }) {
render(html`<${RecordList} zone=${zone} />`, element); render(html`<${ZoneRecords} zone=${zone} />`, element);
}; };

View file

@ -3,12 +3,13 @@ body {
min-height: 100vh; min-height: 100vh;
min-width: 100vw; min-width: 100vw;
margin: 0; margin: 0;
font-family: sans-serif;
} }
:root { :root {
--color-primary: #712da0; --color-primary: #5e0c97;
--color-hightlight-1: #ffbac6; --color-hightlight-1: #ffd4ba;
--color-hightlight-2: #f560f5; --color-hightlight-2: #dd39dd;
--color-contrast: white; --color-contrast: white;
} }
@ -23,6 +24,10 @@ h1 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
a {
color: var(--color-primary)
}
p.feedback { p.feedback {
padding: .35rem; padding: .35rem;
margin: 0; margin: 0;
@ -33,13 +38,14 @@ p.feedback.error {
color: #710000; color: #710000;
} }
button,
input { input {
padding: .35rem .35rem; padding: .35rem .35rem;
font-size: 1rem; font-size: 1rem;
} }
button,
input[type="submit"] { input[type="submit"] {
margin-top: 2rem;
background: var(--color-primary); background: var(--color-primary);
color: var(--color-contrast); color: var(--color-contrast);
border-left: 5px solid var(--color-hightlight-1); border-left: 5px solid var(--color-hightlight-1);
@ -48,12 +54,21 @@ input[type="submit"] {
border-bottom: 5px solid var(--color-hightlight-2); border-bottom: 5px solid var(--color-hightlight-2);
} }
button:hover,
input[type="submit"]:hover { input[type="submit"]:hover {
background: #7a43a1; background: #7a43a1;
} }
button:active ,
input[type="submit"]:active { input[type="submit"]:active {
background: #875ba6; 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);
}
form input[type="submit"] {
margin-top: 2rem;
} }
form label { form label {
@ -71,7 +86,7 @@ nav.main {
display: flex; display: flex;
flex: 0; flex: 0;
padding: 1rem; padding: 1rem;
border-right: 5px solid var(--color-hightlight-1); border-right: 5px solid var(--color-hightlight-2);
} }
nav.main a { nav.main a {

View file

@ -12,3 +12,105 @@ nav.secondary li {
main { main {
flex-direction: column; flex-direction: column;
} }
main > section {
max-width: 120ch;
}
header {
display: flex;
margin: 1rem 0;
}
h2 {
margin: 0;
flex-grow: 1;
}
header > :not(:last-of-type) {
margin-right: 2ch;
}
zone-content h3, zone-content h4 {
margin: 0;
font-weight: normal;
font-size: 1rem;
width: 30%;
}
zone-content article {
display: flex;
}
zone-content > article > div {
flex-grow: 1;
}
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 > *{
margin-right: 2ch;
}
zone-content article ul {
padding: 0;
margin: 0;
list-style-type: none;
}
zone-content article dl {
display: grid;
grid-template: auto / max-content 1fr;
}
zone-content article dd {
margin: 0;
}
zone-content article dt span {
display: inline-block;
background-color: var(--color-hightlight-1);
padding: 0.1em 0.5em;
border-radius: 0.5em;
margin-right: 0.1rem;
font-size: .7rem;
}
form.new-record {
display: flex;
flex-direction: row;
}
form.new-record > div {
display: flex;
flex-direction: column;
margin-right: 2ch;
}
form.new-record > div:first-child {
width: 30%;
}
form.new-record > div:first-child span {
flex-grow: 1;
font-size: .8rem;
}
form.new-record > div:nth-child(2) {
width: calc( .3 * (70% - 4ch));
}
form.new-record > div:nth-child(2) select {
flex-grow: 1;
}
form.new-record > div:nth-child(3) {
flex: 1;
}

View file

@ -13,7 +13,7 @@ pub async fn do_login(
auth_request: models::AuthTokenRequest, auth_request: models::AuthTokenRequest,
cookies: &CookieJar<'_> cookies: &CookieJar<'_>
) -> Result<models::Session, models::UserError> { ) -> Result<models::Session, models::UserError> {
let session_duration = config.web_app.token_duration; let session_duration = config.web_app.token_duration;
let session = conn.run(move |c| { let session = conn.run(move |c| {
let user_info = models::LocalUser::get_user_by_creds( let user_info = models::LocalUser::get_user_by_creds(

View file

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<nav aria-label="Principal" class="main"> <nav aria-label="Principal" class="main">
<ul> <ul>
<li><a href="/profil">Mon profile</a></li> <li><a href="/profile">Mon profil</a></li>
<li> <li>
{{ macros::nav_link( {{ macros::nav_link(
content="Mes zones", content="Mes zones",

View file

@ -27,7 +27,6 @@
</ul> </ul>
</nav> </nav>
<section> <section>
<h2>Enregistrements</h2>
<zone-content> <zone-content>
</zone-content> </zone-content>
</section> </section>