Added first parts of auth editor

This commit is contained in:
Lukas Metzger 2018-04-12 09:27:56 +02:00
parent 5d2ef81610
commit 5208c36d8e
11 changed files with 453 additions and 14 deletions

View file

@ -0,0 +1,20 @@
export class RecordApitype {
public id = 0;
public name = '';
public type = '';
public content = '';
public priority = 0;
public ttl = 0;
public domain = 0;
constructor(init: Object) {
Object.assign(this, init);
}
}

View file

@ -1,3 +1,5 @@
import { EditAuthLineComponent } from './pages/edit-auth/edit-auth-line.component';
import { RecordsOperation } from './operations/records.operations';
import { LoggedOutGuard } from './services/logged-out-guard.service';
import { NativeGuard } from './services/native-guard.service';
import { SearchComponent } from './partials/search/search.component';
@ -64,7 +66,8 @@ import { UsersComponent } from './pages/users/users.component';
UsersComponent,
EditUserComponent,
CreateUserComponent,
SearchComponent
SearchComponent,
EditAuthLineComponent
],
imports: [
BrowserModule,
@ -79,6 +82,7 @@ import { UsersComponent } from './pages/users/users.component';
PasswordOperation,
DomainsOperation,
UsersOperation,
RecordsOperation,
AuthGuard,
AdminGuard,
NativeGuard,

View file

@ -0,0 +1,99 @@
import { RecordApitype } from './../apitypes/Record.apitype';
import { SoaApitype } from './../apitypes/Soa.apitype';
import { DomainApitype } from './../apitypes/Domain.apitype';
import { ListApitype } from './../apitypes/List.apitype';
import { Injectable } from '@angular/core';
import { HttpService } from '../services/http.service';
import { StateService } from '../services/state.service';
import { SessionApitype } from '../apitypes/Session.apitype';
@Injectable()
export class RecordsOperation {
constructor(private http: HttpService, private gs: StateService) { }
public async getListForDomain(domainId: number, page?: number, pageSize?: number, queryName?: string,
type?: Array<string>, queryContent?: string, sort?: Array<String> | string, ): Promise<ListApitype<RecordApitype>> {
try {
return new ListApitype<RecordApitype>(await this.http.get('/records', {
domain: domainId,
page: page,
pagesize: pageSize,
queryName: queryName,
type: type,
queryContent: queryContent,
sort: sort
}));
} catch (e) {
console.error(e);
return new ListApitype<RecordApitype>({ paging: {}, results: [] });
}
}
public async delete(recordId: number): Promise<boolean> {
try {
await this.http.delete(['/records', recordId.toString()]);
return true;
} catch (e) {
console.error(e);
return false;
}
}
public async getSingle(recordId: number): Promise<RecordApitype> {
try {
return new RecordApitype(await this.http.get(['/records', recordId.toString()]));
} catch (e) {
console.error(e);
return new RecordApitype({});
}
}
public async updateRecord(recordId: number, name?: string, type?: string, content?: string,
priority?: number, ttl?: number): Promise<boolean> {
const data = {};
if (name !== null && name !== undefined) {
data['name'] = name;
}
if (type !== null && type !== undefined) {
data['type'] = type;
}
if (content !== null && content !== undefined) {
data['content'] = content;
}
if (priority !== null && priority !== undefined) {
data['priority'] = priority;
}
if (ttl !== null && ttl !== undefined) {
data['ttl'] = ttl;
}
try {
await this.http.put(['/records', recordId.toString()], data);
return true;
} catch (e) {
console.error(e);
return false;
}
}
public async create(domainId: number, name: string, type: string, content: string,
priority: number, ttl: number): Promise<RecordApitype> {
try {
const result = new RecordApitype(await this.http.post('/records', {
name: name,
type: type,
content: content,
priority: priority,
ttl: ttl,
domain: domainId
}));
return result;
} catch (e) {
console.error(e);
return new RecordApitype({});
}
}
}

View file

@ -0,0 +1,48 @@
<td>
<span>{{ entry.id }}</span>
</td>
<td class="align-middle py-0">
<div class="text-nowrap text-truncate" *ngIf="!editMode">{{ fullName() }}</div>
<div class="input-group input-group-sm" *ngIf="editMode">
<input class="form-control" type="text" [formControl]="inputName">
<div class="input-group-append">
<span class="input-group-text">{{ domainPart() }}</span>
</div>
</div>
</td>
<td class="align-middle py-0">
<span *ngIf="!editMode">{{ inputType.value }}</span>
<app-select *ngIf="editMode" [options]="gs.recordTypes" notNull [formControl]="inputType"></app-select>
</td>
<td class="align-middle py-0">
<div class="text-nowrap text-truncate" *ngIf="!editMode">{{ inputContent.value }}</div>
<input *ngIf="editMode" class="form-control form-control-sm" type="text" [formControl]="inputContent">
</td>
<td class="align-middle py-0">
<span *ngIf="!editMode">{{ inputPriority.value }}</span>
<div *ngIf="editMode" class="form-group m-0 position-relative">
<input class="form-control form-control-sm auto-invalid" type="text" [formControl]="inputPriority">
<div class="invalid-tooltip w-200 mw-200">
Must be positive integer.
</div>
</div>
</td>
<td class="align-middle py-0">
<span *ngIf="!editMode">{{ inputTtl.value }}</span>
<div *ngIf="editMode" class="form-group m-0 position-relative">
<input class="form-control form-control-sm auto-invalid" type="text" [formControl]="inputTtl">
<div class="invalid-tooltip w-200 mw-200">
Must be positive integer.
</div>
</div>
</td>
<td class="align-middle text-center py-0">
<span *ngIf="!editMode">
<app-fa-icon class="cursor-pointer mx-1" icon="edit" appStopPropagateClick (click)="onEditClick()"></app-fa-icon>
<app-fa-icon class="cursor-pointer mx-1" icon="trash" appStopPropagateClick></app-fa-icon>
<app-fa-icon class="cursor-pointer mx-1" icon="key" appStopPropagateClick></app-fa-icon>
</span>
<span *ngIf="editMode">
<button class="btn btn-primary btn-sm w-100" (click)="onSave()">Save</button>
</span>
</td>

View file

@ -0,0 +1,81 @@
import { RecordsOperation } from './../../operations/records.operations';
import { StateService } from './../../services/state.service';
import { DomainApitype } from './../../apitypes/Domain.apitype';
import { FormControl, FormBuilder, Validators } from '@angular/forms';
import { RecordApitype } from './../../apitypes/Record.apitype';
import { Component, OnInit, Input, OnChanges, SimpleChanges, EventEmitter, Output } from '@angular/core';
@Component({
// tslint:disable-next-line:component-selector
selector: '[app-edit-auth-line]',
templateUrl: './edit-auth-line.component.html'
})
export class EditAuthLineComponent implements OnInit, OnChanges {
@Input() entry: RecordApitype;
@Input() domain: DomainApitype;
@Output() recordUpdated = new EventEmitter<void>();
public editMode = false;
public inputName: FormControl;
public inputType: FormControl;
public inputContent: FormControl;
public inputPriority: FormControl;
public inputTtl: FormControl;
constructor(private fb: FormBuilder, public gs: StateService, private records: RecordsOperation) {
this.setupFormControls();
}
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges): void {
this.editMode = false;
this.inputName.reset(this.recordPart());
this.inputType.reset(this.entry.type);
this.inputContent.reset(this.entry.content);
this.inputPriority.reset(this.entry.priority);
this.inputTtl.reset(this.entry.ttl);
}
public async setupFormControls() {
this.inputName = this.fb.control('');
this.inputType = this.fb.control('');
this.inputContent = this.fb.control('');
this.inputPriority = this.fb.control('', [Validators.required, Validators.pattern(/^[0-9]+$/)]);
this.inputTtl = this.fb.control('', [Validators.required, Validators.pattern(/^[0-9]+$/)]);
}
public async onEditClick() {
this.editMode = true;
}
public domainPart(): string {
return '.' + this.domain.name;
}
public recordPart(): string {
const pos = this.entry.name.lastIndexOf(this.domain.name);
return this.entry.name.substr(0, pos).replace(/\.$/, '');
}
public fullName(): string {
if (this.inputName.value !== '') {
return this.inputName.value + '.' + this.domain.name;
} else {
return this.domain.name;
}
}
public async onSave() {
await this.records.updateRecord(this.entry.id, this.fullName(),
this.inputType.value, this.inputContent.value, this.inputPriority.value, this.inputTtl.value);
this.editMode = false;
this.recordUpdated.emit();
}
}

