feat(console): integrate frontend (#95)

* feat: console frontend

* chore(dependabot): cycle and npm

* chore: rename citadel to zitadel, remove generated files

* chore: delete go files

* chore(frontend): ci steps

* chore: remove docker and envoy files

* chore: remove docker file

* chore: working dir

* chore: run proto build

* add console start

* chore: restructure folders

* chore: remove gui build

* statikFs

* generate proto for console

* add statik import

* import

* chore: try statik

* chore: path

* chore: path

* chore: script in root

* chore: order build steps

* chore: go get

* chore: folder traversal

* chore: non empty test file

* chore: gitignore

* chore: gitignore

* chore: statik path

* chore: switch to failing FE build

* fix: build

* fix: project-grant-test

* fix: rm test

* add statik.go

* go mod tidy

* chore: place test, seperate test from build

* chore: lint all the world

* chore: ci the world instead

* chore: tune docker

* chore: undo container test

* chore: fix run

* chore: docker build

* chore: test docker build

* chore: go build flags

* finaly

* fix caos_local

* go mod

Co-authored-by: Livio Amstutz <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Florian Forster
2020-05-13 14:41:43 +02:00
committed by GitHub
co-authored by Livio Amstutz Max Peintner
parent 9e32740eb8
commit 92a294f5c8
375 changed files with 97826 additions and 52 deletions
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { OrgCreateComponent } from './org-create.component';
const routes: Routes = [
{
path: '',
component: OrgCreateComponent,
data: { animation: 'DetailPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrgCreateRoutingModule { }
@@ -0,0 +1,145 @@
<div class="max-width-container container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'ORG.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
</div>
<ng-container *ngIf="currentCreateStep == 1">
<h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
<form [formGroup]="orgForm">
<div class="content">
<mat-form-field class="formfield">
<mat-label>{{ 'ORG_DETAIL.DETAIL.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'ORG_DETAIL.DETAIL.DOMAIN' | translate }}</mat-label>
<input matInput formControlName="domain" />
</mat-form-field>
</div>
</form>
<div *ngIf="name?.touched" @openClose>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate }}</p>
<p>{{domain?.value}}/.well-known/caos-developer-domain-association.txt</p>
<div class="btn-container">
<button color="primary" type="submit"
mat-stroked-button>{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" type="submit" mat-raised-button>{{ 'ACTIONS.VERIFY' | translate }}</button>
</div>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_SKIP' | translate }}</p>
</div>
</ng-container>
<ng-container *ngIf="currentCreateStep == createSteps">
<h1>{{'ORG.PAGES.ORGDETAILUSER_TITLE' | translate}}</h1>
<div class="user">
<form [formGroup]="userForm" (ngSubmit)="finish()" class="form">
<div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.EMAIL' | translate }}</mat-label>
<input matInput formControlName="email" required />
<mat-error *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</mat-label>
<input matInput formControlName="firstName" required />
<mat-error *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</mat-label>
<input matInput formControlName="lastName" required />
<mat-error *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</mat-label>
<input matInput formControlName="nickName" />
<mat-error *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.DISPLAYNAME' | translate }}</mat-label>
<input matInput formControlName="displayName" />
<mat-error *ngIf="displayName?.invalid && displayName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.GENDER' | translate }}</mat-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
{{ 'GENDERS.'+gender | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="gender?.invalid && gender?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</mat-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<mat-error *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-select>
</mat-form-field>
<p class="section">{{ 'USER.CREATE.PASSWORDSECTION' | translate }}</p>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PASSWORD.NEW' | translate }}</mat-label>
<input autocomplete="off" name="firstpassword" matInput formControlName="password"
type="password" />
<mat-error *ngIf="password?.errors?.required">{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
<mat-error *ngIf="password?.errors?.pattern">{{ 'USER.VALIDATION.INVALIDPATTERN' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</mat-label>
<input autocomplete="off" name="firstpasswordRepeat" matInput formControlName="confirmPassword"
type="password" />
<mat-error *ngIf="confirmPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="btn-container">
<button color="accent" class="continue-button" [disabled]="orgForm.invalid || userForm.invalid"
type="submit" mat-raised-button>{{ 'ACTIONS.FINISH' | translate }}</button>
</div>
</form>
</div>
</ng-container>
<ng-container *ngIf="currentCreateStep == 1">
<button (click)="next()" color="primary" mat-raised-button class="continue-button" cdkFocusInitial
type="submit">
{{'CONTINUE' | translate}}
</button>
</ng-container>
</div>
@@ -0,0 +1,142 @@
h1 {
font-weight: 500;
}
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
.desc {
color: #81868a;
font-size: .9rem;
margin: 1rem 0;
}
}
.margin-right {
margin-right: 0.5rem;
}
.user {
flex: 1;
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
a {
display: block;
}
h1 {
font-size: 2rem;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #81868a;
}
}
.form {
padding-top: 1rem;
.btn-container {
display: flex;
justify-content: flex-end;
button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
.section {
padding: .5rem;
flex-basis: 100%;
color: #81868a;
font-size: .9rem;
}
&.nowrap{
flex-wrap: nowrap;
}
mat-form-field {
flex: 1 0 33%;
margin: 0 .5rem;
}
}
}
}
.column {
display: flex;
flex-direction: column;
.formfield {
width: 400px;
input {
font-size: 1.5rem;
}
&.autocomplete {
margin-top: 1rem;
}
}
}
.btn-container {
display: flex;
margin: 0 -.5rem;
button {
border-radius: .5rem;
margin: 0 .5rem;
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgCreateComponent } from './org-create.component';
describe('OrgCreateComponent', () => {
let component: OrgCreateComponent;
let fixture: ComponentFixture<OrgCreateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OrgCreateComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,156 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { CreateOrgRequest, Gender, OrgSetUpResponse, RegisterUserRequest } from 'src/app/proto/generated/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
function passwordConfirmValidator(c: AbstractControl): any {
if (!c.parent || !c) {
return;
}
const pwd = c.parent.get('password');
const cpwd = c.parent.get('confirmPassword');
if (!pwd || !cpwd) {
return;
}
if (pwd.value !== cpwd.value) {
return { invalid: true };
}
}
@Component({
selector: 'app-org-create',
templateUrl: './org-create.component.html',
styleUrls: ['./org-create.component.scss'],
animations: [
trigger('openClose', [
transition(':enter', [
style({ height: '0', opacity: 0 }),
animate('150ms ease-in-out', style({ height: '*', opacity: 1 })),
]),
transition(':leave', [
animate('150ms ease-in-out', style({ height: '0', opacity: 0 })),
]),
]),
],
})
export class OrgCreateComponent {
public orgForm!: FormGroup;
public userForm!: FormGroup;
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
public languages: string[] = ['de', 'en'];
constructor(
private router: Router,
private toast: ToastService,
private adminService: AdminService,
private _location: Location,
private fb: FormBuilder,
) {
this.orgForm = this.fb.group({
name: ['', [Validators.required]],
domain: ['', [Validators.required]],
});
this.userForm = this.fb.group({
firstName: ['', [Validators.required]],
lastName: ['', [Validators.required]],
displayName: [''],
email: ['', [Validators.required]],
gender: [''],
nickName: [''],
preferredLanguage: [''],
password: ['', [Validators.required]],
confirmPassword: ['', [Validators.required, passwordConfirmValidator]],
});
}
public createSteps: number = 2;
public currentCreateStep: number = 1;
public finish(): void {
const createOrgRequest: CreateOrgRequest = new CreateOrgRequest();
createOrgRequest.setName(this.name?.value);
createOrgRequest.setDomain(this.domain?.value);
const registerUserRequest: RegisterUserRequest = new RegisterUserRequest();
registerUserRequest.setEmail(this.email?.value);
registerUserRequest.setFirstName(this.firstName?.value);
registerUserRequest.setLastName(this.lastName?.value);
registerUserRequest.setNickName(this.nickName?.value);
registerUserRequest.setDisplayName(this.displayName?.value);
registerUserRequest.setGender(this.gender?.value);
registerUserRequest.setPassword(this.password?.value);
registerUserRequest.setPreferredLanguage(this.preferredLanguage?.value);
this.adminService
.SetUpOrg(createOrgRequest, registerUserRequest)
.then((data: OrgSetUpResponse) => {
this.router.navigate(['orgs', data.toObject().org?.id]);
})
.catch(data => {
this.toast.showError(data.message);
});
}
public next(): void {
this.currentCreateStep++;
}
public previous(): void {
this.currentCreateStep--;
}
public get name(): AbstractControl | null {
return this.orgForm.get('name');
}
public get domain(): AbstractControl | null {
return this.orgForm.get('domain');
}
public get firstName(): AbstractControl | null {
return this.userForm.get('firstName');
}
public get lastName(): AbstractControl | null {
return this.userForm.get('lastName');
}
public get displayName(): AbstractControl | null {
return this.userForm.get('displayName');
}
public get email(): AbstractControl | null {
return this.userForm.get('email');
}
public get nickName(): AbstractControl | null {
return this.userForm.get('nickName');
}
public get preferredLanguage(): AbstractControl | null {
return this.userForm.get('preferredLanguage');
}
public get gender(): AbstractControl | null {
return this.userForm.get('gender');
}
public get password(): AbstractControl | null {
return this.userForm.get('password');
}
public get confirmPassword(): AbstractControl | null {
return this.userForm.get('confirmPassword');
}
public close(): void {
this._location.back();
}
}
@@ -0,0 +1,37 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { HttpLoaderFactory } from 'src/app/app.module';
import { OrgCreateRoutingModule } from './org-create-routing.module';
import { OrgCreateComponent } from './org-create.component';
@NgModule({
declarations: [OrgCreateComponent],
imports: [
OrgCreateRoutingModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatIconModule,
MatSelectModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
],
})
export class OrgCreateModule { }
@@ -0,0 +1,37 @@
<app-meta-layout>
<div class="max-width-container">
<!-- {{ 'ORG_DETAIL.TITLE' | translate }} -->
<h1>{{org?.name}}</h1>
<p class="sub">{{'ORG_DETAIL.DESCRIPTION' | translate}}
</p>
<ng-template appHasRole [appHasRole]="['org.member.read']">
<h3>{{ 'ORG_DETAIL.MEMBER.TITLE' | translate }}</h3>
<app-org-members [orgId]="orgId"></app-org-members>
</ng-template>
</div>
<ng-template appHasRole [appHasRole]="['policy.read']">
<div class="enlarged-container">
<app-policy-grid></app-policy-grid>
</div>
</ng-template>
<metainfo class="side">
<div class="details">
<div class="row">
<span class="first">Domain:</span>
<span class="second" *ngIf="org?.domain">{{org.domain}}</span>
</div>
<div class="row">
<span class="first">State:</span>
<span *ngIf="org && org.state !== undefined"
class="second">{{'ORG.STATE.'+org.state | translate}}</span>
</div>
</div>
<p class="side-section">{{ 'CHANGES.ORG.TITLE' | translate }}</p>
<app-changes *ngIf="org" [changeType]="ChangeType.ORG" [id]="org.id"></app-changes>
</metainfo>
</app-meta-layout>
@@ -0,0 +1,113 @@
h1 {
font-family: ailerons;
margin-top: 0;
}
.sub {
color: #81868a;
margin-bottom: 2rem;
}
.state-label {
font-size: .9rem;
color: #81868a;
margin-bottom: .5rem;
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
.table-header-row {
display: flex;
align-items: center;
.col {
display: flex;
flex-direction: column;
.desc {
font-size: .8rem;
color: #81868a;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.icon-button {
margin-right: .5rem;
}
.add-button {
border-radius: .5rem;
}
}
.side {
.details {
margin-bottom: 1rem;
border-bottom: 1px solid #81868a40;
padding-bottom: 1rem;
.row {
display: flex;
margin-bottom: 0.5rem;
align-items: center;
button {
display: none;
visibility: hidden;
}
&:hover {
button {
display: inline-block;
visibility: visible;
mat-icon {
font-size: 1.2rem;
}
}
}
.first {
flex: 1;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.fill-space {
flex: 1;
}
.second {
font-size: 0.8rem;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 1rem;
}
a {
&:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
.side-section {
color: #81868a;
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgDetailComponent } from './org-detail.component';
describe('OrgDetailComponent', () => {
let component: OrgDetailComponent;
let fixture: ComponentFixture<OrgDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OrgDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,72 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { Org, OrgMember, OrgMemberSearchResponse, OrgState } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-org-detail',
templateUrl: './org-detail.component.html',
styleUrls: ['./org-detail.component.scss'],
})
export class OrgDetailComponent implements OnInit, OnDestroy {
public orgId: string = '';
public org!: Org.AsObject;
public dataSource: MatTableDataSource<OrgMember.AsObject> = new MatTableDataSource<OrgMember.AsObject>();
public memberResult!: OrgMemberSearchResponse.AsObject;
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
public selection: SelectionModel<OrgMember.AsObject> = new SelectionModel<OrgMember.AsObject>(true, []);
public OrgState: any = OrgState;
public ChangeType: any = ChangeType;
private subscription: Subscription = new Subscription();
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private orgService: OrgService,
private toast: ToastService,
) { }
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private async getData({ id }: Params): Promise<void> {
this.orgId = id;
this.orgService.GetOrgById(this.orgId).then((org: Org) => {
this.org = org.toObject();
}).catch(error => {
this.toast.showError(error.message);
});
}
public changeState(event: MatButtonToggleChange | any): void {
if (event.value === OrgState.ORGSTATE_ACTIVE) {
this.orgService.ReactivateOrg(this.orgId).then(() => {
this.toast.showInfo('Reactivated Org');
}).catch(error => {
this.toast.showError(error.message);
});
} else if (event.value === OrgState.ORGSTATE_INACTIVE) {
this.orgService.DeactivateOrg(this.orgId).then(() => {
this.toast.showInfo('Deactivated Org');
}).catch(error => {
this.toast.showError(error.message);
});
}
}
}
@@ -0,0 +1,50 @@
<div class="max-width-container">
<h1>{{ 'ORG.PAGES.LIST' | translate }}</h1>
<p class="top-desc">{{'ORG.PAGES.LISTDESCRIPTION' | translate}}</p>
<div class="view-toggle">
<div class="anim-list" *ngIf="selection.selected.length > 0">
</div>
<button disabled mat-icon-button>
<mat-icon></mat-icon>
</button>
</div>
<div class="container">
<div matTooltip="{{'ORG.PAGES.SELECTORGTOOLTIP' | translate}}" class="item card"
*ngFor="let org of orgList; index as i" (click)="selectOrg(org, $event)"
[ngClass]="{ selected: selection.isSelected(org) }">
<!-- <mat-icon matTooltip="select org" (click)="selection.toggle(org)" class="selection-icon">
check_circle</mat-icon> -->
<div class="text-part">
<span *ngIf="org?.changeDate" class="top">last modified on
{{
dateFromTimestamp(org.changeDate) | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="org.name">{{ org.name }}</span>
<span class="name" *ngIf="!org.name">No Name</span>
<span class="description">{{org.id}}</span>
<span class="fill-space"></span>
<div class="icons">
<div class="current" *ngIf="activeOrg?.id === org?.id">
<span>{{'ORG.PAGES.ACTIVE' | translate}}</span></div>
</div>
</div>
<button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #editMenu="matMenu">
<ng-template matMenuContent>
<button (click)="selectOrg(org)" mat-menu-item>
{{'ACTIONS.VIEW' | translate}}
</button>
<button (click)="selection.toggle(org)" mat-menu-item>
{{'ACTIONS.INFO' | translate}}
</button>
</ng-template>
</mat-menu>
</div>
</div>
</div>
@@ -0,0 +1,183 @@
h1 {
font-family: ailerons;
margin-top: 0;
}
.top-desc {
color: #81868a;
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
padding-bottom: .5rem;
border-bottom: 1px solid #2d2e30;
.anim-list {
display: flex;
justify-content: flex-end;
align-items: center;
}
button {
&.left-button {
margin-right: 1rem;
}
}
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
.item {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 250px;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: 0.5rem;
box-sizing: border-box;
min-height: 166px;
* {
box-sizing: border-box;
}
.selection-icon {
opacity: 0;
position: absolute;
top: -12px;
left: -12px;
}
img {
height: 50px;
width: 50px;
margin: 1rem;
}
.text-part {
flex: 1;
display: flex;
flex-direction: column;
// justify-content: center;
min-height: 70px;
padding: 0.5rem 0;
.top {
font-size: 0.8rem;
margin-bottom: 0;
margin-top: .5rem;
}
.name {
margin-top: 1rem;
font-size: 1.2rem;
margin-bottom: 0.5rem;
font-family: 'Rubik', sans-serif;
}
.description {
font-size: 0.8rem;
}
.created {
font-size: 0.8rem;
}
.organization {
display: flex;
align-items: center;
margin-top: 1rem;
.org_avatar {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0;
margin-right: 1rem;
}
}
.fill-space {
flex: 1;
}
.icons {
margin-top: 1rem;
transition: all 0.3s;
display: flex;
align-items: center;
.current {
height: 10px;
width: 10px;
border-radius: 50%;
background-color: rgb(144,212,210);
display: flex;
margin: .5rem 0;
align-items: center;
box-sizing: border-box;
span {
margin-left: 1.5rem;
text-transform: uppercase;
color: rgb(144,212,210);
}
}
.icon {
opacity: 0;
margin-right: 3px;
font-size: 1.3rem;
height: 1.4rem;
color: #81868a;
}
}
}
.edit-button {
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: 0.25rem;
}
&:hover {
.selection-icon {
opacity: 1;
}
.text-part {
.icons {
.icon {
opacity: 1;
}
}
}
}
&.selected {
.text-part {
.icons {
opacity: 1;
}
}
.selection-icon {
opacity: 1;
}
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgGridComponent } from './org-grid.component';
describe('OrgGridComponent', () => {
let component: OrgGridComponent;
let fixture: ComponentFixture<OrgGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OrgGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,53 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { Org } from 'src/app/proto/generated/auth_pb';
import { AuthUserService } from 'src/app/services/auth-user.service';
import { AuthService } from 'src/app/services/auth.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-org-grid',
templateUrl: './org-grid.component.html',
styleUrls: ['./org-grid.component.scss'],
})
export class OrgGridComponent {
public activeOrg!: Org.AsObject;
public orgList: Org.AsObject[] = [];
public selection: SelectionModel<Org.AsObject> = new SelectionModel<Org.AsObject>(true, []);
public selectedIndex: number = -1;
constructor(
public authService: AuthService,
private userService: AuthUserService,
private toast: ToastService,
private router: Router,
) {
this.getData(10, 0);
this.authService.GetActiveOrg().then(org => this.activeOrg = org);
}
private getData(limit: number, offset: number): void {
this.userService.SearchMyProjectOrgs(limit, offset).then(res => {
this.orgList = res.toObject().resultList;
console.log(this.orgList);
}).catch(error => {
console.error(error);
this.toast.showError(error.message);
});
}
public selectOrg(item: Org.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) {
this.authService.setActiveOrg(item);
this.router.navigate(['/orgs', item.id]);
}
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
}
@@ -0,0 +1,14 @@
<form>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Role Name</mat-label>
<mat-select [formControl]="myControl" multiple>
<mat-option *ngIf="isLoading" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let role of allRoles" [value]="role">
{{'ROLES.'+role | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</form>
@@ -0,0 +1,3 @@
.full-width {
width: 100%;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgMemberRolesAutocompleteComponent } from './org-member-roles-autocomplete.component';
describe('OrgMemberRolesAutocompleteComponent', () => {
let component: OrgMemberRolesAutocompleteComponent;
let fixture: ComponentFixture<OrgMemberRolesAutocompleteComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OrgMemberRolesAutocompleteComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgMemberRolesAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,33 @@
import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatAutocomplete } from '@angular/material/autocomplete';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-org-member-roles-autocomplete',
templateUrl: './org-member-roles-autocomplete.component.html',
styleUrls: ['./org-member-roles-autocomplete.component.scss'],
})
export class OrgMemberRolesAutocompleteComponent {
public myControl: FormControl = new FormControl();
public names: string[] = [];
public roles: string[] = [];
public allRoles: string[] = [];
public isLoading: boolean = false;
@ViewChild('nameInput') public nameInput!: ElementRef<HTMLInputElement>;
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Output() public selectionChanged: EventEmitter<string[]> = new EventEmitter();
constructor(private orgService: OrgService, private toast: ToastService) {
this.orgService.GetOrgMemberRoles().then(resp => {
this.allRoles = resp.toObject().rolesList;
console.log(this.allRoles);
}).catch(error => {
this.toast.showError(error.message);
});
this.myControl.valueChanges.subscribe(change => {
this.selectionChanged.emit(change);
});
}
}
@@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { OrgMemberRolesAutocompleteComponent } from './org-member-roles-autocomplete.component';
@NgModule({
declarations: [OrgMemberRolesAutocompleteComponent],
imports: [
CommonModule,
MatButtonModule,
MatSelectModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
exports: [
OrgMemberRolesAutocompleteComponent,
],
})
export class OrgMemberRolesAutocompleteModule { }
@@ -0,0 +1,56 @@
import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { OrgMember } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
/**
* Data source for the OrgMembers view. This class should
* encapsulate all logic for fetching and manipulating the displayed data
* (including sorting, pagination, and filtering).
*/
export class OrgMembersDataSource extends DataSource<OrgMember.AsObject> {
public totalResult: number = 0;
public membersSubject: BehaviorSubject<OrgMember.AsObject[]> = new BehaviorSubject<OrgMember.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(private orgService: OrgService) {
super();
}
public loadMembers(orgId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.orgService.SearchOrgMembers(orgId, pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.toObject().totalResult;
return resp.toObject().resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
/**
* Connect this data source to the table. The table will only update when
* the returned stream emits new items.
* @returns A stream of the items to be rendered.
*/
public connect(): Observable<OrgMember.AsObject[]> {
return this.membersSubject.asObservable();
}
/**
* Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect.
*/
public disconnect(): void {
this.membersSubject.complete();
this.loadingSubject.complete();
}
}
@@ -0,0 +1,87 @@
<div class="table-header-row">
<div class="col">
<ng-container *ngIf="!selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
<span class="count">{{dataSource.membersSubject.value.length}}</span>
</ng-container>
<ng-container *ngIf="selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.SELECTION' | translate}}</span>
<span class="count">{{selection?.selected?.length}}</span>
</ng-container>
</div>
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['org.member.delete']">
<button [disabled]="disabled" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button"
(click)="removeSelectedOrgMembers()" mat-icon-button *ngIf="selection.hasValue()">
<mat-icon>delete_outline</mat-icon>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['org.member.write']">
<button [disabled]="disabled" color="primary" class="add-button" mat-raised-button (click)="openAddMember()">
<mat-icon>add</mat-icon>{{ 'ORG_DETAIL.MEMBER.ADD' | translate }}
</button>
</ng-template>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table mat-table class="background-style full-width-table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'ORG_DETAIL.MEMBER.FIRSTNAME' | translate }} </th>
<td [routerLink]="['/user', member.userId]" class="pointer" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'ORG_DETAIL.MEMBER.LASTNAME' | translate }} </th>
<td [routerLink]="['/user', member.userId]" class="pointer" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'ORG_DETAIL.MEMBER.USERNAME' | translate }} </th>
<td [routerLink]="['/user', member.userId]" class="pointer" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'ORG_DETAIL.MEMBER.EMAIL' | translate }} </th>
<td [routerLink]="['/user', member.userId]" class="pointer" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ORG_DETAIL.MEMBER.ROLES' | translate }} </th>
<td [routerLink]="['/user', member.userId]" class="pointer" mat-cell *matCellDef="let member">
<span class="role app-label"
*ngFor="let role of member.rolesList; index as i">{{'ROLES.'+role | translate }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator class="background-style" #paginator [length]="dataSource.totalResult" [pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
</div>
@@ -0,0 +1,82 @@
h1 {
font-family: ailerons;
}
.table-header-row {
display: flex;
align-items: center;
.col {
display: flex;
flex-direction: column;
.desc {
font-size: .8rem;
color: #81868a;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.icon-button {
margin-right: .5rem;
}
.add-button {
border-radius: .5rem;
}
}
.table-wrapper {
overflow: auto;
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
table, mat-paginator {
width: 100%;
td, th {
padding: 0 1rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
.role {
display: inline-block;
margin: .25rem;
}
}
}
.pointer {
outline: none;
cursor: pointer;
}
@@ -0,0 +1,34 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { OrgMembersComponent } from './org-members.component';
describe('OrgMembersComponent', () => {
let component: OrgMembersComponent;
let fixture: ComponentFixture<OrgMembersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OrgMembersComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgMembersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,108 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { OrgMember, User } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
import {
CreationType,
ProjectMemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/project-member-create-dialog.component';
import { OrgMembersDataSource } from './org-members-datasource';
@Component({
selector: 'app-org-members',
templateUrl: './org-members.component.html',
styleUrls: ['./org-members.component.scss'],
})
export class OrgMembersComponent implements AfterViewInit, OnInit {
@Input() public orgId: string = '';
@Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<OrgMember.AsObject>;
@Output() public changedSelection: EventEmitter<Array<OrgMember.AsObject>> = new EventEmitter();
public dataSource!: OrgMembersDataSource;
public selection: SelectionModel<OrgMember.AsObject> = new SelectionModel<OrgMember.AsObject>(true, []);
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor(private orgService: OrgService, private dialog: MatDialog, private toast: ToastService) { }
public ngOnInit(): void {
this.dataSource = new OrgMembersDataSource(this.orgService);
this.dataSource.loadMembers(this.orgId, 0, 25, 'asc');
this.selection.changed.subscribe(change => {
console.log(change);
// this.changedSelection.emit(change)
});
}
public ngAfterViewInit(): void {
this.paginator.page
.pipe(
tap(() => this.loadMembersPage()),
)
.subscribe();
}
private loadMembersPage(): void {
this.dataSource.loadMembers(
this.orgId,
this.paginator.pageIndex,
this.paginator.pageSize,
);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach((row: OrgMember.AsObject) => this.selection.select(row));
}
public openAddMember(): void {
const dialogRef = this.dialog.open(ProjectMemberCreateDialogComponent, {
data: {
creationType: CreationType.ORG,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.orgService.AddOrgMember(this.orgId, user.id, roles);
})).then(() => {
this.toast.showError('members added');
}).catch(error => {
this.toast.showError(error.message);
});
}
}
});
}
public removeSelectedOrgMembers(): void {
Promise.all(this.selection.selected.map(member => {
return this.orgService.RemoveOrgMember(this.orgId, member.userId).then(() => {
this.toast.showInfo('Removed successfully');
}).catch(error => {
this.toast.showError(error.message);
});
}));
}
}
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ProjectMemberCreateDialogModule } from '../../../modules/add-member-dialog/project-member-create-dialog.module';
import { OrgMembersComponent } from './org-members.component';
@NgModule({
declarations: [OrgMembersComponent],
imports: [
CommonModule,
FormsModule,
ProjectMemberCreateDialogModule,
HasRoleModule,
MatButtonModule,
MatDialogModule,
MatTableModule,
MatPaginatorModule,
MatIconModule,
RouterModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatTooltipModule,
TranslateModule,
],
exports: [
OrgMembersComponent,
],
})
export class OrgMembersModule { }
@@ -0,0 +1,49 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RoleGuard } from 'src/app/guards/role.guard';
import { OrgCreateComponent } from './org-create/org-create.component';
import { OrgDetailComponent } from './org-detail/org-detail.component';
import { OrgGridComponent } from './org-grid/org-grid.component';
import { PasswordPolicyComponent, PolicyComponentAction } from './password-policy/password-policy.component';
const routes: Routes = [
{
path: 'create',
component: OrgCreateComponent,
canActivate: [RoleGuard],
data: {
roles: ['iam.write'],
},
loadChildren: () => import('./org-create/org-create.module').then(m => m.OrgCreateModule),
},
{
path: ':id/policy/:policytype/create',
component: PasswordPolicyComponent,
data: {
action: PolicyComponentAction.CREATE,
},
},
{
path: ':id/policy/:policytype',
component: PasswordPolicyComponent,
data: {
action: PolicyComponentAction.MODIFY,
},
loadChildren: () => import('./password-policy/password-policy.module').then(m => m.PasswordPolicyModule),
},
{
path: ':id',
component: OrgDetailComponent,
},
{
path: '',
component: OrgGridComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrgsRoutingModule { }
+57
View File
@@ -0,0 +1,57 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { HttpLoaderFactory } from 'src/app/app.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { ChangesModule } from '../../modules/changes/changes.module';
import { OrgDetailComponent } from './org-detail/org-detail.component';
import { OrgGridComponent } from './org-grid/org-grid.component';
import { OrgMembersModule } from './org-members/org-members.module';
import { OrgsRoutingModule } from './orgs-routing.module';
import { PolicyGridComponent } from './policy-grid/policy-grid.component';
@NgModule({
declarations: [OrgDetailComponent, OrgGridComponent, PolicyGridComponent],
imports: [
CommonModule,
OrgsRoutingModule,
OrgMembersModule,
FormsModule,
HasRoleModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatDialogModule,
CardModule,
MatIconModule,
ReactiveFormsModule,
MatButtonToggleModule,
MetaLayoutModule,
MatTooltipModule,
MatMenuModule,
ChangesModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
],
exports: [],
schemas: [NO_ERRORS_SCHEMA],
})
export class OrgsModule { }
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PasswordPolicyComponent } from './password-policy.component';
const routes: Routes = [
{
path: '',
component: PasswordPolicyComponent,
data: { animation: 'DetailPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PasswordPolicyRoutingModule { }
@@ -0,0 +1,133 @@
<div class="max-width-container">
<div class="container">
<div class="left">
<a [routerLink]="[ '/orgs/', orgId]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
</div>
<div class="right">
<div class="head">
<h1 *ngIf="(titleSub | async) || '' as titletrans">{{ titletrans | translate }}</h1>
<p class="desc" *ngIf="(descSub | async) || '' as desctrans">{{ desctrans | translate }}</p>
</div>
<div>
<div class="content" *ngIf="policyType === PolicyComponentType?.LOCKOUT">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="lockoutData.description"
required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXATTEMPTS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAttempts()">
<mat-icon>add</mat-icon>
</button>
<span>{{lockoutData?.maxAttempts}}</span>
<button mat-icon-button (click)="decrementMaxAttempts()">
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="showLockOutFailures" ngDefaultControl
[(ngModel)]="complexityData.showLockOutFailures">
</mat-slide-toggle>
</div>
</div>
<div *ngIf="policyType === PolicyComponentType?.COMPLEXITY" class="content">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="complexityData.description"
required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MINLENGTH' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementLength()">
<mat-icon>add</mat-icon>
</button>
<span>{{complexityData?.minLength}}</span>
<button mat-icon-button (click)="decrementLength()">
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASNUMBER' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASSYMBOL' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASLOWERCASE' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="hasLowercase" ngDefaultControl
[(ngModel)]="complexityData.hasLowercase">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASUPPERCASE' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="hasUppercase" ngDefaultControl
[(ngModel)]="complexityData.hasUppercase">
</mat-slide-toggle>
</div>
</div>
<div class="content" *ngIf="policyType === PolicyComponentType?.AGE">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="ageData.description"
required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementExpireWarnDays()">
<mat-icon>add</mat-icon>
</button>
<span>{{ageData?.expireWarnDays}}</span>
<button mat-icon-button (click)="decrementExpireWarnDays()">
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXAGEDAYS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAgeDays()">
<mat-icon>add</mat-icon>
</button>
<span>{{ageData?.maxAgeDays}}</span>
<button mat-icon-button (click)="decrementMaxAgeDays()">
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="accent" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,82 @@
.container {
display: flex;
padding-bottom: 3rem;
.left {
width: 100px;
display: flex;
padding: 1rem;
justify-content: center;
a {
margin-top: .2rem;
}
}
.right {
flex: 1;
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
a {
display: block;
}
h1 {
font-size: 1.2rem;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #81868a;
}
}
.content {
padding-top: 1rem;
display: flex;
flex-direction: column;
.row {
display: flex;
align-items: center;
padding: .5rem 0;
.left-desc {
text-transform: uppercase;
color: #81868a;
font-size: .9rem;
}
.fill-space {
flex: 1;
}
.length-wrapper {
display: flex;
align-items: center;
}
}
}
.btn-container {
display: flex;
justify-content: flex-end;
button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PasswordPolicyComponent } from './password-policy.component';
describe('PasswordPolicyComponent', () => {
let component: PasswordPolicyComponent;
let fixture: ComponentFixture<PasswordPolicyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PasswordPolicyComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,270 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { PasswordAgePolicy, PasswordComplexityPolicy, PasswordLockoutPolicy } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
export enum PolicyComponentAction {
CREATE = 'create',
MODIFY = 'modify',
}
export enum PolicyComponentType {
LOCKOUT = 'lockout',
AGE = 'age',
COMPLEXITY = 'complexity',
}
@Component({
selector: 'app-password-policy',
templateUrl: './password-policy.component.html',
styleUrls: ['./password-policy.component.scss'],
})
export class PasswordPolicyComponent implements OnInit, OnDestroy {
public orgId: string = '';
titleSub: BehaviorSubject<string> = new BehaviorSubject('');
descSub: BehaviorSubject<string> = new BehaviorSubject('');
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
policyData!: PasswordLockoutPolicy.AsObject | PasswordAgePolicy.AsObject | PasswordComplexityPolicy.AsObject;
policyType: PolicyComponentType = PolicyComponentType.COMPLEXITY;
public PolicyComponentType: any = PolicyComponentType;
public PolicyComponentAction: any = PolicyComponentAction;
public lockoutForm!: FormGroup;
public ageForm!: FormGroup;
public complexityData: any = {
minLength: 8,
description: '',
hasNumber: true,
hasSymbol: true,
hasLowercase: true,
hasUppercase: true,
};
public lockoutData: any = {
description: '',
maxAttempts: 5,
showLockOutFailures: true,
};
public ageData: any = {
description: '',
expireWarnDays: 80,
maxAgeDays: 90,
};
private sub: Subscription = new Subscription();
constructor(
private route: ActivatedRoute,
private orgService: OrgService,
private router: Router,
private toast: ToastService,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
return this.route.params;
})).subscribe(params => {
this.orgId = params.id;
this.policyType = params.policytype;
switch (params.policytype) {
case PolicyComponentType.LOCKOUT:
this.titleSub.next('ORG.POLICY.PWD_LOCKOUT.TITLECREATE');
this.descSub.next('ORG.POLICY.PWD_LOCKOUT.DESCRIPTIONCREATE');
break;
case PolicyComponentType.AGE:
this.titleSub.next('ORG.POLICY.PWD_AGE.TITLECREATE');
this.descSub.next('ORG.POLICY.PWD_AGE.DESCRIPTIONCREATE');
break;
case PolicyComponentType.COMPLEXITY:
this.titleSub.next('ORG.POLICY.PWD_COMPLEXITY.TITLECREATE');
this.descSub.next('ORG.POLICY.PWD_COMPLEXITY.DESCRIPTIONCREATE');
break;
}
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
switch (this.policyType) {
case PolicyComponentType.LOCKOUT:
this.lockoutData = data.toObject();
break;
case PolicyComponentType.AGE:
this.ageData = data.toObject();
break;
case PolicyComponentType.COMPLEXITY:
this.complexityData = data.toObject();
break;
}
});
}
});
}
ngOnInit(): void {
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(params: any): Promise<any> {
switch (params.policytype) {
case PolicyComponentType.LOCKOUT:
this.titleSub.next('ORG.POLICY.PWD_LOCKOUT.TITLE');
this.descSub.next('ORG.POLICY.PWD_LOCKOUT.DESCRIPTION');
return this.orgService.GetPasswordLockoutPolicy();
case PolicyComponentType.AGE:
this.titleSub.next('ORG.POLICY.PWD_AGE.TITLE');
this.descSub.next('ORG.POLICY.PWD_AGE.DESCRIPTION');
return this.orgService.GetPasswordAgePolicy();
case PolicyComponentType.COMPLEXITY:
this.titleSub.next('ORG.POLICY.PWD_COMPLEXITY.TITLE');
this.descSub.next('ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION');
return this.orgService.GetPasswordComplexityPolicy();
}
}
public incrementLength(): void {
if (this.complexityData?.minLength) {
this.complexityData.minLength++;
}
}
public decrementLength(): void {
if (this.complexityData?.minLength && this.complexityData?.minLength > 0) {
this.complexityData.minLength--;
}
}
public incrementMaxAttempts(): void {
if (this.lockoutData?.maxAttempts) {
this.lockoutData.maxAttempts++;
}
}
public decrementMaxAttempts(): void {
if (this.lockoutData?.maxAttempts && this.lockoutData?.maxAttempts > 0) {
this.lockoutData.maxAttempts--;
}
}
public incrementExpireWarnDays(): void {
if (this.ageData?.expireWarnDays) {
this.ageData.expireWarnDays++;
}
}
public decrementExpireWarnDays(): void {
if (this.ageData?.expireWarnDays && this.ageData?.expireWarnDays > 0) {
this.ageData.expireWarnDays--;
}
}
public incrementMaxAgeDays(): void {
if (this.ageData?.maxAgeDays) {
this.ageData.maxAgeDays++;
}
}
public decrementMaxAgeDays(): void {
if (this.ageData?.maxAgeDays && this.ageData?.maxAgeDays > 0) {
this.ageData.maxAgeDays--;
}
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
switch (this.policyType) {
case PolicyComponentType.LOCKOUT:
this.orgService.CreatePasswordLockoutPolicy(
this.lockoutData.description,
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.AGE:
this.orgService.CreatePasswordAgePolicy(
this.ageData.description,
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.COMPLEXITY:
console.log(this.complexityData);
this.orgService.CreatePasswordComplexityPolicy(
this.complexityData.description,
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
switch (this.policyType) {
case PolicyComponentType.LOCKOUT:
this.orgService.UpdatePasswordLockoutPolicy(
this.lockoutData.description,
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.AGE:
this.orgService.UpdatePasswordAgePolicy(
this.ageData.description,
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.COMPLEXITY:
console.log(this.complexityData);
this.orgService.UpdatePasswordComplexityPolicy(
this.complexityData.description,
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
}).catch(error => {
this.toast.showError(error.message);
});
break;
}
}
}
}
@@ -0,0 +1,36 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { HttpLoaderFactory } from 'src/app/app.module';
import { PasswordPolicyRoutingModule } from './password-policy-routing.module';
import { PasswordPolicyComponent } from './password-policy.component';
@NgModule({
declarations: [PasswordPolicyComponent],
imports: [
PasswordPolicyRoutingModule,
CommonModule,
FormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
],
})
export class PasswordPolicyModule { }
@@ -0,0 +1,102 @@
<h1>{{'ORG.POLICY.TITLE' | translate}}</h1>
<p class="top-desc">{{'ORG.POLICY.DESCRIPTION' | translate}}</p>
<div class="row-lyt">
<div class="p-item card">
<div class="avatar">
<mat-icon svgIcon="mdi_lock_reset"></mat-icon>
</div>
<div class="title">
<span>{{'ORG.POLICY.PWD_AGE.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<mat-icon class="icon" *ngIf="agePolicy">check_circle</mat-icon>
</button>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" (click)="deletePolicy(PolicyComponentType.AGE)"
mat-icon-button>
<mat-icon>delete_outline</mat-icon>
</button>
</ng-template>
</div>
<p *ngIf="agePolicy?.description; else showDescAge" class="desc">{{ agePolicy.description }}</p>
<ng-template #showDescAge>
<p class="desc">
{{'ORG.POLICY.PWD_AGE.DESCRIPTION' | translate}}
</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['policy.write']">
<button [disabled]="agePolicy" [routerLink]="[ 'policy', PolicyComponentType.AGE,'create' ]"
[disabled]="agePolicy?.state === PolicyState.ACTIVE_POLICY" color="primary"
mat-raised-button>{{'ORG.POLICY.BTN_INSTALL' | translate}}</button>
</ng-template>
<button [disabled]="!agePolicy" [routerLink]="[ 'policy', PolicyComponentType.AGE ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
<div class="p-item card">
<div class="avatar">
<mat-icon svgIcon="mdi_textbox_password"></mat-icon>
</div>
<div class="title">
<span>{{'ORG.POLICY.PWD_COMPLEXITY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<mat-icon class="icon" *ngIf="complexityPolicy">
check_circle</mat-icon>
</button>
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}"
(click)="deletePolicy(PolicyComponentType.COMPLEXITY)" mat-icon-button>
<mat-icon>delete_outline</mat-icon>
</button>
</div>
<p *ngIf="complexityPolicy?.description; else showDescComplexity" class="desc">
{{ complexityPolicy.description }}</p>
<ng-template #showDescComplexity>
<p class="desc">
{{'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate}}</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<button [disabled]="complexityPolicy" [routerLink]="[ 'policy', PolicyComponentType.COMPLEXITY,'create' ]"
color="primary" mat-raised-button>{{'ORG.POLICY.BTN_INSTALL' | translate}}</button>
<button [disabled]="!complexityPolicy" [routerLink]="[ 'policy', PolicyComponentType.COMPLEXITY ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
<div class="p-item card">
<div class="avatar">
<mat-icon svgIcon="mdi_lock_question"></mat-icon>
</div>
<div class="title"><span>{{'ORG.POLICY.PWD_LOCKOUT.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<mat-icon class="icon" *ngIf="lockoutPolicy">check_circle
</mat-icon>
</button>
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" (click)="deletePolicy(PolicyComponentType.LOCKOUT)"
mat-icon-button>
<mat-icon>delete_outline</mat-icon>
</button>
</div>
<p *ngIf="lockoutPolicy?.description; else showDescLockout" class="desc">{{ lockoutPolicy.description }}</p>
<ng-template #showDescLockout>
<p class="desc">
{{'ORG.POLICY.PWD_LOCKOUT.DESCRIPTION' | translate}}
</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<button [disabled]="lockoutPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOCKOUT,'create' ]"
color="primary" mat-raised-button>{{'ORG.POLICY.BTN_INSTALL' | translate}}</button>
<button [disabled]="!lockoutPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOCKOUT ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
</div>
@@ -0,0 +1,71 @@
h1 {
font-size: 1.2rem;
}
.top-desc {
color: #81868a;
}
.row-lyt {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
.p-item {
flex-basis: 300px;
margin: 1rem;
display: flex;
flex-direction: column;
min-height: 250px;
padding: 1rem;
.avatar {
height: 60px;
width: 60px;
border-radius: 50%;
background: linear-gradient(40deg,rgb(129, 85, 185) 30%, #8983F7);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: .5rem;
mat-icon {
font-size: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
}
}
.title {
display: flex;
align-items: center;
span {
font-size: 1.2rem;
}
.icon {
margin-left: 1rem;
margin-right: 1rem;
}
}
.desc {
font-size: .9rem;
}
.fill-space {
flex: 1;
}
.btn-wrapper {
display: flex;
}
button {
margin-right: 1rem;
border-radius: .5rem;
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PolicyGridComponent } from './policy-grid.component';
describe('PolicyGridComponent', () => {
let component: PolicyGridComponent;
let fixture: ComponentFixture<PolicyGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PolicyGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PolicyGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,66 @@
import { Component, OnInit } from '@angular/core';
import {
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
PolicyState,
} from 'src/app/proto/generated/management_pb';
import { AuthUserService } from 'src/app/services/auth-user.service';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentType } from '../password-policy/password-policy.component';
@Component({
selector: 'app-policy-grid',
templateUrl: './policy-grid.component.html',
styleUrls: ['./policy-grid.component.scss'],
})
export class PolicyGridComponent implements OnInit {
public lockoutPolicy!: PasswordLockoutPolicy.AsObject;
public agePolicy!: PasswordAgePolicy.AsObject;
public complexityPolicy!: PasswordComplexityPolicy.AsObject;
public PolicyState: any = PolicyState;
public PolicyComponentType: any = PolicyComponentType;
constructor(private orgService: OrgService, public authUserService: AuthUserService, private toast: ToastService) {
this.getData();
}
ngOnInit(): void {
}
private getData(): void {
this.orgService.GetPasswordLockoutPolicy().then(data => this.lockoutPolicy = data.toObject()).catch(error => { });
this.orgService.GetPasswordAgePolicy().then(data => this.agePolicy = data.toObject()).catch(error => { });
this.orgService.GetPasswordComplexityPolicy().then(data => this.complexityPolicy = data.toObject())
.catch(error => { });
}
public deletePolicy(type: PolicyComponentType): void {
switch (type) {
case PolicyComponentType.LOCKOUT:
this.orgService.DeletePasswordLockoutPolicy(this.lockoutPolicy.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.AGE:
this.orgService.DeletePasswordAgePolicy(this.agePolicy.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.COMPLEXITY:
this.orgService.DeletePasswordLockoutPolicy(this.lockoutPolicy.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error.message);
});
break;
}
}
}
@@ -0,0 +1,25 @@
<form>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Organization</mat-label>
<mat-chip-list #chipList aria-label="org selection">
<mat-chip class="chip" *ngFor="let selectedOrg of orgs" [selectable]="selectable" [removable]="removable"
(removed)="remove(selectedOrg)">
{{selectedOrg.name}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input placeholder="{{'ORG_DETAIL.DETAIL.DOMAIN' | translate}}" #domainInput [formControl]="myControl"
[matAutocomplete]="auto" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)" />
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let org of filteredOrgs" [value]="org">
{{org.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
@@ -0,0 +1,3 @@
.full-width {
width: 100%;
}
@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
SearchProjectAutocompleteComponent,
} from '../../../modules/search-project-autocomplete/search-project-autocomplete.component';
describe('SearchProjectComponent', () => {
let component: SearchProjectAutocompleteComponent;
let fixture: ComponentFixture<SearchProjectAutocompleteComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SearchProjectAutocompleteComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchProjectAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,100 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { debounceTime, tap } from 'rxjs/operators';
import { Org } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-search-org-autocomplete',
templateUrl: './search-org-autocomplete.component.html',
styleUrls: ['./search-org-autocomplete.component.scss'],
})
export class SearchOrgAutocompleteComponent {
public selectable: boolean = true;
public removable: boolean = true;
public addOnBlur: boolean = true;
public separatorKeysCodes: number[] = [ENTER, COMMA];
public myControl: FormControl = new FormControl();
public names: string[] = [];
public orgs: Array<Org.AsObject> = [];
public filteredOrgs: Array<Org.AsObject> = [];
public isLoading: boolean = false;
@ViewChild('domainInput') public domainInput!: ElementRef<HTMLInputElement>;
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Input() public singleOutput: boolean = false;
@Output() public selectionChanged: EventEmitter<Org.AsObject | Org.AsObject[]> = new EventEmitter();
constructor(private orgService: OrgService, private toast: ToastService) {
this.myControl.valueChanges.pipe(debounceTime(200), tap(() => this.isLoading = true)).subscribe(value => {
return this.orgService.getOrgByDomainGlobal(value).then((org) => {
this.isLoading = false;
if (org) {
this.filteredOrgs = [org.toObject()];
}
}).catch(error => {
this.isLoading = false;
// this.toast.showInfo(error.message);
});
});
}
public displayFn(org?: Org.AsObject): string | undefined {
return org ? `${org.name}` : undefined;
}
public add(event: MatChipInputEvent): void {
if (!this.matAutocomplete.isOpen) {
const input = event.input;
const value = event.value;
if ((value || '').trim()) {
const index = this.filteredOrgs.findIndex((org) => {
if (org.name) {
return org.name === value;
}
});
if (index > -1) {
if (this.orgs && this.orgs.length > 0) {
this.orgs.push(this.filteredOrgs[index]);
} else {
this.orgs = [this.filteredOrgs[index]];
}
}
}
if (input) {
input.value = '';
}
}
}
public remove(org: Org.AsObject): void {
const index = this.orgs.indexOf(org);
if (index >= 0) {
this.orgs.splice(index, 1);
}
}
public selected(event: MatAutocompleteSelectedEvent): void {
const index = this.filteredOrgs.findIndex((org) => org === event.option.value);
if (index !== -1) {
if (this.singleOutput) {
this.selectionChanged.emit(this.filteredOrgs[index]);
} else {
if (this.orgs && this.orgs.length > 0) {
this.orgs.push(this.orgs[index]);
this.selectionChanged.emit(this.orgs);
} else {
this.orgs = [this.filteredOrgs[index]];
}
this.domainInput.nativeElement.value = '';
this.myControl.setValue(null);
}
}
}
}
@@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
@NgModule({
declarations: [SearchOrgAutocompleteComponent],
imports: [
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
MatFormFieldModule,
MatIconModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
exports: [
SearchOrgAutocompleteComponent,
],
})
export class SearchOrgAutocompleteModule { }