wip better records management ui
This commit is contained in:
parent
9fec0cc643
commit
d0df3584c8
6 changed files with 273 additions and 96 deletions
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<section>
|
<section>
|
||||||
<h2>Enregistrements</h2>
|
|
||||||
<zone-content>
|
<zone-content>
|
||||||
</zone-content>
|
</zone-content>
|
||||||
</section>
|
</section>
|
||||||
|
|
Loading…
Reference in a new issue