add record creation in ui
This commit is contained in:
parent
08b21ac010
commit
5f73738465
13 changed files with 253 additions and 146 deletions
49
assets/scripts/add-form-row.js
Normal file
49
assets/scripts/add-form-row.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
function templateString(text, args) {
|
||||
for (const argName in args) {
|
||||
text = text.replace(`{${argName}}`, args[argName]);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function addFormRow(templateName) {
|
||||
const allRows = document.querySelectorAll(`[data-new-item-template="${templateName}"]`);
|
||||
const templateElement = allRows[0];
|
||||
const nextId = allRows.length + 1;
|
||||
|
||||
const newItem = templateElement.cloneNode(true);
|
||||
|
||||
newItem.querySelectorAll('[data-new-item-skip]')
|
||||
.forEach(node => node.remove());
|
||||
|
||||
newItem.querySelectorAll('input')
|
||||
.forEach(input => { input.value = '' });
|
||||
|
||||
const templatedAttrNodes = newItem.querySelectorAll('[data-new-item-template-attr]');
|
||||
for (const node of templatedAttrNodes) {
|
||||
const attributes = node.dataset.newItemTemplateAttr.split(/\s/);
|
||||
for (const attribute of attributes) {
|
||||
const templatedString = node.getAttribute(`data-template-${attribute}`);
|
||||
node.setAttribute(attribute, templateString(templatedString, {i: nextId}));
|
||||
}
|
||||
}
|
||||
|
||||
const templatedNodes = newItem.querySelectorAll('[data-new-item-template-content]');
|
||||
for (const node of templatedNodes) {
|
||||
const templatedString = node.dataset.newItemTemplateContent;
|
||||
node.innerHTML = templateString(templatedString, {i: nextId})
|
||||
}
|
||||
|
||||
allRows[allRows.length - 1].insertAdjacentElement('afterend', newItem);
|
||||
}
|
||||
|
||||
function setUpAddFormRow() {
|
||||
const buttons = document.querySelectorAll('button[data-new-item]');
|
||||
|
||||
for (const button of buttons) {
|
||||
button.addEventListener('click', () => addFormRow(button.dataset.newItem))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setUpAddFormRow);
|
|
@ -34,15 +34,13 @@ h2 {
|
|||
|
||||
article.domain {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
article.domain header {
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
article.domain header h3.folder-tab {
|
||||
h3.folder-tab {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -53,9 +51,8 @@ article.domain header h3.folder-tab {
|
|||
margin: 0;
|
||||
font-weight: inherit;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
article.domain header h3.folder-tab ~ .sep {
|
||||
~ .sep {
|
||||
content: '';
|
||||
width: 3em;
|
||||
background-color: #f2e0fd;
|
||||
|
@ -63,72 +60,81 @@ article.domain header h3.folder-tab ~ .sep {
|
|||
clip-path: url("#corner-folder-tab-right");
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
article.domain .records {
|
||||
.records {
|
||||
background: #f2e0fd;
|
||||
padding: 1rem;
|
||||
border-radius: 0 .3rem .3rem .3rem;
|
||||
|
||||
button,
|
||||
a.button {
|
||||
background-color: #f2e0fd;
|
||||
}
|
||||
|
||||
article.domain .records h4 {
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
article.domain .records > ul {
|
||||
> ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rtype {
|
||||
.rrset {
|
||||
.rtype {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
article.domain .records .rrset ul {
|
||||
ul {
|
||||
padding: 1rem 0 1rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
|
||||
article.domain .records .rrset li {
|
||||
li {
|
||||
align-items: baseline;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata {
|
||||
.rdata {
|
||||
display: flex;
|
||||
gap: .2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata-main {
|
||||
.rdata-main {
|
||||
display: flex;
|
||||
gap: .3rem;
|
||||
margin-right: .1rem;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata-main .pill {
|
||||
.pill {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata-complementary {
|
||||
.rdata-complementary {
|
||||
font-size: .9em;
|
||||
gap: .2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .action {
|
||||
.action {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
position: relative;
|
||||
top: .15rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
border: .1rem solid #bd79bd;
|
||||
|
@ -151,36 +157,27 @@ a.button {
|
|||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color .2s, color .2s;
|
||||
}
|
||||
|
||||
button svg,
|
||||
a.button svg {
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.records button,
|
||||
.records a.button {
|
||||
background-color: #f2e0fd;
|
||||
}
|
||||
|
||||
button.icon,
|
||||
a.button.icon {
|
||||
&.icon {
|
||||
padding: 0;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button:focus-visible,
|
||||
a.button:hover,
|
||||
a.button:focus-visible {
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: white;
|
||||
background-color: #850085;
|
||||
}
|
||||
}
|
||||
|
||||
form h3 {
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
@ -208,7 +205,7 @@ textarea {
|
|||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
|
@ -217,3 +214,23 @@ textarea {
|
|||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-action {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
button.form-new-item {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
border-radius: 0;
|
||||
color: #850085;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: none;
|
||||
text-decoration: none;
|
||||
color: #850085;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,3 +37,4 @@ record-input-addresses =
|
|||
address, like <code>2001:db8:cafe:bc68::2</code>.
|
||||
|
||||
button-save-configuration = Save configuration
|
||||
button-add-address = Add an other address
|
||||
|
|
|
@ -32,8 +32,9 @@ zone-content-new-record-button = Nouvel enregistrement
|
|||
|
||||
record-input-addresses =
|
||||
.input-label = Adresse IP #{ $index }
|
||||
.error-record-parse-ip = Format d'adresse IP inconnu. L'adresse IP doit être
|
||||
.error-record-parse-ip = Format d’adresse IP inconnu. L’adresse IP doit être
|
||||
soit une adresse IPv4, comme <code>198.51.100.3</code>, soit une adresse IPv6,
|
||||
comme <code>2001:db8:cafe:bc68::2</code>.
|
||||
|
||||
button-save-configuration = Sauvegarder la configuration
|
||||
button-add-address = Ajouter une autre adresse
|
||||
|
|
|
@ -111,19 +111,19 @@ impl Node {
|
|||
}).collect()
|
||||
}
|
||||
|
||||
pub fn to_json_value(self) -> serde_json::Value {
|
||||
pub fn into_json_value(self) -> serde_json::Value {
|
||||
match self {
|
||||
Node::Value(value) => serde_json::Value::String(value),
|
||||
Node::Map(map) => {
|
||||
let map = map.into_iter()
|
||||
.map(|(key, node)| (key, node.to_json_value()))
|
||||
.map(|(key, node)| (key, node.into_json_value()))
|
||||
.collect();
|
||||
serde_json::Value::Object(map)
|
||||
},
|
||||
Node::Sequence(list) => {
|
||||
let array = list.to_vec()
|
||||
.into_iter()
|
||||
.map(|node| node.to_json_value())
|
||||
.map(|node| node.into_json_value())
|
||||
.collect();
|
||||
|
||||
serde_json::Value::Array(array)
|
||||
|
@ -245,6 +245,6 @@ mod tests {
|
|||
]
|
||||
});
|
||||
|
||||
assert_eq!(parsed_form.to_json_value(), json_value);
|
||||
assert_eq!(parsed_form.into_json_value(), json_value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -458,6 +458,5 @@ impl TryFrom<internal::Record> for RecordImpl {
|
|||
let ttl = Ttl::from_secs(value.ttl);
|
||||
let data = value.rdata.try_into()?;
|
||||
Ok(Record::new(owner, Class::IN, ttl, data))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::validation;
|
||||
use crate::errors::Error;
|
||||
use crate::macros::{append_errors, check_type, get_object_value, push_error};
|
||||
use crate::resources::dns::internal::{self, Name};
|
||||
use crate::resources::dns::internal::{self, Name, Record};
|
||||
|
||||
use super::{rdata, FriendlyRType};
|
||||
use super::FromValue;
|
||||
|
@ -16,6 +16,21 @@ pub enum ConfigurationType {
|
|||
Web,
|
||||
}
|
||||
|
||||
impl ConfigurationType {
|
||||
pub fn get_records(&self, input_data: serde_json::Value, default_ttl: u32, zone_name: Name) -> Result<Vec<Record>, Vec<Error>> {
|
||||
match self {
|
||||
ConfigurationType::Mail => {
|
||||
NewSectionMail::from_value(input_data.clone())
|
||||
.map(|section| section.internal(default_ttl, zone_name))
|
||||
},
|
||||
ConfigurationType::Web => {
|
||||
NewSectionWeb::from_value(input_data.clone())
|
||||
.map(|section| section.internal(default_ttl, zone_name))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewRecordQuery {
|
||||
pub name: Option<String>,
|
||||
|
@ -59,7 +74,7 @@ pub struct NewRecord<T> {
|
|||
}
|
||||
|
||||
impl<T: FromValue> FromValue for NewRecord<T> {
|
||||
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let object = check_type!(value, Object, errors);
|
||||
|
@ -113,7 +128,7 @@ pub struct NewRequiredRecord<T> {
|
|||
}
|
||||
|
||||
impl<T: FromValue> FromValue for NewRequiredRecord<T> {
|
||||
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let object = check_type!(value, Object, errors);
|
||||
|
|
|
@ -304,11 +304,10 @@ impl FromValue for Spf {
|
|||
|
||||
impl ToInternal for Spf {
|
||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
||||
|
||||
vec![
|
||||
internal::Record {
|
||||
ttl,
|
||||
name: Name::new(format!("_spf.{node_name}")),
|
||||
name: node_name,
|
||||
rdata: internal::RData::Txt(internal::Txt::new(self.policy))
|
||||
}
|
||||
]
|
||||
|
|
|
@ -14,6 +14,9 @@ pub struct RecordList {
|
|||
}
|
||||
|
||||
impl RecordList {
|
||||
pub fn new(records: &[Record]) -> Self {
|
||||
RecordList { records: records.into() }
|
||||
}
|
||||
pub fn sort(&mut self) {
|
||||
self.records.sort_by(|r1, r2| {
|
||||
let key1 = (&r1.name, r1.rdata.rtype());
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use async_trait::async_trait;
|
||||
use axum::response::Response;
|
||||
use domain::base::record;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rusqlite::Error as RusqliteError;
|
||||
|
||||
|
@ -7,7 +9,7 @@ use crate::database::{BoxedDb, sqlite::SqliteDB};
|
|||
use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError};
|
||||
use crate::errors::Error;
|
||||
use crate::macros::push_error;
|
||||
use crate::resources::dns::internal::RecordList;
|
||||
use crate::resources::dns::internal::{self, RecordList};
|
||||
use crate::validation;
|
||||
|
||||
pub enum ZoneError {
|
||||
|
@ -48,6 +50,12 @@ impl Zone {
|
|||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
pub async fn add_records(&self, record_driver: BoxedRecordDriver, records: internal::RecordList) -> Result<(), Error> {
|
||||
record_driver.add_records(&self.name, records).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use axum::extract::{Query, Path, State, OriginalUri};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Extension;
|
||||
use serde_json::{Value, json};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
@ -8,7 +10,7 @@ use crate::macros::append_errors;
|
|||
use crate::AppState;
|
||||
use crate::errors::{Error, error_map};
|
||||
use crate::template::Template;
|
||||
use crate::resources::dns::friendly::{self, NewRecordQuery, ConfigurationType, FromValue, NewSectionMail, NewSectionWeb, ToInternal};
|
||||
use crate::resources::dns::friendly::{self, NewRecordQuery};
|
||||
use crate::resources::dns::internal;
|
||||
|
||||
pub async fn get_records_page(
|
||||
|
@ -68,33 +70,21 @@ pub async fn post_new_record(
|
|||
OriginalUri(url): OriginalUri,
|
||||
Extension(lang): Extension<LanguageIdentifier>,
|
||||
form: Node,
|
||||
) -> Result<Template<'static, Value>, Error> {
|
||||
) -> Result<Response, Error> {
|
||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||
let mut errors = Vec::new();
|
||||
append_errors!(params.validate(&zone.name), errors);
|
||||
|
||||
if !errors.is_empty() || params.name.is_none() || !(params.config.is_none() ^ params.config.is_some()) {
|
||||
// TODO: return 404
|
||||
todo!()
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
}
|
||||
|
||||
let name = params.name.clone().unwrap();
|
||||
let input_data = form.to_json_value();
|
||||
|
||||
let new_records = if errors.is_empty() {
|
||||
let name = internal::Name::new(name);
|
||||
let name = params.name.clone().map(internal::Name::new).unwrap();
|
||||
let input_data = form.into_json_value();
|
||||
|
||||
let new_records = {
|
||||
let new_records = if let Some(config_type) = params.config.clone() {
|
||||
match config_type {
|
||||
ConfigurationType::Mail => {
|
||||
NewSectionMail::from_value(input_data.clone())
|
||||
.map(|section| section.internal(3600, name))
|
||||
},
|
||||
ConfigurationType::Web => {
|
||||
NewSectionWeb::from_value(input_data.clone())
|
||||
.map(|section| section.internal(3600, name))
|
||||
},
|
||||
}
|
||||
config_type.get_records(input_data.clone(), 3600, name)
|
||||
} else if let Some(_rtype) = params.rtype {
|
||||
unimplemented!()
|
||||
} else {
|
||||
|
@ -102,11 +92,16 @@ pub async fn post_new_record(
|
|||
};
|
||||
|
||||
append_errors!(new_records, errors)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !errors.is_empty() {
|
||||
if let Some(new_records) = new_records {
|
||||
zone.add_records(
|
||||
app.records,
|
||||
internal::RecordList::new(&new_records)
|
||||
).await?;
|
||||
|
||||
Ok(().into_response())
|
||||
} else {
|
||||
Ok(Template::new(
|
||||
"pages/new_record.html",
|
||||
app.template_engine,
|
||||
|
@ -120,10 +115,7 @@ pub async fn post_new_record(
|
|||
"url": url.to_string(),
|
||||
"lang": lang.to_string(),
|
||||
})
|
||||
))
|
||||
} else {
|
||||
println!("{:#?}", new_records);
|
||||
todo!()
|
||||
).into_response())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,3 +12,7 @@
|
|||
{% block main %}{% endblock main %}
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/assets/scripts/add-form-row.js"></script>
|
||||
{% endblock scripts %}
|
||||
|
|
|
@ -14,19 +14,27 @@
|
|||
|
||||
{% for address in input_data.addresses.data.addresses | default(value=[""]) %}
|
||||
{% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %}
|
||||
<div class="form-input">
|
||||
<label for="address-{{ loop.index0 }}">
|
||||
<div class="form-input" data-new-item-template="address">
|
||||
<label
|
||||
for="address-{{ loop.index0 }}"
|
||||
data-new-item-template-attr="for"
|
||||
data-template-for="address-{i}"
|
||||
data-new-item-template-content="{{ tr(msg="record-input-addresses", attr="input-label", index="{i}", lang=lang) }}"
|
||||
>
|
||||
{{ tr(msg="record-input-addresses", attr="input-label", index=loop.index, lang=lang) }}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="addresses[data][addresses][{{ loop.index0 }}][address]"
|
||||
id="address-{{ loop.index0 }}"
|
||||
data-new-item-template-attr="name id"
|
||||
data-template-name="addresses[data][addresses][{i}][address]"
|
||||
data-template-id="address-{i}"
|
||||
{% if domain_error %}aria-invalid="true"{% endif %}
|
||||
value="{{ address.address | default(value="") }}"
|
||||
>
|
||||
{% if address_error %}
|
||||
<p class="error" id="address-{{ loop.index0 }}-error">
|
||||
<p class="error" id="address-{{ loop.index0 }}-error" data-new-item-skip>
|
||||
{{ tr(
|
||||
msg="record-input-addresses",
|
||||
attr="error-" ~ address_error.code | replace(from=":", to="-"),
|
||||
|
@ -34,9 +42,18 @@
|
|||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button class="form-new-item" type="button" data-new-item="address">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
||||
</svg>
|
||||
{{ tr(msg="button-add-address", lang=lang) }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-action">
|
||||
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% elif config == "mail" %}
|
||||
|
@ -104,6 +121,8 @@
|
|||
</fieldset>
|
||||
|
||||
|
||||
<button type="submit">Save configuration</button>
|
||||
<div class="form-action">
|
||||
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in a new issue