View file

@ -1,9 +1,9 @@
<div class="row">
<div class="col-12">
<p class="font-weight-bold">Update SOA data for {{ domainName }}</p>
<p class="font-weight-bold">Update SOA data for {{ domain.name }}</p>
</div>
</div>
<form [formGroup]="soaForm" (submit)="onSubmit()">
<form [formGroup]="soaForm" (submit)="onSoaSubmit()">
<div class="row">
<div class="col-12 col-md-4 col-lg-3">
<div class="form-group">
@ -67,4 +67,59 @@
<button type="submit" class="btn btn-primary float-right float-md-left" [disabled]="!soaForm.valid || soaForm.pristine">Save</button>
</div>
</div>
</form>
</form>
<div class="row justify-content-end">
<div class="col-12 col-md-6 mt-2 mt-md-0">
<app-pagesize class="float-md-right" [pagesizes]="gs.pageSizes" [currentPagesize]="gs.pageSize" (pagesizeChange)="onPagesizeChange($event)"></app-pagesize>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="table-responsive-lg">
<table class="table table-hover table-layout-fixed table-triple">
<thead>
<tr>
<th class="w-6 align-middle">
<span>ID</span>
<app-sort field="id" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
</th>
<th class="w-25 align-middle">
<div class="form-inline">
<span>Name</span>
<app-sort field="name" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
<input class="form-control form-control-sm no-shadow" type="text" placeholder="Search" [formControl]="queryNameInput">
</div>
</th>
<th class="w-15 align-middle">
<div class="form-inline">
<span>Type</span>
<app-sort field="type" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
<app-select class="w-60" [options]="gs.recordTypes" [formControl]="typeFilter" multiple></app-select>
</div>
</th>
<th class="align-middle">
<div class="form-inline">
<span>Content</span>
<app-sort field="content" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
<input class="form-control form-control-sm no-shadow" type="text" placeholder="Search" [formControl]="queryContentInput">
</div>
</th>
<th class="w-10 align-middle">
<span>Priority</span>
<app-sort field="priority" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
</th>
<th class="w-8 align-middle">
<span>TTL</span>
<app-sort field="ttl" [activeFields]="sortField" (sort)="onSortEvent($event)"></app-sort>
</th>
<th class="w-9 align-middle"></th>
</tr>
</thead>
<tbody>
<tr app-edit-auth-line *ngFor="let record of recordList" [entry]="record" [domain]="domain" (recordUpdated)="updateSerial()"></tr>
</tbody>
</table>
</div>
</div>
</div>
<app-paging [pagingInfo]="pagingInfo" [pageWidth]="3" (pageChange)="onPageChange($event)"></app-paging>

