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 { PermissionEditLineComponent } from '@components/permissions-management/permission-edit-line/permission-edit-line.component';
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 { MethodButtonComponent } from './components/permissions-management/method-button/method-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,
MethodButtonComponent,
ActionButtonComponent,
DeletePermissionDialogComponent
DeletePermissionDialogComponent,
PathAutoCompleteComponent,
FilterCompletePipe
],
imports: [
BrowserModule,
@ -537,16 +542,17 @@ import {MatFormFieldModule} from "@angular/material/form-field";
NgxElectronModule,
FileUploadModule,
MatSidenavModule,
MatFormFieldModule,
ResizableModule,
DragAndDropModule,
DragDropModule,
NgxChildProcessModule,
MatFormFieldModule,
MATERIAL_IMPORTS,
NgCircleProgressModule.forRoot(),
OverlayModule,
MatSlideToggleModule,
MatCheckboxModule,
MatAutocompleteModule,
],
providers: [
SettingsService,

View File

@ -1,19 +1,48 @@
<mat-form-field appearance="fill">
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="selectedType" (ngModelChange)="changeType($event)">
<mat-option *ngFor="let type of objectTypes" [value]="type">{{type}}</mat-option>
</mat-select>
<div class="box-border">
<div *ngIf="edit; else add">
<div class="edit-mode">
<div class="information-box">
<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 appearance="fill">
<mat-select>
<mat-option *ngFor="let elt of elements" [value]="elt">{{elt.name ? elt.name : elt.filename}}</mat-option>
</mat-select>
</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()">
</div>
</div>
</div>
<div class="button-box">
<button mat-button (click)="reset()">
<mat-icon>cancel</mat-icon>
</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
*/
import {Component, Input, OnInit, Output} from '@angular/core';
import {ProjectService} from "@services/project.service";
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Server} from "@models/server";
import {ComputeService} from "@services/compute.service";
import EventEmitter from "events";
import {ApiInformationService} from "@services/api-information.service";
import {Methods, Permission, PermissionActions} from "@models/api/permission";
import {PermissionsService} from "@services/permissions.service";
import {ToasterService} from "@services/toaster.service";
@Component({
selector: 'app-add-permission-line',
@ -23,51 +24,56 @@ import EventEmitter from "events";
})
export class AddPermissionLineComponent implements OnInit {
objectTypes = ['projects', 'images', 'templates', 'computes']
elements = [];
selectedType = 'projects';
@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 {
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.addPermissionEvent.emit('save');
this.permission.methods = Array.from(set);
}
onCancel() {
this.addPermissionEvent.emit('cancel');
reset() {
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="default-header">
<div class="row">
<button class="col" mat-raised-button color="primary" (click)="addPermission()" class="add-button" *ngIf="!newPermissionEdit" >
Add Permission
</button>
<div class="add">
<app-add-permission-line
[server]="server"
(addPermissionEvent)="refresh()"></app-add-permission-line>
</div>
</div>
<div class="permission-content default-content">
<!--<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">
<app-permission-add-edit-line
[permission]="permission"

View File

@ -18,3 +18,13 @@
top: 0;
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;
action: PermissionActions;
description: string;
created_at: string;
updated_at: string;
permission_id: string;
created_at?: string;
updated_at?: string;
permission_id?: string;
}

View File

@ -13,8 +13,10 @@
import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable, ReplaySubject} from "rxjs";
import {map} from "rxjs/operators";
import {map, switchMap} from "rxjs/operators";
import {Methods} from "@models/api/permission";
import {HttpServer} from "@services/http-server.service";
import {Server} from "@models/server";
export interface IPathDict {
methods: ('POST' | 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'PATH')[];
@ -23,37 +25,93 @@ export interface IPathDict {
subPaths: string[];
}
export interface IApiObject {
name: string;
path: string;
}
export interface IQueryObject {
id: string;
text: string[];
}
export interface IFormatedList {
id: string;
name?: string;
}
@Injectable({
providedIn: 'root'
})
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.data.subscribe((data) => {
localStorage.setItem('api-definition', JSON.stringify(data));
});
this.objs.subscribe((data) => {
localStorage.setItem('api-definition-objs', JSON.stringify(data));
});
this.httpClient
.get(`https://apiv3.gns3.net/openapi.json`)
.subscribe((openapi: any) => {
const data = this.apiModelAdapter(openapi);
const objs = this.apiObjectModelAdapter(openapi);
const data = this.apiPathModelAdapter(openapi);
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);
return keys
.map(path => {
const subPaths = path.split('/').filter(elem => !(elem === '' || elem === 'v3'));
return {originalPath: path, path: subPaths.join('/'), subPaths};
})
.filter(d => this.allowed.includes(d.subPaths[0]))
.map(path => {
//FIXME
// @ts-ignore
@ -69,7 +127,6 @@ export class ApiInformationService {
.pipe(
map((data: IPathDict[]) => {
const availableMethods = new Set<string>();
data.forEach((p: IPathDict) => {
p.methods.forEach(method => availableMethods.add(method));
});
@ -83,24 +140,24 @@ export class ApiInformationService {
.asObservable()
.pipe(
map((data) => {
const splinted = path
.split('/')
.filter(elem => !(elem === '' || elem === 'v3'));
const splinted = path.split('/').filter(elem => !(elem === '' || elem === 'v3'));
let remains = data;
splinted.forEach((value, index) => {
if (value === '*') {
return remains;
return;
}
remains = remains.filter((val => {
if (!val.subPaths[index]) {
return false;
}
if (val.subPaths[index].includes('{')) {
return true;
}
return val.subPaths[index] === value;
}));
});
let matchUrl = remains.filter(val => val.subPaths[index]?.includes(value));
if (matchUrl.length === 0) {
matchUrl = remains.filter(val => val.subPaths[index]?.match(this.bracketIdRegex));
}
remains = matchUrl;
});
return remains;
})
);
@ -111,5 +168,63 @@ export class ApiInformationService {
if (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]
};
});
}));
}
}