Permission management, create add permission component and apiInformation service, which parse swagger api information schema

This commit is contained in:
Sylvain MATHIEU 2022-01-13 09:41:02 +01:00
parent 2664911455
commit 65f1d45dc5
14 changed files with 772 additions and 376 deletions

View File

@ -300,6 +300,9 @@ import { PermissionEditorValidateDialogComponent } from './components/role-manag
import { PermissionsManagementComponent } from './components/permissions-management/permissions-management.component'; import { PermissionsManagementComponent } from './components/permissions-management/permissions-management.component';
import { PermissionEditLineComponent } from '@components/permissions-management/permission-edit-line/permission-edit-line.component'; import { PermissionEditLineComponent } from '@components/permissions-management/permission-edit-line/permission-edit-line.component';
import {MatSlideToggleModule} from '@angular/material/slide-toggle'; import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {PathAutoCompleteComponent} from './components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component';
import {FilterCompletePipe} from './components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe';
import { AddPermissionLineComponent } from './components/permissions-management/add-permission-line/add-permission-line.component'; import { AddPermissionLineComponent } from './components/permissions-management/add-permission-line/add-permission-line.component';
import { MethodButtonComponent } from './components/permissions-management/method-button/method-button.component'; import { MethodButtonComponent } from './components/permissions-management/method-button/method-button.component';
import { ActionButtonComponent } from './components/permissions-management/action-button/action-button.component'; import { ActionButtonComponent } from './components/permissions-management/action-button/action-button.component';
@ -523,7 +526,9 @@ import {MatFormFieldModule} from "@angular/material/form-field";
AddPermissionLineComponent, AddPermissionLineComponent,
MethodButtonComponent, MethodButtonComponent,
ActionButtonComponent, ActionButtonComponent,
DeletePermissionDialogComponent DeletePermissionDialogComponent,
PathAutoCompleteComponent,
FilterCompletePipe
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -537,16 +542,17 @@ import {MatFormFieldModule} from "@angular/material/form-field";
NgxElectronModule, NgxElectronModule,
FileUploadModule, FileUploadModule,
MatSidenavModule, MatSidenavModule,
MatFormFieldModule,
ResizableModule, ResizableModule,
DragAndDropModule, DragAndDropModule,
DragDropModule, DragDropModule,
NgxChildProcessModule, NgxChildProcessModule,
MatFormFieldModule,
MATERIAL_IMPORTS, MATERIAL_IMPORTS,
NgCircleProgressModule.forRoot(), NgCircleProgressModule.forRoot(),
OverlayModule, OverlayModule,
MatSlideToggleModule, MatSlideToggleModule,
MatCheckboxModule, MatCheckboxModule,
MatAutocompleteModule,
], ],
providers: [ providers: [
SettingsService, SettingsService,

View File

@ -1,19 +1,48 @@
<mat-form-field appearance="fill"> <div class="box-border">
<mat-label>Type</mat-label> <div *ngIf="edit; else add">
<mat-select [(ngModel)]="selectedType" (ngModelChange)="changeType($event)"> <div class="edit-mode">
<mat-option *ngFor="let type of objectTypes" [value]="type">{{type}}</mat-option> <div class="information-box">
</mat-select> <div>
<app-path-auto-complete
[server]="server"
(update)="permission.path = $event"></app-path-auto-complete>
</div>
<div class="methods">
<app-action-button
[disabled]="false"
[action]="permission.action"></app-action-button>
<div *ngFor="let method of apiInformation.getMethods(permission.path) | async">
<app-method-button
[name]="method"
[disabled]="false"
(update)="updateMethod($event)"></app-method-button>
</div>
<div class="description">
<mat-form-field>
<input
[(ngModel)]="permission.description"
matInput
type="text"
placeholder="Description"/>
</mat-form-field> </mat-form-field>
</div>
<mat-form-field appearance="fill"> </div>
<mat-select> </div>
<mat-option *ngFor="let elt of elements" [value]="elt">{{elt.name ? elt.name : elt.filename}}</mat-option> <div class="button-box">
</mat-select> <button mat-button (click)="reset()">
</mat-form-field>
<button mat-button matTooltip="Save Changes" (click)="onSave()" color="primary">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-button matTooltip="Cancel Changes" color="warn" (click)="onCancel()">
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
<button mat-button (click)="save()">
<mat-icon>done</mat-icon>
</button>
</div>
</div>
</div>
<ng-template #add>
<div class="not-edit">
<button mat-button (click)="edit = true">
<mat-icon>add</mat-icon>
</button>
</div>
</ng-template>
</div>

View File

@ -0,0 +1,49 @@
.box-border {
width: 100%;
margin-top: 20px;
border-bottom: 1px solid;
}
.edit-mode {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.information-box {
margin-left: 10px;
width: 100%;
}
.information-box > div {
margin-bottom: 10px;
}
.methods {
display: flex;
flex-direction: row;
align-items: center;
}
.button-box {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.description {
width: 100%;
margin-left: 10px;
margin-right: 10px;
}
.description > mat-form-field {
width: 100%;
}
.not-edit {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}

View File

@ -10,11 +10,12 @@
* *
* Author: Sylvain MATHIEU, Elise LEBEAU * Author: Sylvain MATHIEU, Elise LEBEAU
*/ */
import {Component, Input, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {ProjectService} from "@services/project.service";
import {Server} from "@models/server"; import {Server} from "@models/server";
import {ComputeService} from "@services/compute.service"; import {ApiInformationService} from "@services/api-information.service";
import EventEmitter from "events"; import {Methods, Permission, PermissionActions} from "@models/api/permission";
import {PermissionsService} from "@services/permissions.service";
import {ToasterService} from "@services/toaster.service";
@Component({ @Component({
selector: 'app-add-permission-line', selector: 'app-add-permission-line',
@ -23,51 +24,56 @@ import EventEmitter from "events";
}) })
export class AddPermissionLineComponent implements OnInit { export class AddPermissionLineComponent implements OnInit {
objectTypes = ['projects', 'images', 'templates', 'computes']
elements = [];
selectedType = 'projects';
@Input() server: Server; @Input() server: Server;
@Output() addPermissionEvent = new EventEmitter<void>();
permission: Permission = {
action: PermissionActions.ALLOW,
description: "",
methods: [],
path: "/",
};
edit = false;
@Output() addPermissionEvent = new EventEmitter(); constructor(public apiInformation: ApiInformationService,
private permissionService: PermissionsService,
private toasterService: ToasterService) {
constructor(private projectService: ProjectService, }
private computeService: ComputeService) { }
ngOnInit(): void { ngOnInit(): void {
this.projectService.list(this.server)
.subscribe(elts => {
this.elements = elts;
})
}
changeType(value) {
console.log(value);
this.selectedType = value;
switch (this.selectedType) {
case 'projects':
this.projectService.list(this.server)
.subscribe(elts => {
this.elements = elts;
})
break;
case 'computes':
this.computeService.getComputes(this.server)
.subscribe(elts => {
this.elements = elts;
})
break;
default:
console.log("TODO");
this.elements = [];
} }
updateMethod(data: { name: Methods; enable: boolean }) {
const set = new Set(this.permission.methods);
if (data.enable) {
set.add(data.name);
} else {
set.delete(data.name);
} }
onSave() { this.permission.methods = Array.from(set);
this.addPermissionEvent.emit('save');
} }
onCancel() { reset() {
this.addPermissionEvent.emit('cancel'); this.permission = {
action: PermissionActions.ALLOW,
description: "",
methods: [],
path: "/",
};
this.edit = false;
}
save() {
this.permissionService.add(this.server, this.permission)
.subscribe(() => {
this.toasterService.success(`permission was created`);
this.reset();
}, (error) => {
this.toasterService.error(error);
});
} }
} }

View File

@ -0,0 +1,8 @@
import { FilterCompletePipe } from './filter-complete.pipe';
describe('FilterCompletePipe', () => {
it('create an instance', () => {
const pipe = new FilterCompletePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from '@angular/core';
import {IFormatedList} from "@services/api-information.service";
@Pipe({
name: 'filterComplete'
})
export class FilterCompletePipe implements PipeTransform {
transform(value: IFormatedList[], searchText: string): IFormatedList[] {
if (!searchText || searchText === '') { return value; }
return value.filter((v) => {
return v.name.includes(searchText) || v.id.includes(searchText);
});
}
}

View File

@ -0,0 +1,37 @@
<div class="path">
<div>Path: /</div>
<div *ngFor="let p of path">{{p}}/</div>
<div class="path-edit-line">
<div>
<div *ngIf="mode === 'SELECT'">
<mat-select (valueChange)="valueChanged($event)" class="edit-area">
<mat-option *ngFor="let value of values" value="{{value}}">{{value}}</mat-option>
</mat-select>
</div>
<div *ngIf="mode === 'COMPLETE'">
<input matInput
autofocus
class="complete edit-area"
aria-label="find"
[(ngModel)]="completeField"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option [value]="'*'">*</mat-option>
<mat-option *ngFor="let data of completeData | filterComplete: completeField"
[value]="data.id">
<span>{{data.name}}</span> |
<small>{{data.id}}</small>
</mat-option>
</mat-autocomplete>
</div>
</div>
<div class="command-button">
<mat-icon (click)="removePrevious()" *ngIf="path.length > 0">cancel</mat-icon>
<mat-icon (click)="getNext()" *ngIf="!this.mode">add_circle_outline</mat-icon>
<mat-icon
matTooltip="validate data"
(click)="validComplete()"
*ngIf="this.mode === 'COMPLETE'">check_circle</mat-icon>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
.path {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
mat-select {
width: 150px;
}
.edit-area {
border: 1px solid;
}
.command-button {
margin-left: 5px;
}
.path-edit-line {
display: flex;
flex-direction: row;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PathAutoCompleteComponent } from './path-auto-complete.component';
describe('PathAutoCompleteComponent', () => {
let component: PathAutoCompleteComponent;
let fixture: ComponentFixture<PathAutoCompleteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PathAutoCompleteComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PathAutoCompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,74 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {ApiInformationService, IFormatedList} from "@services/api-information.service";
import {Server} from "@models/server";
@Component({
selector: 'app-path-auto-complete',
templateUrl: './path-auto-complete.component.html',
styleUrls: ['./path-auto-complete.component.scss']
})
export class PathAutoCompleteComponent implements OnInit {
@Output() update = new EventEmitter<string>();
@Input() server: Server;
path: string[] = [];
values: string[] = [];
private completeData: IFormatedList[];
public mode: 'SELECT' | 'COMPLETE' | undefined;
completeField: string;
constructor(private apiInformationService: ApiInformationService) {
}
updatePath(value: string) {
this.path.push(value);
this.update.emit(`/${this.path.join('/')}`);
}
popPath() {
this.path.pop();
this.update.emit(`/${this.path.join('/')}`);
}
ngOnInit(): void {
}
getNext() {
this.apiInformationService
.getPathNextElement(this.path)
.subscribe((next: string[]) => {
this.values = next;
this.mode = 'SELECT';
});
}
removePrevious() {
if (this.mode) {
return this.mode = undefined;
}
if (this.path.length > 0) {
return this.popPath();
}
}
valueChanged(value: string) {
if (value.match(this.apiInformationService.bracketIdRegex)) {
this.apiInformationService.getListByObjectId(this.server, value)
.subscribe((data) => {
this.mode = 'COMPLETE';
this.completeData = data;
});
} else {
this.updatePath(value);
this.mode = undefined;
}
}
validComplete() {
this.updatePath(this.completeField);
this.mode = undefined;
}
}

View File

@ -1,15 +1,13 @@
<div class="content" *ngIf="isReady; else loading"> <div class="content" *ngIf="isReady; else loading">
<div class="default-header"> <div class="add">
<div class="row"> <app-add-permission-line
<button class="col" mat-raised-button color="primary" (click)="addPermission()" class="add-button" *ngIf="!newPermissionEdit" > [server]="server"
Add Permission (addPermissionEvent)="refresh()"></app-add-permission-line>
</button>
</div> </div>
</div>
<div class="permission-content default-content"> <div class="permission-content default-content">
<!--<ng-template #dynamic></ng-template>--> <!--<ng-template #dynamic></ng-template>-->
<app-add-permission-line [server]="server" (addPermissionEvent)="updateList($event)" *ngIf="newPermissionEdit"></app-add-permission-line> <app-add-permission-line [server]="server" (addPermissionEvent)="updateList($event)"
*ngIf="newPermissionEdit"></app-add-permission-line>
<div *ngFor="let permission of permissions"> <div *ngFor="let permission of permissions">
<app-permission-add-edit-line <app-permission-add-edit-line
[permission]="permission" [permission]="permission"

View File

@ -18,3 +18,13 @@
top: 0; top: 0;
width: 175px; width: 175px;
} }
.add {
/* display: flex;
flex-direction: row;
justify-content: flex-end;
padding-right: 20px;
padding-bottom: 20px;
border-bottom: 1px solid;
align-items: center;*/
}

View File

@ -29,7 +29,7 @@ export interface Permission {
path: string; path: string;
action: PermissionActions; action: PermissionActions;
description: string; description: string;
created_at: string; created_at?: string;
updated_at: string; updated_at?: string;
permission_id: string; permission_id?: string;
} }

View File

@ -13,8 +13,10 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {Observable, ReplaySubject} from "rxjs"; import {Observable, ReplaySubject} from "rxjs";
import {map} from "rxjs/operators"; import {map, switchMap} from "rxjs/operators";
import {Methods} from "@models/api/permission"; import {Methods} from "@models/api/permission";
import {HttpServer} from "@services/http-server.service";
import {Server} from "@models/server";
export interface IPathDict { export interface IPathDict {
methods: ('POST' | 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'PATH')[]; methods: ('POST' | 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'PATH')[];
@ -23,37 +25,93 @@ export interface IPathDict {
subPaths: string[]; subPaths: string[];
} }
export interface IApiObject {
name: string;
path: string;
}
export interface IQueryObject {
id: string;
text: string[];
}
export interface IFormatedList {
id: string;
name?: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiInformationService { export class ApiInformationService {
private data: ReplaySubject<IPathDict[]> = new ReplaySubject<IPathDict[]>(1);
constructor(private httpClient: HttpClient) { private allowed = ['projects', 'images', 'templates', 'computes', 'symbols', 'notifications'];
private data: ReplaySubject<IPathDict[]> = new ReplaySubject<IPathDict[]>(1);
private objs: ReplaySubject<IApiObject[]> = new ReplaySubject<IApiObject[]>(1);
public readonly bracketIdRegex = new RegExp("\{(.*?)\}");
public readonly finalBracketIdRegex = new RegExp("\{(.*?)\}$");
constructor(private httpClient: HttpClient,
private httpServer: HttpServer) {
this.loadLocalInformation(); this.loadLocalInformation();
this.data.subscribe((data) => { this.data.subscribe((data) => {
localStorage.setItem('api-definition', JSON.stringify(data)); localStorage.setItem('api-definition', JSON.stringify(data));
}); });
this.objs.subscribe((data) => {
localStorage.setItem('api-definition-objs', JSON.stringify(data));
});
this.httpClient this.httpClient
.get(`https://apiv3.gns3.net/openapi.json`) .get(`https://apiv3.gns3.net/openapi.json`)
.subscribe((openapi: any) => { .subscribe((openapi: any) => {
const data = this.apiModelAdapter(openapi); const objs = this.apiObjectModelAdapter(openapi);
const data = this.apiPathModelAdapter(openapi);
this.data.next(data); this.data.next(data);
this.objs.next(objs);
}); });
} }
private apiModelAdapter(openapi: any): IPathDict[] { private apiObjectModelAdapter(openapi: any): IApiObject[] {
function haveGetMethod(path: string): boolean {
const obj = openapi.paths[path];
if (obj) {
const methods = Object.keys(obj);
return methods.includes("get");
} else {
return false;
}
}
function extractId(originalPath: string): IApiObject {
const d = originalPath.split('/');
const name = d.pop();
const path = d.join('/');
return {name, path};
}
const keys = Object.keys(openapi.paths);
return keys
.filter((path: string) => path.match(this.finalBracketIdRegex))
.filter(haveGetMethod)
.map(extractId)
.filter((object) => haveGetMethod(object.path));
}
private apiPathModelAdapter(openapi: any): IPathDict[] {
const keys = Object.keys(openapi.paths); const keys = Object.keys(openapi.paths);
return keys return keys
.map(path => { .map(path => {
const subPaths = path.split('/').filter(elem => !(elem === '' || elem === 'v3')); const subPaths = path.split('/').filter(elem => !(elem === '' || elem === 'v3'));
return {originalPath: path, path: subPaths.join('/'), subPaths}; return {originalPath: path, path: subPaths.join('/'), subPaths};
}) })
.filter(d => this.allowed.includes(d.subPaths[0]))
.map(path => { .map(path => {
//FIXME //FIXME
// @ts-ignore // @ts-ignore
@ -69,7 +127,6 @@ export class ApiInformationService {
.pipe( .pipe(
map((data: IPathDict[]) => { map((data: IPathDict[]) => {
const availableMethods = new Set<string>(); const availableMethods = new Set<string>();
data.forEach((p: IPathDict) => { data.forEach((p: IPathDict) => {
p.methods.forEach(method => availableMethods.add(method)); p.methods.forEach(method => availableMethods.add(method));
}); });
@ -83,24 +140,24 @@ export class ApiInformationService {
.asObservable() .asObservable()
.pipe( .pipe(
map((data) => { map((data) => {
const splinted = path
.split('/')
.filter(elem => !(elem === '' || elem === 'v3'));
const splinted = path.split('/').filter(elem => !(elem === '' || elem === 'v3'));
let remains = data; let remains = data;
splinted.forEach((value, index) => { splinted.forEach((value, index) => {
if (value === '*') { if (value === '*') {
return remains; return;
} }
remains = remains.filter((val => { let matchUrl = remains.filter(val => val.subPaths[index]?.includes(value));
if (!val.subPaths[index]) {
return false;
}
if (val.subPaths[index].includes('{')) {
return true;
}
return val.subPaths[index] === value;
}));
});
if (matchUrl.length === 0) {
matchUrl = remains.filter(val => val.subPaths[index]?.match(this.bracketIdRegex));
}
remains = matchUrl;
});
return remains; return remains;
}) })
); );
@ -111,5 +168,63 @@ export class ApiInformationService {
if (data) { if (data) {
this.data.next(data); this.data.next(data);
} }
const obj = JSON.parse(localStorage.getItem('api-definition-objs'));
if (obj) {
this.objs.next(obj);
} }
} }
getPathNextElement(path: string[]): Observable<string[]> {
return this.getPath(path.join('/'))
.pipe(map((paths: IPathDict[]) => {
const set = new Set<string>();
paths.forEach((p) => {
if (p.subPaths[path.length]) {
set.add(p.subPaths[path.length]);
}
});
return Array.from(set);
}));
}
getListByObjectId(server: Server, value: string) {
function findElement(data: IApiObject[]): IApiObject {
const elem = data.find(d => d.name === value);
if (!elem) {
throw new Error('entry not found');
}
return elem;
}
return this.objs.pipe(
map(findElement),
switchMap(elem => {
const url = `${server.protocol}//${server.host}:${server.port}${elem.path}`;
return this.httpClient.get<any[]>(url, {headers: {Authorization: `Bearer ${server.authToken}`}});
}
),
map(response => {
if (response.length === 0) {
return [];
}
const keys = Object.keys(response[0]);
const idKey = keys.find(k => k.match(/_id$|filename/));
const nameKey = keys.find(k => k.match(/name/));
return response.map(o => {
return {
id: o[idKey],
name: o[nameKey]
};
});
}));
}
}