View file

@ -1,9 +1,16 @@
import { EditAuthLineComponent } from './edit-auth-line.component';
import { RecordApitype } from './../../apitypes/Record.apitype';
import { StateService } from './../../services/state.service';
import { RecordsOperation } from './../../operations/records.operations';
import { DomainApitype } from './../../apitypes/Domain.apitype';
import { SoaApitype } from './../../apitypes/Soa.apitype';
import { DomainsOperation } from './../../operations/domains.operations';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { PagingApitype } from '../../apitypes/Paging.apitype';
import { SortEventDatatype } from '../../datatypes/sort-event.datatype';
import 'rxjs/add/operator/filter';
@Component({
selector: 'app-edit-auth',
@ -15,11 +22,24 @@ export class EditAuthComponent implements OnInit {
public type = '';
public domainName = '';
public domain: DomainApitype = new DomainApitype({});
public domainId = 0;
constructor(private route: ActivatedRoute, private fb: FormBuilder, private domains: DomainsOperation) { }
public pagingInfo = new PagingApitype({});
public pageRequested = 1;
public recordList: RecordApitype[] = [];
public sortField = '';
public sortOrder = 'asc';
public queryNameInput: FormControl;
public queryContentInput: FormControl;
public typeFilter: FormControl;
constructor(private route: ActivatedRoute, private fb: FormBuilder, public gs: StateService,
private domains: DomainsOperation, private records: RecordsOperation) { }
ngOnInit() {
this.createForm();
@ -33,7 +53,7 @@ export class EditAuthComponent implements OnInit {
this.domainId = +params.get('domainId');
this.domains.getSingle(this.domainId).then((domain: DomainApitype) => {
this.domainName = domain.name;
this.domain = domain;
});
this.domains.getSoa(this.domainId).then((soa: SoaApitype) => {
@ -47,6 +67,12 @@ export class EditAuthComponent implements OnInit {
serial: soa.serial
});
});
this.queryNameInput.reset();
this.queryContentInput.reset();
// this triggers also a reload of the records, therefore this function is ommited here
this.typeFilter.reset();
}
private async updateSerial() {
@ -66,13 +92,62 @@ export class EditAuthComponent implements OnInit {
ttl: ['', [Validators.required, Validators.pattern(/^[0-9]+$/)]],
serial: ['']
});
this.queryNameInput = new FormControl('');
this.queryNameInput.valueChanges.filter((d) => d !== null).debounceTime(500).subscribe(() => this.loadRecords());
this.queryContentInput = new FormControl('');
this.queryContentInput.valueChanges.filter((d) => d !== null).debounceTime(500).subscribe(() => this.loadRecords());
this.typeFilter = new FormControl(null);
this.typeFilter.valueChanges.subscribe(() => this.loadRecords());
}
public async onSubmit() {
public async onSoaSubmit() {
const v = this.soaForm.value;
await this.domains.setSoa(this.domainId, v.primary, v.email, v.refresh, v.retry, v.expire, v.ttl);
this.soaForm.markAsPristine();
await this.updateSerial();
}
public async loadRecords() {
const sortStr = this.sortField !== '' ? this.sortField + '-' + this.sortOrder : null;
const queryName = this.queryNameInput.value !== '' ? this.queryNameInput.value : null;
const queryContent = this.queryContentInput.value !== '' ? this.queryContentInput.value : null;
const typeFilter = this.typeFilter.value;
const res = await this.records.getListForDomain(this.domainId, this.pageRequested,
this.gs.pageSize, queryName, typeFilter, queryContent, sortStr);
this.pagingInfo = res.paging;
this.recordList = res.results;
if (res.paging.total < this.pageRequested && res.paging.total > 1) {
this.pageRequested = Math.max(1, res.paging.total);
await this.loadRecords();
}
}
public async onPageChange(newPage: number) {
this.pageRequested = newPage;
await this.loadRecords();
}
public async onPagesizeChange(pagesize: number) {
this.gs.pageSize = pagesize;
this.pageRequested = 1;
await this.loadRecords();
}
public async onSortEvent(sortEvent: SortEventDatatype) {
if (sortEvent.order === 0) {
this.sortField = '';
this.sortOrder = 'asc';
} else {
this.sortField = sortEvent.field;
this.sortOrder = sortEvent.order === 1 ? 'asc' : 'desc';
}
await this.loadRecords();
}
}

View file

@ -1,11 +1,13 @@
<div class="dropdown">
<a class="btn btn-sm dropdown-toggle no-shadow" (click)="toggleOpen()" [class.disabled]="!enabled">
<a class="btn btn-sm dropdown-toggle-left no-shadow mw-100 text-truncate" (click)="toggleOpen()" [class.disabled]="!enabled">
{{ buttonText() }}
</a>
<div class="dropdown-menu" [class.show]="open">
<div class="dropdown-menu" [class.show]="open" style="overflow-y: auto;">
<span class="dropdown-item cursor-pointer" *ngIf="notNull === false" (click)="reset()">Clear</span>
<div class="dropdown-divider" *ngIf="notNull === false"></div>
<span *ngFor="let option of options" [class.active]="isActive(option)" class="dropdown-item cursor-pointer"
(click)="onClick(option)">{{ option }}</span>
<div class="scrolling">
<span *ngFor="let option of options" [class.active]="isActive(option)" class="dropdown-item cursor-pointer"
(click)="onClick(option)">{{ option }}</span>
</div>
</div>
</div>

View file

@ -0,0 +1,4 @@
.scrolling {
overflow-y: auto;
max-height: 20em;
}

View file

@ -54,6 +54,18 @@ export class StateService {
return this._pageSizes;
}
private _recordTypes = [
'A', 'A6', 'AAAA', 'AFSDB', 'ALIAS', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'DHCID',
'DLV', 'DNAME', 'DNSKEY', 'DS', 'EUI48', 'EUI64', 'HINFO',
'IPSECKEY', 'KEY', 'KX', 'LOC', 'MAILA', 'MAILB', 'MINFO', 'MR',
'MX', 'NAPTR', 'NS', 'NSEC', 'NSEC3', 'NSEC3PARAM', 'OPENPGPKEY',
'OPT', 'PTR', 'RKEY', 'RP', 'RRSIG', 'SIG', 'SPF',
'SRV', 'TKEY', 'SSHFP', 'TLSA', 'TSIG', 'TXT', 'WKS', 'MBOXFW', 'URL'
];
get recordTypes(): Array<string> {
return this._recordTypes;
}
constructor() {
this.loadLocalStorage();
}

View file

@ -3,7 +3,15 @@ $table-cell-padding: .60rem !default;
$sizes: (
0: 0%,
1: 1%,
2: 2%,
3: 3%,
4: 4%,
5: 5%,
6: 6%,
7: 7%,
8: 8%,
9: 9%,
10: 10%,
15: 15%,
20: 20%,
@ -22,7 +30,11 @@ $sizes: (
85: 85%,
90: 90%,
95: 95%,
100: 100%
100: 100%,
150: 150%,
200: 200%,
250: 250%,
300: 300%
);
@import '~bootstrap/scss/bootstrap.scss';
@ -31,6 +43,12 @@ $sizes: (
@extend .dropdown-item:hover;
}
@each $prop, $abbrev in (max-width: w, max-height: h) {
@each $size, $length in $sizes {
.m#{$abbrev}-#{$size} { #{$prop}: $length !important; }
}
}
/* Add font awesome */
$fa-font-path: "~font-awesome/fonts";
@import '~font-awesome/scss/font-awesome';
@ -68,3 +86,24 @@ $fa-font-path: "~font-awesome/fonts";
box-shadow: none!important;
border-color: #CCC!important;
}
/* Table with fixed layout */
.table-layout-fixed {
table-layout: fixed;
}
/* Dropdown with toggle carret left */
.dropdown-toggle-left::before {
@extend .dropdown-toggle::after;
margin-left: unset;
margin-right: 0.255em;
}
/* Make table wider for small screens */
@include media-breakpoint-down(sm) {
.table-triple {
min-width: 100%;
max-width: 300%;
width: auto;
}
}