mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-06 22:32:16 +00:00
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:
co-authored by
Livio Amstutz
Max Peintner
parent
9e32740eb8
commit
92a294f5c8
@@ -0,0 +1,86 @@
|
||||
<div class="container">
|
||||
<div class="abort-container">
|
||||
<button (click)="close()" mat-icon-button>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span><span class="abort-2">Step
|
||||
{{ currentCreateStep }} of
|
||||
{{ createSteps }}</span>
|
||||
</div>
|
||||
|
||||
<h1>{{'APP.PAGES.CREATE_OIDC_DESc' | translate}}</h1>
|
||||
<form [formGroup]="form" (ngSubmit)="saveOIDCApp()">
|
||||
<div class="content">
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
|
||||
<mat-select formControlName="responseTypesList" multiple>
|
||||
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
|
||||
{{ 'APP.OIDC.RESPONSE'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
|
||||
<mat-select formControlName="grantTypesList" multiple>
|
||||
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
|
||||
{{ 'APP.OIDC.GRANT'+grant | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
|
||||
<mat-select formControlName="applicationType">
|
||||
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
|
||||
{{ 'APP.OIDC.APPTYPE'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
|
||||
<mat-select formControlName="authMethodType">
|
||||
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
|
||||
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
|
||||
<mat-chip-list #chipRedirectList aria-label="uri selection">
|
||||
<mat-chip *ngFor="let uri of oidcApp.redirectUrisList" removable
|
||||
(removed)="removeUri(uri, 'REDIRECT')">
|
||||
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
|
||||
</mat-chip>
|
||||
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'REDIRECT')">
|
||||
</mat-chip-list>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
|
||||
<mat-chip-list #chipPostRedirectList aria-label="uri selection">
|
||||
<mat-chip *ngFor="let uri of oidcApp.postLogoutRedirectUrisList" removable
|
||||
(removed)="removeUri(uri, 'POSTREDIRECT')">
|
||||
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
|
||||
</mat-chip>
|
||||
<input [matChipInputFor]="chipPostRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'POSTREDIRECT')">
|
||||
</mat-chip-list>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
|
||||
type="submit">
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.formfield {
|
||||
width: 400px;
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.autocomplete {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.access-token {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
margin: 0 -.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 0 40%;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
flex-basis: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.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 { AppCreateComponent } from './app-create.component';
|
||||
|
||||
describe('AppCreateComponent', () => {
|
||||
let component: AppCreateComponent;
|
||||
let fixture: ComponentFixture<AppCreateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppCreateComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppCreateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
Application,
|
||||
OIDCApplicationCreate,
|
||||
OIDCApplicationType,
|
||||
OIDCAuthMethodType,
|
||||
OIDCGrantType,
|
||||
OIDCResponseType,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-create',
|
||||
templateUrl: './app-create.component.html',
|
||||
styleUrls: ['./app-create.component.scss'],
|
||||
})
|
||||
export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
private subscription?: Subscription;
|
||||
public projectId: string = '';
|
||||
public oidcApp: OIDCApplicationCreate.AsObject = new OIDCApplicationCreate().toObject();
|
||||
public oidcResponseTypes: OIDCResponseType[] = [
|
||||
OIDCResponseType.OIDCRESPONSETYPE_CODE,
|
||||
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
|
||||
OIDCResponseType.OIDCRESPONSETYPE_TOKEN,
|
||||
];
|
||||
public oidcGrantTypes: OIDCGrantType[] = [
|
||||
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
|
||||
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
|
||||
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
|
||||
];
|
||||
public oidcAppTypes: OIDCApplicationType[] = [
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
|
||||
];
|
||||
public oidcAuthMethodType: OIDCAuthMethodType[] = [
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
|
||||
];
|
||||
|
||||
public form!: FormGroup;
|
||||
public createSteps: number = 1;
|
||||
public currentCreateStep: number = 1;
|
||||
public postLogoutRedirectUrisList: string[] = [];
|
||||
|
||||
public addOnBlur: boolean = true;
|
||||
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private projectService: ProjectService,
|
||||
private fb: FormBuilder,
|
||||
private _location: Location,
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', [Validators.required]],
|
||||
responseTypesList: ['', []],
|
||||
grantTypesList: ['', []],
|
||||
applicationType: ['', []],
|
||||
authMethodType: [],
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private async getData({ projectid }: Params): Promise<void> {
|
||||
this.projectId = projectid;
|
||||
this.oidcApp.projectId = projectid;
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public saveOIDCApp(): void {
|
||||
this.oidcApp.name = this.name?.value;
|
||||
this.oidcApp.applicationType = this.applicationType?.value;
|
||||
this.oidcApp.grantTypesList = this.grantTypesList?.value;
|
||||
this.oidcApp.responseTypesList = this.responseTypesList?.value;
|
||||
this.oidcApp.authMethodType = this.authMethodType?.value;
|
||||
|
||||
console.log(this.oidcApp);
|
||||
|
||||
this.projectService
|
||||
.CreateOIDCApp(this.oidcApp)
|
||||
.then((data: Application) => {
|
||||
this.showSavedDialog(data.toObject());
|
||||
})
|
||||
.catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
}
|
||||
|
||||
public showSavedDialog(app: Application.AsObject): void {
|
||||
if (app.oidcConfig !== undefined) {
|
||||
const dialogRef = this.dialog.open(AppSecretDialogComponent, {
|
||||
data: app.oidcConfig,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed');
|
||||
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
|
||||
}
|
||||
}
|
||||
|
||||
public addUri(event: MatChipInputEvent, target: string): void {
|
||||
const input = event.input;
|
||||
const value = event.value.trim();
|
||||
|
||||
if (value !== '') {
|
||||
if (target === 'REDIRECT') {
|
||||
this.oidcApp.redirectUrisList.push(value);
|
||||
} else if (target === 'POSTREDIRECT') {
|
||||
this.oidcApp.postLogoutRedirectUrisList.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
public removeUri(uri: string, target: string): void {
|
||||
if (target === 'REDIRECT') {
|
||||
const index = this.oidcApp.redirectUrisList.indexOf(uri);
|
||||
|
||||
if (index !== undefined && index >= 0) {
|
||||
this.oidcApp.redirectUrisList.splice(index, 1);
|
||||
}
|
||||
} else if (target === 'POSTREDIRECT') {
|
||||
const index = this.oidcApp.postLogoutRedirectUrisList.indexOf(uri);
|
||||
|
||||
if (index !== undefined && index >= 0) {
|
||||
this.oidcApp.postLogoutRedirectUrisList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get name(): AbstractControl | null {
|
||||
return this.form.get('name');
|
||||
}
|
||||
|
||||
|
||||
get responseTypesList(): AbstractControl | null {
|
||||
return this.form.get('responseTypesList');
|
||||
}
|
||||
|
||||
|
||||
get grantTypesList(): AbstractControl | null {
|
||||
return this.form.get('grantTypesList');
|
||||
}
|
||||
|
||||
|
||||
get applicationType(): AbstractControl | null {
|
||||
return this.form.get('applicationType');
|
||||
}
|
||||
|
||||
get authMethodType(): AbstractControl | null {
|
||||
return this.form.get('authMethodType');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<div class="max-width-container">
|
||||
<div class="head">
|
||||
<a (click)="navigateBack()" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
<h1>{{ 'APP.PAGES.TITLE' | translate }} {{app?.name}}</h1>
|
||||
|
||||
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
|
||||
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
|
||||
<div class="content">
|
||||
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
|
||||
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
|
||||
<mat-icon svgIcon="mdi_light_off"></mat-icon>
|
||||
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
|
||||
<mat-icon svgIcon="mdi_light_on"></mat-icon>
|
||||
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<mat-form-field class="formfield">
|
||||
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
|
||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</app-card>
|
||||
|
||||
<app-card title="{{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig">
|
||||
<card-actions class="card-actions">
|
||||
<button mat-stroked-button
|
||||
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
|
||||
</card-actions>
|
||||
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
|
||||
<div class="content">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.CLIENTID' | translate }}</mat-label>
|
||||
<input matInput formControlName="clientId" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
|
||||
<mat-select formControlName="responseTypesList" multiple>
|
||||
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
|
||||
{{ 'APP.OIDC.RESPONSE'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
|
||||
<mat-select formControlName="grantTypesList" multiple>
|
||||
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
|
||||
{{ 'APP.OIDC.GRANT'+grant | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
|
||||
<mat-select formControlName="applicationType">
|
||||
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
|
||||
{{ 'APP.OIDC.APPTYPE'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
|
||||
<mat-select formControlName="authMethodType">
|
||||
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
|
||||
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
|
||||
<mat-chip-list #chipRedirectList>
|
||||
<mat-chip *ngFor="let redirect of redirectUrisList" [selectable]="selectable"
|
||||
(removed)="remove(redirect, RedirectType.REDIRECT)">
|
||||
{{redirect}}
|
||||
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||
</mat-chip>
|
||||
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[matChipInputAddOnBlur]="addOnBlur"
|
||||
(matChipInputTokenEnd)="add($event, RedirectType.REDIRECT)">
|
||||
</mat-chip-list>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
|
||||
<mat-chip-list #chipPostRedirectList>
|
||||
<mat-chip *ngFor="let redirect of postLogoutRedirectUrisList" [selectable]="selectable"
|
||||
(removed)="remove(redirect, RedirectType.POSTREDIRECT)">
|
||||
{{redirect}}
|
||||
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||
</mat-chip>
|
||||
<input [matChipInputFor]="chipPostRedirectList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
|
||||
(matChipInputTokenEnd)="add($event, RedirectType.POSTREDIRECT)">
|
||||
</mat-chip-list>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
|
||||
<button type="submit" color="primary" [disabled]="appForm.invalid"
|
||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</app-card>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0 1rem;
|
||||
margin-left: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-size: .9rem;
|
||||
color: #81868a;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -.5rem;
|
||||
|
||||
&.nowrap{
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 1 30%;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
align-self: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 0 -.5rem;
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppDetailComponent } from './app-detail.component';
|
||||
|
||||
describe('AppDetailComponent', () => {
|
||||
let component: AppDetailComponent;
|
||||
let fixture: ComponentFixture<AppDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppDetailComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatButtonToggleChange } from '@angular/material/button-toggle';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
Application,
|
||||
AppState,
|
||||
OIDCApplicationType,
|
||||
OIDCAuthMethodType,
|
||||
OIDCConfig,
|
||||
OIDCGrantType,
|
||||
OIDCResponseType,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||
|
||||
enum RedirectType {
|
||||
REDIRECT = 'redirect',
|
||||
POSTREDIRECT = 'postredirect',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-detail',
|
||||
templateUrl: './app-detail.component.html',
|
||||
styleUrls: ['./app-detail.component.scss'],
|
||||
})
|
||||
export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public selectable: boolean = false;
|
||||
public removable: boolean = true;
|
||||
public addOnBlur: boolean = true;
|
||||
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
|
||||
private subscription?: Subscription;
|
||||
public projectId: string = '';
|
||||
public app!: Application.AsObject;
|
||||
public oidcResponseTypes: OIDCResponseType[] = [
|
||||
OIDCResponseType.OIDCRESPONSETYPE_CODE,
|
||||
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
|
||||
OIDCResponseType.OIDCRESPONSETYPE_TOKEN,
|
||||
];
|
||||
public oidcGrantTypes: OIDCGrantType[] = [
|
||||
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
|
||||
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
|
||||
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
|
||||
];
|
||||
public oidcAppTypes: OIDCApplicationType[] = [
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
|
||||
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
|
||||
];
|
||||
|
||||
public oidcAuthMethodType: OIDCAuthMethodType[] = [
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
|
||||
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
|
||||
];
|
||||
|
||||
public AppState: any = AppState;
|
||||
public appNameForm!: FormGroup;
|
||||
public appForm!: FormGroup;
|
||||
public redirectUrisList: string[] = [];
|
||||
public postLogoutRedirectUrisList: string[] = [];
|
||||
|
||||
public RedirectType: any = RedirectType;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private fb: FormBuilder,
|
||||
private _location: Location,
|
||||
private dialog: MatDialog,
|
||||
) {
|
||||
this.appNameForm = this.fb.group({
|
||||
state: ['', []],
|
||||
name: ['', [Validators.required]],
|
||||
});
|
||||
this.appForm = this.fb.group({
|
||||
clientId: [{ value: '', disabled: true }],
|
||||
responseTypesList: [],
|
||||
grantTypesList: [],
|
||||
applicationType: [],
|
||||
authMethodType: [],
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private async getData({ projectid, id }: Params): Promise<void> {
|
||||
this.projectId = projectid;
|
||||
this.app = (await this.projectService.GetApplicationById(projectid, id)).toObject();
|
||||
this.appNameForm.patchValue(this.app);
|
||||
if (this.app.state !== AppState.APPSTATE_ACTIVE) {
|
||||
this.appNameForm.controls['name'].disable();
|
||||
this.appForm.disable();
|
||||
} else {
|
||||
this.appNameForm.controls['name'].enable();
|
||||
this.appForm.enable();
|
||||
this.clientId?.disable();
|
||||
}
|
||||
if (this.app.oidcConfig?.redirectUrisList) {
|
||||
this.redirectUrisList = this.app.oidcConfig.redirectUrisList;
|
||||
}
|
||||
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
|
||||
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
|
||||
}
|
||||
if (this.app.oidcConfig) {
|
||||
this.appForm.patchValue(this.app.oidcConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public changeState(event: MatButtonToggleChange): void {
|
||||
if (event.value === AppState.APPSTATE_ACTIVE) {
|
||||
this.projectService.ReactivateApplication(this.app.id).then(() => {
|
||||
this.toast.showInfo('Reactivated Application');
|
||||
}).catch((error: any) => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
} else if (event.value === AppState.APPSTATE_INACTIVE) {
|
||||
this.projectService.DectivateApplication(this.app.id).then(() => {
|
||||
this.toast.showInfo('Deactivated Application');
|
||||
}).catch((error: any) => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.value !== AppState.APPSTATE_ACTIVE) {
|
||||
this.appNameForm.controls['name'].disable();
|
||||
this.appForm.disable();
|
||||
} else {
|
||||
this.appNameForm.controls['name'].enable();
|
||||
this.appForm.enable();
|
||||
this.clientId?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public add(event: MatChipInputEvent, target: RedirectType): void {
|
||||
if (target === RedirectType.POSTREDIRECT) {
|
||||
const input = event.input;
|
||||
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
|
||||
this.postLogoutRedirectUrisList.push(event.value);
|
||||
}
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
} else if (target === RedirectType.REDIRECT) {
|
||||
const input = event.input;
|
||||
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
|
||||
this.redirectUrisList.push(event.value);
|
||||
}
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public remove(redirect: any, target: RedirectType): void {
|
||||
|
||||
if (target === RedirectType.POSTREDIRECT) {
|
||||
const index = this.postLogoutRedirectUrisList.indexOf(redirect);
|
||||
|
||||
if (index >= 0) {
|
||||
this.postLogoutRedirectUrisList.splice(index, 1);
|
||||
}
|
||||
} else if (target === RedirectType.REDIRECT) {
|
||||
const index = this.redirectUrisList.indexOf(redirect);
|
||||
|
||||
if (index >= 0) {
|
||||
this.redirectUrisList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public saveOIDCApp(): void {
|
||||
if (this.appNameForm.valid) {
|
||||
this.app.name = this.name?.value;
|
||||
}
|
||||
|
||||
if (this.appForm.valid) {
|
||||
if (this.app.oidcConfig) {
|
||||
this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
|
||||
this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
|
||||
this.app.oidcConfig.applicationType = this.applicationType?.value;
|
||||
this.app.oidcConfig.authMethodType = this.authMethodType?.value;
|
||||
this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
|
||||
this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
|
||||
|
||||
console.log(this.app.oidcConfig);
|
||||
|
||||
this.projectService
|
||||
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
|
||||
.then((data: OIDCConfig) => {
|
||||
this.toast.showInfo('OIDC Config saved');
|
||||
})
|
||||
.catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public regenerateOIDCClientSecret(): void {
|
||||
this.projectService.RegenerateOIDCClientSecret(this.app.id).then((data: OIDCConfig) => {
|
||||
console.log(data.toObject());
|
||||
this.toast.showInfo('OIDC Secret Regenerated');
|
||||
this.dialog.open(AppSecretDialogComponent, {
|
||||
data: {
|
||||
clientId: data.toObject().clientId,
|
||||
clientSecret: data.toObject().clientSecret,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public get name(): AbstractControl | null {
|
||||
return this.appNameForm.get('name');
|
||||
}
|
||||
|
||||
public get clientId(): AbstractControl | null {
|
||||
return this.appForm.get('clientId');
|
||||
}
|
||||
|
||||
public get responseTypesList(): AbstractControl | null {
|
||||
return this.appForm.get('responseTypesList');
|
||||
}
|
||||
|
||||
public get grantTypesList(): AbstractControl | null {
|
||||
return this.appForm.get('grantTypesList');
|
||||
}
|
||||
|
||||
public get applicationType(): AbstractControl | null {
|
||||
return this.appForm.get('applicationType');
|
||||
}
|
||||
|
||||
public get authMethodType(): AbstractControl | null {
|
||||
return this.appForm.get('authMethodType');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
|
||||
</h1>
|
||||
<p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
|
||||
<div mat-dialog-content>
|
||||
<p *ngIf="data.clientId">ClientId: {{data.clientId}}</p>
|
||||
|
||||
<div *ngIf="data.clientSecret" class="flex">
|
||||
<button matTooltip="copy to clipboard" (click)="copytoclipboard(data.clientSecret)" mat-icon-button>
|
||||
<mat-icon *ngIf="!copied" svgIcon="mdi_content_copy"></mat-icon>
|
||||
<mat-icon *ngIf="copied">check</mat-icon>
|
||||
|
||||
</button>
|
||||
<span class="secret">{{data.clientSecret}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
|
||||
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
|
||||
{{'ACTIONS.CLOSE' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #81868a;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.ok-button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
border: 1px solid #ffffff20;
|
||||
border-radius: .5rem;
|
||||
|
||||
.secret {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppSecretDialogComponent } from './app-secret-dialog.component';
|
||||
|
||||
describe('AppSecretDialogComponent', () => {
|
||||
let component: AppSecretDialogComponent;
|
||||
let fixture: ComponentFixture<AppSecretDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppSecretDialogComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppSecretDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-secret-dialog',
|
||||
templateUrl: './app-secret-dialog.component.html',
|
||||
styleUrls: ['./app-secret-dialog.component.scss'],
|
||||
})
|
||||
export class AppSecretDialogComponent {
|
||||
public copied: boolean = false;
|
||||
constructor(public dialogRef: MatDialogRef<AppSecretDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
public copytoclipboard(value: string): void {
|
||||
const selBox = document.createElement('textarea');
|
||||
selBox.style.position = 'fixed';
|
||||
selBox.style.left = '0';
|
||||
selBox.style.top = '0';
|
||||
selBox.style.opacity = '0';
|
||||
selBox.value = value;
|
||||
document.body.appendChild(selBox);
|
||||
selBox.focus();
|
||||
selBox.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(selBox);
|
||||
this.copied = true;
|
||||
setTimeout(() => {
|
||||
this.copied = false;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AppCreateComponent } from '../apps/app-create/app-create.component';
|
||||
import { AppDetailComponent } from '../apps/app-detail/app-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'create',
|
||||
component: AppCreateComponent,
|
||||
data: { animation: 'AddPage' },
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: AppDetailComponent,
|
||||
data: { animation: 'HomePage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppsRoutingModule { }
|
||||
@@ -0,0 +1,69 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, 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 { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
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 { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
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 { AppCreateComponent } from './app-create/app-create.component';
|
||||
import { AppDetailComponent } from './app-detail/app-detail.component';
|
||||
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
|
||||
import { AppsRoutingModule } from './apps-routing.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppCreateComponent,
|
||||
AppDetailComponent,
|
||||
AppSecretDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppsRoutingModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
ReactiveFormsModule,
|
||||
HasRoleModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatSelectModule,
|
||||
MatButtonToggleModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatProgressBarModule,
|
||||
MatDialogModule,
|
||||
MatCheckboxModule,
|
||||
CardModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule.forChild({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
],
|
||||
entryComponents: [
|
||||
AppSecretDialogComponent,
|
||||
],
|
||||
exports: [TranslateModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
|
||||
})
|
||||
export class AppsModule { }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponent,
|
||||
data: { animation: 'HomePage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class HomeRoutingModule { }
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<img *ngIf="dark; else lighttheme" src="../assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<ng-template #lighttheme>
|
||||
<img src="../assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
</ng-template>
|
||||
<p>{{'HOME.DESCRIPTION' | translate}}</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="item">
|
||||
<div class="top">
|
||||
<h2>{{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
|
||||
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="footer">
|
||||
<a color="accent" mat-button
|
||||
[routerLink]="['/users/me']">{{'HOME.SECURITYANDPRIVACY_BUTTON' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top">
|
||||
<h2>{{'HOME.PROJECTS'| translate}}</h2>
|
||||
<p>{{'HOME.PROJECTS_DESC'| translate}}</p>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="footer">
|
||||
<a color="accent" mat-button [routerLink]="['/users/me']">{{'HOME.PROJECTS_BUTTON' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top">
|
||||
<h2>{{'HOME.PROTECTION'| translate}}</h2>
|
||||
<p>{{'HOME.PROTECTION_DESC'| translate}}</p>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="footer">
|
||||
<a color="accent" mat-button [routerLink]="['/users/me']">{{'HOME.PROTECTION_BUTTON' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top">
|
||||
<h2>{{'HOME.USERS'| translate}}</h2>
|
||||
<p>{{'HOME.USERS_DESC'| translate}}</p>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="footer">
|
||||
<a color="accent" mat-button [routerLink]="['/users/me']">{{'HOME.USERS_BUTTON' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
.circle {
|
||||
position: relative;
|
||||
border-radius: 100%;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
background: linear-gradient(40deg, #8983F7, #A3DAFB 70%);
|
||||
margin: auto;
|
||||
}
|
||||
.crescent {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
right: 0;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
background: #212224;
|
||||
transform: scale(1);
|
||||
transform-origin: top right;
|
||||
transition: transform .6s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 80%;
|
||||
max-width: 1350px;
|
||||
@media only screen and (max-width: 700px) {
|
||||
width: 90%;
|
||||
}
|
||||
margin: auto;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 4rem 0;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
font-size: 100px;
|
||||
line-height: 100px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-family: ailerons;
|
||||
}
|
||||
p {
|
||||
color: #81868a;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 90px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -1rem;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.item {
|
||||
flex: 1 1 45%;
|
||||
box-sizing: border-box;
|
||||
margin: 1rem;
|
||||
border: 1px solid #ffffff20;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top {
|
||||
padding: 1rem 2rem;
|
||||
h2 {
|
||||
display: block;
|
||||
margin-top: .5rem;
|
||||
font-family: 'Rubik';
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
color: #81868a;
|
||||
font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
// position: absolute;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
border-top: 1px solid #ffffff20;
|
||||
|
||||
a {
|
||||
// text-decoration: none;
|
||||
// color: #e8eaed;
|
||||
// font-size: .8rem;
|
||||
// font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [HomeComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
})
|
||||
export class HomeComponent {
|
||||
public dark: boolean = true;
|
||||
|
||||
constructor() {
|
||||
const theme = localStorage.getItem('theme');
|
||||
this.dark = theme === 'dark-theme' ? true : theme === 'light-theme' ? false : true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { HttpLoaderFactory } from 'src/app/app.module';
|
||||
|
||||
import { HomeRoutingModule } from './home-routing.module';
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
HomeRoutingModule,
|
||||
MatButtonModule,
|
||||
TranslateModule.forChild({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class HomeModule { }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+33
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+32
@@ -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 { }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
+27
@@ -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();
|
||||
});
|
||||
});
|
||||
+100
@@ -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 { }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ProjectCreateComponent } from './project-create.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectCreateComponent,
|
||||
data: { animation: 'DetailPage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProjectCreateRoutingModule { }
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="container">
|
||||
<div class="abort-container">
|
||||
<button (click)="close()" mat-icon-button>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
|
||||
{{ currentCreateStep }} of
|
||||
{{ createSteps }}</span>
|
||||
</div>
|
||||
|
||||
<h1>{{'PROJECT.PAGES.CREATE_DESC' | translate}}</h1>
|
||||
<form (ngSubmit)="saveProject()">
|
||||
<div class="column">
|
||||
<mat-form-field class="formfield" hintLabel="The name is required!">
|
||||
<mat-label>{{'PROJECT.NAME' | translate}}</mat-label>
|
||||
<input matInput autofocus [(ngModel)]="project.name" [ngModelOptions]="{ standalone: true }" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<button color="primary" mat-raised-button class="continue-button" [disabled]="!project.name" cdkFocusInitial
|
||||
type="submit">
|
||||
{{'CONTINUE' | translate}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.formfield {
|
||||
width: 400px;
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.autocomplete {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.access-token {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 { ProjectCreateComponent } from './project-create.component';
|
||||
|
||||
describe('ProjectCreateComponent', () => {
|
||||
let component: ProjectCreateComponent;
|
||||
let fixture: ComponentFixture<ProjectCreateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectCreateComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectCreateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Project, ProjectCreateRequest } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-create',
|
||||
templateUrl: './project-create.component.html',
|
||||
styleUrls: ['./project-create.component.scss'],
|
||||
})
|
||||
export class ProjectCreateComponent implements OnInit {
|
||||
public project: ProjectCreateRequest.AsObject = new ProjectCreateRequest().toObject();
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private _location: Location,
|
||||
) { }
|
||||
|
||||
public createSteps: number = 1;
|
||||
public currentCreateStep: number = 1;
|
||||
public ngOnInit(): void { }
|
||||
|
||||
public saveProject(): void {
|
||||
console.log(this.project);
|
||||
this.projectService
|
||||
.CreateProject(this.project)
|
||||
.then((data: Project) => {
|
||||
this.router.navigate(['projects', data.getId()]);
|
||||
})
|
||||
.catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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 { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { HttpLoaderFactory } from 'src/app/app.module';
|
||||
|
||||
import { ProjectCreateRoutingModule } from './project-create-routing.module';
|
||||
import { ProjectCreateComponent } from './project-create.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectCreateComponent],
|
||||
imports: [
|
||||
ProjectCreateRoutingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule.forChild({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ProjectCreateModule { }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ProjectGrantCreateComponent } from './project-grant-create.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectGrantCreateComponent,
|
||||
data: { animation: 'AddPage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProjectGrantCreateRoutingModule { }
|
||||
@@ -0,0 +1,59 @@
|
||||
<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">{{ 'PROJECT.GRANT.CREATE.TITLE' | translate }}</span><span class="abort-2">Step
|
||||
{{ currentCreateStep }} of
|
||||
{{ STEPS }}</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="currentCreateStep === 1">
|
||||
<h1>{{'PROJECT.GRANT.CREATE.SEL_ORG' | translate}}</h1>
|
||||
|
||||
<form (ngSubmit)="searchOrg(domain)">
|
||||
<mat-form-field class="org-domain">
|
||||
<mat-label>{{'PROJECT.GRANT.CREATE.SEL_ORG' | translate}}</mat-label>
|
||||
<input #domain matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<button [disabled]="domain.value.length == 0" color="primary" type="submit" class="domain-button"
|
||||
mat-raised-button>
|
||||
{{'PROJECT.GRANT.CREATE.SEL_ORG_BUTTON' | translate}}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="card org" *ngIf="org">
|
||||
<p>{{org?.name}}</p>
|
||||
<span *ngIf="org.creationDate">created:
|
||||
{{dateFromTimestamp(org.creationDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentCreateStep === 2">
|
||||
<h1>{{'PROJECT.GRANT.CREATE.SEL_ROLES' | translate}}</h1>
|
||||
|
||||
<app-card *ngIf="projectId">
|
||||
<app-project-roles (changedSelection)="selectRoles($event)" [projectId]="projectId"></app-project-roles>
|
||||
</app-card>
|
||||
</ng-container>
|
||||
|
||||
<div class="btn-container">
|
||||
<ng-container *ngIf="currentCreateStep === 1">
|
||||
<button [disabled]="!org" (click)="next()" color="primary" mat-raised-button class="big-button"
|
||||
cdkFocusInitial>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentCreateStep === STEPS">
|
||||
<button (click)="previous()" color="primary" mat-button class="small-button">
|
||||
{{ 'ACTIONS.BACK' | translate }}
|
||||
</button>
|
||||
<button color="primary" [disabled]="!org || rolesKeyList.length == 0" (click)="addGrant()" mat-raised-button
|
||||
class="big-button" cdkFocusInitial>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,113 @@
|
||||
h1 {
|
||||
font-family: ailerons;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 4rem 4rem 2rem 4rem;
|
||||
|
||||
.abort-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
white-space: nowrap;
|
||||
|
||||
.abort {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.abort-2 {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.org-domain {
|
||||
width: 400px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.domain-button {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.org {
|
||||
height: 120px;
|
||||
// width: 120px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.formfield {
|
||||
width: 400px;
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.autocomplete {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.access-token {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
margin: 0 -.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 0 40%;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
|
||||
.small-button {
|
||||
display: block;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.big-button {
|
||||
display: block;
|
||||
padding: 0.5rem 4rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectGrantCreateComponent } from './project-grant-create.component';
|
||||
|
||||
describe('GrantCreateComponent', () => {
|
||||
let component: ProjectGrantCreateComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantCreateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantCreateComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantCreateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Org, ProjectRole } from 'src/app/proto/generated/management_pb';
|
||||
import { AuthService } from 'src/app/services/auth.service';
|
||||
import { OrgService } from 'src/app/services/org.service';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-grant-create',
|
||||
templateUrl: './project-grant-create.component.html',
|
||||
styleUrls: ['./project-grant-create.component.scss'],
|
||||
})
|
||||
export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
|
||||
public org!: Org.AsObject;
|
||||
public projectId: string = '';
|
||||
public grantId: string = '';
|
||||
public rolesKeyList: string[] = [];
|
||||
|
||||
public STEPS: number = 2;
|
||||
public currentCreateStep: number = 1;
|
||||
|
||||
private routeSubscription: Subscription = new Subscription();
|
||||
constructor(
|
||||
private orgService: OrgService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private authService: AuthService,
|
||||
private _location: Location,
|
||||
) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.routeSubscription = this.route.params.subscribe(params => {
|
||||
this.projectId = params.projectid;
|
||||
console.log(params);
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.routeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
public searchOrg(domain: any): void {
|
||||
this.orgService.getOrgByDomainGlobal(domain.value).then((ret) => {
|
||||
console.log(ret.toObject());
|
||||
const tmp = ret.toObject();
|
||||
this.authService.GetActiveOrg().then((org) => {
|
||||
if (tmp !== org) {
|
||||
this.org = tmp;
|
||||
}
|
||||
});
|
||||
this.org = ret.toObject();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public addGrant(): void {
|
||||
this.projectService
|
||||
.CreateProjectGrant(this.org.id, this.projectId, this.rolesKeyList)
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
this.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public dateFromTimestamp(date: Timestamp.AsObject): any {
|
||||
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
|
||||
return ts;
|
||||
}
|
||||
|
||||
public selectRoles(roles: ProjectRole.AsObject[]): void {
|
||||
this.rolesKeyList = roles.map(role => role.key);
|
||||
}
|
||||
|
||||
public next(): void {
|
||||
this.currentCreateStep++;
|
||||
}
|
||||
|
||||
public previous(): void {
|
||||
this.currentCreateStep--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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 { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
|
||||
import { ProjectRolesModule } from '../../modules/project-roles/project-roles.module';
|
||||
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
|
||||
import { ProjectGrantCreateComponent } from './project-grant-create.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectGrantCreateComponent],
|
||||
imports: [
|
||||
ProjectGrantCreateRoutingModule,
|
||||
CommonModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
CardModule,
|
||||
MatCheckboxModule,
|
||||
ProjectRolesModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class ProjectGrantCreateModule { }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ProjectRoleCreateComponent } from './project-role-create.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectRoleCreateComponent,
|
||||
data: { animation: 'DetailPage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProjectRoleCreateRoutingModule { }
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="container">
|
||||
<div class="abort-container">
|
||||
<button (click)="close()" mat-icon-button>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<span class="abort">{{ 'PROJECT.ROLE.ADDTITLE' | translate }}</span><span class="abort-2">Step
|
||||
{{ currentCreateStep }} of
|
||||
{{ createSteps }}</span>
|
||||
</div>
|
||||
|
||||
<h1>{{'PROJECT.ROLE.ADDDESCRIPTION' | translate}}</h1>
|
||||
<button class="add-line-btn" color="primary" mat-stroked-button (click)="addEntry()">
|
||||
Add another role
|
||||
</button>
|
||||
<form @list (ngSubmit)="addRole()">
|
||||
<div @animate *ngFor="let formGroup of formArray.controls; index as i" class="content">
|
||||
<ng-container [formGroup]="formGroup">
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'PROJECT.ROLE.NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
<!-- <mat-error *ngIf="name?.errors?.required">{{'ERRORS.REQUIRED' | translate}}</mat-error> -->
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="displayName" />
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'PROJECT.ROLE.GROUP' | translate }}</mat-label>
|
||||
<input matInput formControlName="group" />
|
||||
</mat-form-field>
|
||||
<button mat-icon-button (click)="removeEntry(i)" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}">
|
||||
<mat-icon>remove_circle</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button color="primary" mat-raised-button class="continue-button" [disabled]="formArray.invalid" cdkFocusInitial
|
||||
type="submit">
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.add-line-btn {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
margin: 0 -.5rem;
|
||||
flex-wrap: wrap;
|
||||
// align-items: center;
|
||||
|
||||
mat-form-field {
|
||||
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 { ProjectRoleCreateComponent } from './project-role-create.component';
|
||||
|
||||
describe('ProjectRoleCreateComponent', () => {
|
||||
let component: ProjectRoleCreateComponent;
|
||||
let fixture: ComponentFixture<ProjectRoleCreateComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectRoleCreateComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectRoleCreateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ProjectRoleAdd } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-role-create',
|
||||
templateUrl: './project-role-create.component.html',
|
||||
styleUrls: ['./project-role-create.component.scss'],
|
||||
animations: [
|
||||
trigger('list', [
|
||||
transition(':enter', [
|
||||
query('@animate',
|
||||
stagger(80, animateChild()),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
trigger('animate', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0, transform: 'translateY(-100%)' }),
|
||||
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({ opacity: 1, transform: 'translateY(0)' }),
|
||||
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
|
||||
private subscription?: Subscription;
|
||||
public projectId: string = '';
|
||||
|
||||
public formArray!: FormArray;
|
||||
public formGroup!: FormGroup;
|
||||
public createSteps: number = 1;
|
||||
public currentCreateStep: number = 1;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private _location: Location,
|
||||
private fb: FormBuilder,
|
||||
) {
|
||||
this.formGroup = new FormGroup({
|
||||
name: new FormControl(''),
|
||||
displayName: new FormControl(''),
|
||||
group: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
this.formArray = new FormArray([this.formGroup]);
|
||||
|
||||
}
|
||||
|
||||
public addEntry(): void {
|
||||
const newGroup = new FormGroup({
|
||||
name: new FormControl(''),
|
||||
displayName: new FormControl(''),
|
||||
group: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
this.formArray.push(newGroup);
|
||||
}
|
||||
|
||||
public removeEntry(index: number): void {
|
||||
this.formArray.removeAt(index);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private getData({ projectid }: Params): void {
|
||||
this.projectId = projectid;
|
||||
}
|
||||
|
||||
public addRole(): void {
|
||||
Promise.all(this.formArray.value.map((role: ProjectRoleAdd.AsObject) => {
|
||||
role.id = this.projectId;
|
||||
console.log(role);
|
||||
return this.projectService.AddProjectRole(role);
|
||||
})).then(() => {
|
||||
this.router.navigate(['projects', this.projectId]);
|
||||
}).catch(data => {
|
||||
console.log(data);
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { ProjectRoleCreateRoutingModule } from './project-role-create-routing.module';
|
||||
import { ProjectRoleCreateComponent } from './project-role-create.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectRoleCreateComponent],
|
||||
imports: [
|
||||
ProjectRoleCreateRoutingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class ProjectRoleCreateModule { }
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<div class="header">
|
||||
<h3>{{'APP.LIST' | translate}}</h3>
|
||||
<span class="fill-space"></span>
|
||||
<button mat-icon-button (click)="closeView()">
|
||||
<mat-icon matTooltip="show list view">list</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="app-container">
|
||||
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap"
|
||||
*ngFor="let app of appsSubject | async">
|
||||
<div class="morph-card">
|
||||
{{ app.name.charAt(0)}}
|
||||
</div>
|
||||
<span class="name">{{app.name}}</span>
|
||||
</div>
|
||||
<ng-template appHasRole [appHasRole]="['project.app.write']">
|
||||
<div class="app-wrap" *ngIf="!disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']">
|
||||
<div class="morph-card add">
|
||||
<mat-icon>add</mat-icon>
|
||||
</div>
|
||||
<span class="name">{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: 'Rubik';
|
||||
}
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -1rem;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.app-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 150px;
|
||||
|
||||
.morph-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: .8rem;
|
||||
color: #8a868a;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectApplicationGridComponent } from './project-application-grid.component';
|
||||
|
||||
describe('AppGridComponent', () => {
|
||||
let component: ProjectApplicationGridComponent;
|
||||
let fixture: ComponentFixture<ProjectApplicationGridComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectApplicationGridComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectApplicationGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-application-grid',
|
||||
templateUrl: './project-application-grid.component.html',
|
||||
styleUrls: ['./project-application-grid.component.scss'],
|
||||
})
|
||||
export class ProjectApplicationGridComponent implements OnInit {
|
||||
@Input() public projectId: string = '';
|
||||
@Input() public disabled: boolean = false;
|
||||
@Output() public changeView: EventEmitter<void> = new EventEmitter();
|
||||
public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.loadApps();
|
||||
}
|
||||
|
||||
public loadApps(): void {
|
||||
from(this.projectService.SearchApplications(this.projectId, 100, 0)).pipe(
|
||||
map(resp => {
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe((apps) => {
|
||||
this.appsSubject.next(apps as Application.AsObject[]);
|
||||
});
|
||||
}
|
||||
|
||||
public closeView(): void {
|
||||
this.changeView.emit();
|
||||
}
|
||||
}
|
||||
+56
@@ -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 { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
/**
|
||||
* Data source for the ProjectMembers view. This class should
|
||||
* encapsulate all logic for fetching and manipulating the displayed data
|
||||
* (including sorting, pagination, and filtering).
|
||||
*/
|
||||
export class ProjectApplicationsDataSource extends DataSource<Application.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public loadApps(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
|
||||
const offset = pageIndex * pageSize;
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
from(this.projectService.SearchApplications(projectId, pageSize, offset)).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(apps => {
|
||||
this.appsSubject.next(apps);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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<Application.AsObject[]> {
|
||||
return this.appsSubject.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.appsSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<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?.appsSubject.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>
|
||||
<!-- <button [disabled]="disabled" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button"
|
||||
mat-icon-button *ngIf="selection.hasValue()">
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button> -->
|
||||
<ng-template appHasRole [appHasRole]="['project.app.write']">
|
||||
<a [disabled]="disabled" class="add-button" [routerLink]="[ '/projects', projectId, 'apps', 'create']"
|
||||
color="primary" mat-raised-button>
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table [dataSource]="dataSource" mat-table class="full-width-table" matSort aria-label="Elements">
|
||||
<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="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.NAME' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', role.id ]" mat-cell
|
||||
*matCellDef="let role">
|
||||
{{role.name}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator #paginator [length]="dataSource.totalResult" [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
|
||||
.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%;
|
||||
background-color: #2d2e30;
|
||||
|
||||
td, th {
|
||||
&: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;
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
+34
@@ -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 { ProjectApplicationsComponent } from './project-applications.component';
|
||||
|
||||
describe('ProjectApplicationsComponent', () => {
|
||||
let component: ProjectApplicationsComponent;
|
||||
let fixture: ComponentFixture<ProjectApplicationsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectApplicationsComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectApplicationsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable } from '@angular/material/table';
|
||||
import { merge } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { ProjectApplicationsDataSource } from './project-applications-datasource';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-applications',
|
||||
templateUrl: './project-applications.component.html',
|
||||
styleUrls: ['./project-applications.component.scss'],
|
||||
})
|
||||
export class ProjectApplicationsComponent implements AfterViewInit, OnInit {
|
||||
@Input() public projectId: string = '';
|
||||
@Input() public disabled: boolean = false;
|
||||
@ViewChild(MatPaginator) public paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
@ViewChild(MatTable) public table!: MatTable<Application.AsObject>;
|
||||
public dataSource!: ProjectApplicationsDataSource;
|
||||
public selection: SelectionModel<Application.AsObject> = new SelectionModel<Application.AsObject>(true, []);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
public displayedColumns: string[] = ['select', 'name'];
|
||||
|
||||
constructor(private projectService: ProjectService, private toast: ToastService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.dataSource = new ProjectApplicationsDataSource(this.projectService);
|
||||
this.dataSource.loadApps(this.projectId, 0, 25, 'asc');
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
|
||||
|
||||
merge(this.sort.sortChange, this.paginator.page)
|
||||
.pipe(
|
||||
tap(() => this.loadRolesPage()),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
}
|
||||
|
||||
private loadRolesPage(): void {
|
||||
this.dataSource.loadApps(
|
||||
this.projectId,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize,
|
||||
this.sort.direction,
|
||||
);
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.dataSource.appsSubject.value.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
public masterToggle(): void {
|
||||
this.isAllSelected() ?
|
||||
this.selection.clear() :
|
||||
this.dataSource.appsSubject.value.forEach((row: Application.AsObject) => this.selection.select(row));
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<div class="groups">
|
||||
<span class="header">{{ 'PROJECT.MEMBER.TITLE' | translate }}</span>
|
||||
<span class="sub-header">{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}</span>
|
||||
<div class="people">
|
||||
<div class="img-list">
|
||||
<ng-container *ngIf="totalResult < 10; else compact">
|
||||
<ng-container *ngFor="let member of membersSubject | async">
|
||||
<img (click)="showDetail()" *ngIf="member.imageURL; else render" class="avatar-img"
|
||||
[src]="member.imageURL" matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
|
||||
alt="editor avatar" />
|
||||
<ng-template #render>
|
||||
<div (click)="showDetail()" class="avatar-circle"
|
||||
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
|
||||
<mat-icon>face</mat-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #compact>
|
||||
<div (click)="showDetail()" class="avatar-circle" matTooltip="Click to show detail">
|
||||
<span>{{totalResult}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<button class="add-img" (click)="openAddMember()"
|
||||
[disabled]="project?.state !== ProjectState.ACTIVE_PROJECT" mat-icon-button
|
||||
aria-label="Edit contributors">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
.groups {
|
||||
padding-top: 1rem;
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 400;
|
||||
color: #81868a;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
font-size: .8rem;
|
||||
color: #81868a;
|
||||
}
|
||||
|
||||
.people {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.owner {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.img-list {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar-img, .avatar-circle {
|
||||
float: left;
|
||||
margin: 0 8px 0 -15px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.5), 0 3px 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.add-img {
|
||||
float: left;
|
||||
margin: 0 8px 0 -15px;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: indianred;
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectContributorsComponent } from './project-contributors.component';
|
||||
|
||||
describe('ProjectContributorsComponent', () => {
|
||||
let component: ProjectContributorsComponent;
|
||||
let fixture: ComponentFixture<ProjectContributorsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectContributorsComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectContributorsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { BehaviorSubject, from, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { User } from 'src/app/proto/generated/auth_pb';
|
||||
import {
|
||||
Project,
|
||||
ProjectMember,
|
||||
ProjectMemberSearchResponse,
|
||||
ProjectState,
|
||||
ProjectType,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import {
|
||||
CreationType,
|
||||
ProjectMemberCreateDialogComponent,
|
||||
} from '../../../modules/add-member-dialog/project-member-create-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-contributors',
|
||||
templateUrl: './project-contributors.component.html',
|
||||
styleUrls: ['./project-contributors.component.scss'],
|
||||
})
|
||||
export class ProjectContributorsComponent implements OnInit {
|
||||
@Input() public project!: Project.AsObject;
|
||||
@Input() public disabled: boolean = false;
|
||||
|
||||
public totalResult: number = 0;
|
||||
public membersSubject: BehaviorSubject<ProjectMember.AsObject[]> = new BehaviorSubject<ProjectMember.AsObject[]>([]);
|
||||
public ProjectState: any = ProjectState;
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
constructor(private projectService: ProjectService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
private router: Router) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
const promise: Promise<ProjectMemberSearchResponse> | undefined =
|
||||
this.project.type === ProjectType.PROJECTTYPE_SELF ?
|
||||
this.projectService.SearchProjectMembers(this.project.id, 100, 0) :
|
||||
this.project.type === ProjectType.PROJECTTYPE_GRANTED ?
|
||||
this.projectService.SearchProjectGrantMembers(this.project.id, this.project.grantId, 100, 0) : undefined;
|
||||
if (promise) {
|
||||
from(promise).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(members => {
|
||||
console.log(members);
|
||||
this.membersSubject.next(members);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public openAddMember(): void {
|
||||
const dialogRef = this.dialog.open(ProjectMemberCreateDialogComponent, {
|
||||
data: {
|
||||
creationType: this.project.type ===
|
||||
ProjectType.PROJECTTYPE_GRANTED ? CreationType.PROJECT_GRANTED :
|
||||
ProjectType.PROJECTTYPE_SELF ? CreationType.PROJECT_OWNED : undefined,
|
||||
projectId: this.project.id,
|
||||
},
|
||||
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.projectService.AddProjectMember(this.project.id, user.id, roles);
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public showDetail(): void {
|
||||
if (this.project?.state === ProjectState.PROJECTSTATE_ACTIVE) {
|
||||
this.router.navigate(['projects', this.project.id, 'members']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<app-meta-layout>
|
||||
<div class="max-width-container">
|
||||
<div class="head" *ngIf="project?.id">
|
||||
<a [routerLink]="[ '/projects' ]" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
|
||||
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
|
||||
<button *ngIf="project?.type == ProjectType.PROJECTTYPE_SELF" mat-icon-button
|
||||
(click)="editstate = !editstate" aria-label="Edit project name">
|
||||
<mat-icon *ngIf="!editstate">edit</mat-icon>
|
||||
<mat-icon *ngIf="editstate">close</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<div class="full-width">
|
||||
<ng-container *ngIf="editstate">
|
||||
<mat-form-field *ngIf="editstate && project?.name" class="formfield"
|
||||
hintLabel="The name is required!">
|
||||
<mat-label>Project Name</mat-label>
|
||||
<input matInput [(ngModel)]="project.name" />
|
||||
</mat-form-field>
|
||||
<button class="icon-button" *ngIf="editstate" mat-icon-button (click)="updateName()">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-stroked-button color="accent"
|
||||
*ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE" class="second"
|
||||
(click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' | translate}}</button>
|
||||
<button mat-stroked-button color="accent"
|
||||
*ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE" class="second"
|
||||
(click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' | translate}}</button>
|
||||
</ng-container>
|
||||
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- show only on owned projects-->
|
||||
<ng-container *ngIf="project?.type === ProjectType.PROJECTTYPE_SELF; else granteddetail">
|
||||
<ng-template appHasRole [appHasRole]="['project.app.read:' + project.id, 'project.app.read']">
|
||||
<app-project-application-grid *ngIf="grid"
|
||||
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" (changeView)="grid = false"
|
||||
[projectId]="projectId"></app-project-application-grid>
|
||||
<app-card *ngIf="!grid" title="{{ 'PROJECT.APP.TITLE' | translate }}">
|
||||
<card-actions class="card-actions">
|
||||
<button mat-icon-button (click)="grid = true">
|
||||
<mat-icon matTooltip="show grid view">grid_on</mat-icon>
|
||||
</button>
|
||||
</card-actions>
|
||||
<app-project-applications [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[projectId]="projectId"></app-project-applications>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="(isZitadel$ | async) == false">
|
||||
<ng-template appHasRole [appHasRole]="['project.grant.read:' + project.id, 'project.grant.read']">
|
||||
<app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}">
|
||||
<app-project-grants [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[projectId]="projectId">
|
||||
</app-project-grants>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-template appHasRole [appHasRole]="['project.role.read:' + project.id, 'project.role.read']">
|
||||
<app-card title="{{ 'PROJECT.ROLE.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}">
|
||||
<app-project-roles [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[actionsVisible]="project?.type == ProjectType.PROJECTTYPE_SELF" [projectId]="projectId">
|
||||
</app-project-roles>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #granteddetail>
|
||||
<app-card>
|
||||
<app-project-grant-members *ngIf="project"
|
||||
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [project]="project">
|
||||
</app-project-grant-members>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
<metainfo class="side">
|
||||
|
||||
<div class="details">
|
||||
<div class="row">
|
||||
<span class="first">{{'PROJECT.TYPE.TITLE' | translate}}:</span>
|
||||
<span class="second" *ngIf="project?.type">{{'PROJECT.TYPE.'+ project.type | translate}}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
|
||||
<span *ngIf="project && project.state !== undefined"
|
||||
class="second">{{'PROJECT.STATE.'+project.state | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="project?.type === ProjectType.PROJECTTYPE_SELF; else grantedmeta">
|
||||
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
|
||||
<mat-tab label="Details">
|
||||
<app-project-contributors *ngIf="project"
|
||||
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [project]="project">
|
||||
</app-project-contributors>
|
||||
</mat-tab>
|
||||
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="flex-col">
|
||||
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #grantedmeta>
|
||||
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
|
||||
</ng-template>
|
||||
</metainfo>
|
||||
</app-meta-layout>
|
||||
@@ -0,0 +1,117 @@
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0 1rem;
|
||||
margin-left: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
padding-top: 1rem;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
.icon-button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: .9rem;
|
||||
color: #81868a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -.5rem;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 1 33%;
|
||||
margin: 0 .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;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-tab-label {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectDetailComponent } from './project-detail.component';
|
||||
|
||||
describe('ProjectDetailComponent', () => {
|
||||
let component: ProjectDetailComponent;
|
||||
let fixture: ComponentFixture<ProjectDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectDetailComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { from, Observable, of, Subscription } from 'rxjs';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import {
|
||||
Application,
|
||||
ApplicationSearchResponse,
|
||||
Project,
|
||||
ProjectMember,
|
||||
ProjectMemberSearchResponse,
|
||||
ProjectRole,
|
||||
ProjectRoleSearchResponse,
|
||||
ProjectState,
|
||||
ProjectType,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { GrpcService } from 'src/app/services/grpc.service';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-detail',
|
||||
templateUrl: './project-detail.component.html',
|
||||
styleUrls: ['./project-detail.component.scss'],
|
||||
|
||||
})
|
||||
export class ProjectDetailComponent implements OnInit, OnDestroy {
|
||||
public projectId: string = '';
|
||||
public project!: Project.AsObject;
|
||||
|
||||
public pageSizeRoles: number = 10;
|
||||
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
|
||||
public roleResult!: ProjectRoleSearchResponse.AsObject;
|
||||
public roleColumns: string[] = ['name', 'displayname', 'group', 'actions'];
|
||||
|
||||
public pageSizeMembers: number = 10;
|
||||
public projectDataSource: MatTableDataSource<ProjectMember.AsObject> = new MatTableDataSource<ProjectMember.AsObject>();
|
||||
public memberResult!: ProjectMemberSearchResponse.AsObject;
|
||||
public memberColumns: string[] = ['firstname', 'lastname', 'username', 'email', 'roles'];
|
||||
public selection: SelectionModel<ProjectMember.AsObject> = new SelectionModel<ProjectMember.AsObject>(true, []);
|
||||
|
||||
public pageSizeApps: number = 10;
|
||||
public appsDataSource: MatTableDataSource<Application.AsObject> = new MatTableDataSource<Application.AsObject>();
|
||||
public appsResult!: ApplicationSearchResponse.AsObject;
|
||||
public appsColumns: string[] = ['name'];
|
||||
|
||||
public ProjectState: any = ProjectState;
|
||||
public ProjectType: any = ProjectType;
|
||||
public ChangeType: any = ChangeType;
|
||||
|
||||
public grid: boolean = true;
|
||||
private subscription?: Subscription;
|
||||
public editstate: boolean = false;
|
||||
|
||||
public isZitadel$: Observable<boolean> = of(false);
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private _location: Location,
|
||||
private grpcService: GrpcService,
|
||||
) {
|
||||
}
|
||||
|
||||
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.projectId = id;
|
||||
this.projectService.GetProjectById(id).then(proj => {
|
||||
this.project = proj.toObject();
|
||||
if (this.project.type !== ProjectType.PROJECTTYPE_SELF ||
|
||||
this.project.state === ProjectState.PROJECTSTATE_INACTIVE ||
|
||||
this.project.state === ProjectState.PROJECTSTATE_UNSPECIFIED) {
|
||||
}
|
||||
|
||||
this.isZitadel$ = from(this.projectService.SearchApplications(this.project.id, 100, 0).then(appsResp => {
|
||||
const ret = appsResp.toObject().resultList
|
||||
.filter(app => app.oidcConfig?.clientId === this.grpcService.clientid).length > 0;
|
||||
return ret;
|
||||
})); // TODO: replace with prettier thing
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public changeState(newState: ProjectState): void {
|
||||
if (newState === ProjectState.PROJECTSTATE_ACTIVE) {
|
||||
this.projectService.ReactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Reactivated Project');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
} else if (newState === ProjectState.PROJECTSTATE_INACTIVE) {
|
||||
this.projectService.DeactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Deactivated Project');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public saveProject(): void {
|
||||
this.projectService.UpdateProject(this.project).then(() => {
|
||||
this.toast.showInfo('Project updated');
|
||||
}).catch(error => {
|
||||
this.toast.showInfo(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public updateName(): void {
|
||||
this.saveProject();
|
||||
this.editstate = false;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||
</h1>
|
||||
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||
<div mat-dialog-content>
|
||||
<app-search-user-autocomplete (selectionChanged)="selectUsers($event)">
|
||||
</app-search-user-autocomplete>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="roleKeyList" multiple>
|
||||
<mat-option *ngFor="let key of data.roleKeysList" [value]="key">
|
||||
{{ key }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button mat-button (click)="closeDialog()">
|
||||
cancel
|
||||
</button>
|
||||
|
||||
<button [disabled]="userIds.length == 0 || roleKeyList.length == 0" color="primary" mat-raised-button
|
||||
class="ok-button" (click)="closeDialogWithSuccess()">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectGrantMembersCreateDialogComponent } from './project-grant-members-create-dialog.component';
|
||||
|
||||
describe('ProjectGrantMembersCreateDialogComponent', () => {
|
||||
let component: ProjectGrantMembersCreateDialogComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantMembersCreateDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantMembersCreateDialogComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantMembersCreateDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user