feat(console): angular 10, iam settings, timestamp to date pipe, org iam indicator, general gui fixes and i18n (#270)

* prettier member dialog, iam indicator

* changes i18n

* fix timestamp conversion, timestamp to date pipe

* rm create, update iam policy

* add iam policy

* add iam section, members component

* add iam contributors

* gen admin protos

* iam member search

* update angular

* update cdk material

* add module for iam members

* add iam roles to member dialog

* home shortcuts

* project view, i18n

* lint
This commit is contained in:
Max Peintner
2020-06-25 12:52:57 +02:00
committed by GitHub
parent d947bb1247
commit 785c8d9763
91 changed files with 7635 additions and 1631 deletions

View File

@@ -16,11 +16,11 @@
<div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
<i class="las la-toggle-off"></i>
<!-- <i class="las la-toggle-off"></i> -->
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
</mat-button-toggle>
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
<i class="las la-toggle-on"></i>
<!-- <i class="las la-toggle-on"></i> -->
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
</mat-button-toggle>
</mat-button-toggle-group>

View File

@@ -8,9 +8,25 @@
</div>
<div class="container">
<ng-template appHasRole [appHasRole]="['iam.write']">
<div class="item">
<div class="top">
<h2>
<i class="icon las la-gem"></i>
{{'HOME.IAM'| translate}}</h2>
<p>{{'HOME.IAM_DESC'| translate}}</p>
</div>
<span class="fill-space"></span>
<div class="footer">
<a color="accent" mat-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a>
</div>
</div>
</ng-template>
<div class="item">
<div class="top">
<h2>{{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<h2> <i class="icon las la-user-circle"></i>
{{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
</div>
<span class="fill-space"></span>
@@ -19,35 +35,50 @@
[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>
<ng-template appHasRole [appHasRole]="['project.read']">
<div class="item">
<div class="top">
<h2>
<i class="icon las la-layer-group"></i>
{{'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]="['/projects']">{{'HOME.PROJECTS_BUTTON' | translate}}</a>
</div>
</div>
<span class="fill-space"></span>
<div class="footer">
<a color="accent" mat-button [routerLink]="['/users/me']">{{'HOME.PROJECTS_BUTTON' | translate}}</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['org.read']">
<div class="item">
<div class="top">
<h2> <i class="icon las la-archway"></i>
{{'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>
<div class="item">
<div class="top">
<h2>{{'HOME.PROTECTION'| translate}}</h2>
<p>{{'HOME.PROTECTION_DESC'| translate}}</p>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.read']">
<div class="item">
<div class="top">
<h2>
<i class="las la-crosshairs"></i>
{{'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>
<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>
</ng-template>
</div>
</div>

View File

@@ -66,26 +66,42 @@
.item {
flex: 1 1 45%;
box-sizing: border-box;
// box-sizing: border-box;
margin: 1rem;
border: 1px solid #ffffff20;
border-radius: .5rem;
display: flex;
flex-direction: column;
flex-direction: column;
transition: border-color .1s;
&:hover {
i {
color: #fe11e280;
}
// border-width: 2px;
border-color: #fe11e270;
}
.top {
padding: 1rem 2rem;
h2 {
display: block;
margin-top: .5rem;
font-family: 'Rubik';
font-family: 'Rubik';
display: flex;
align-items: center;
}
p {
display: block;
color: #81868a;
font-size: .9rem;
}
}
i{
font-size: 2.5rem;
margin-right: 1rem;
transition: color .1s;
}
}
.fill-space {
@@ -93,22 +109,11 @@
}
.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;
}
}
}
}

View File

@@ -5,6 +5,7 @@ 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 { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HomeRoutingModule } from './home-routing.module';
import { HomeComponent } from './home.component';
@@ -12,19 +13,20 @@ import { HomeComponent } from './home.component';
@NgModule({
declarations: [HomeComponent],
imports: [
CommonModule,
MatIconModule,
HomeRoutingModule,
MatButtonModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
],
declarations: [HomeComponent],
imports: [
CommonModule,
MatIconModule,
HasRoleModule,
HomeRoutingModule,
MatButtonModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
],
})
export class HomeModule { }

View File

@@ -0,0 +1,24 @@
<div class="groups">
<span class="header">{{ 'IAM.MEMBER.TITLE' | translate }}</span>
<span class="sub-header">{{ 'IAM.MEMBER.DESCRIPTION' | translate }}</span>
<div class="people">
<div class="img-list">
<ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async">
<div (click)="showDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
<i class="avatar las la-user-circle"></i>
</div>
</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()" mat-icon-button aria-label="Edit contributors">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
.groups {
padding-top: 1rem;
.header {
display: block;
margin-bottom: 1rem;
font-weight: 400;
}
.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;
}
}
}
}

View File

@@ -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 { IamContributorsComponent } from './iam-contributors.component';
describe('OrgContributorsComponent', () => {
let component: IamContributorsComponent;
let fixture: ComponentFixture<IamContributorsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IamContributorsComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IamContributorsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,90 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table';
import { Router } from '@angular/router';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { OrgMember, OrgMemberView, OrgState, User } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import {
CreationType,
MemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/member-create-dialog.component';
@Component({
selector: 'app-iam-contributors',
templateUrl: './iam-contributors.component.html',
styleUrls: ['./iam-contributors.component.scss'],
})
export class IamContributorsComponent implements OnInit {
@Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<OrgMember.AsObject>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public totalResult: number = 0;
public membersSubject: BehaviorSubject<OrgMemberView.AsObject[]>
= new BehaviorSubject<OrgMemberView.AsObject[]>([]);
public OrgState: any = OrgState;
constructor(private adminService: AdminService, private dialog: MatDialog,
private toast: ToastService,
private router: Router) { }
public ngOnInit(): void {
this.loadMembers(0, 25, 'asc');
}
public loadMembers(pageIndex: number, pageSize: number, sortDirection?: string): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.adminService.SearchIamMembers(pageSize, offset)).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(MemberCreateDialogComponent, {
data: {
creationType: CreationType.IAM,
},
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.adminService.AddIamMember(user.id, roles);
})).then(() => {
this.toast.showError('members added');
}).catch(error => {
this.toast.showError(error.message);
});
}
}
});
}
public showDetail(): void {
this.router.navigate(['iam/members']);
}
}

View File

@@ -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 { MemberCreateDialogModule } from '../../../modules/add-member-dialog/member-create-dialog.module';
import { IamContributorsComponent } from './iam-contributors.component';
@NgModule({
declarations: [IamContributorsComponent],
imports: [
CommonModule,
FormsModule,
MemberCreateDialogModule,
HasRoleModule,
MatButtonModule,
MatDialogModule,
MatTableModule,
MatPaginatorModule,
MatIconModule,
RouterModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatTooltipModule,
TranslateModule,
],
exports: [
IamContributorsComponent,
],
})
export class IamContributorsModule { }

View File

@@ -0,0 +1,64 @@
import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { IamMemberSearchResponse } from 'src/app/proto/generated/admin_pb';
import { ProjectMember } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.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 IamMembersDataSource extends DataSource<ProjectMember.AsObject> {
public totalResult: number = 0;
public membersSubject: BehaviorSubject<ProjectMember.AsObject[]> = new BehaviorSubject<ProjectMember.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(private adminService: AdminService) {
super();
}
public loadMembers(
pageIndex: number, pageSize: number, grantId?: string, sortDirection?: string): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
// TODO
const promise: Promise<IamMemberSearchResponse> =
this.adminService.SearchIamMembers(pageSize, offset);
if (promise) {
from(promise).pipe(
map(resp => {
this.totalResult = resp.toObject().totalResult;
console.log(this.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<ProjectMember.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();
}
}

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IamMembersComponent } from './iam-members.component';
const routes: Routes = [
{
path: '',
component: IamMembersComponent,
data: { animation: 'AddPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class IamMembersRoutingModule { }

View File

@@ -0,0 +1,95 @@
<div class="max-width-container">
<div class="container">
<div class="head">
<h1>{{ 'IAM.MEMBER.TITLE' | translate }}</h1>
<p class="desc">{{ 'IAM.MEMBER.DESCRIPTION' | translate }}</p>
</div>
<div class="table-header-row" *ngIf="org">
<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:'+org.id,'org.member.delete']">
<button (click)="removeProjectMemberSelection()" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()">
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['org.member.write:'+org.id,'org.member.write']">
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" 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 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> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" 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 class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator class="background-style" #paginator [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
</div>
</div>
</div>

View File

@@ -0,0 +1,106 @@
.container {
padding-bottom: 3rem;
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
margin-bottom: 2rem;
flex-wrap: wrap;
a {
display: block;
}
h1 {
font-size: 1.2rem;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #81868a;
}
}
}
.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: .5rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.action {
width: 40px;
}
.data-row {
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
.role {
display: inline-block;
margin: .25rem;
}
}
}
.pointer {
outline: none;
cursor: pointer;
}

View File

@@ -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 { ProjectMembersComponent } from './project-members.component';
describe('ProjectMembersComponent', () => {
let component: ProjectMembersComponent;
let fixture: ComponentFixture<ProjectMembersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectMembersComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectMembersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,109 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, 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 { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { Org, ProjectMember, ProjectType, User } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { IamMembersDataSource } from './iam-members-datasource';
@Component({
selector: 'app-iam-members',
templateUrl: './iam-members.component.html',
styleUrls: ['./iam-members.component.scss'],
})
export class IamMembersComponent implements AfterViewInit {
public org!: Org.AsObject;
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>;
public dataSource!: IamMembersDataSource;
public selection: SelectionModel<ProjectMember.AsObject> = new SelectionModel<ProjectMember.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 adminService: AdminService,
private dialog: MatDialog,
private toast: ToastService) {
this.dataSource = new IamMembersDataSource(this.adminService);
this.dataSource.loadMembers(0, 25, 'asc');
}
public ngAfterViewInit(): void {
this.paginator.page
.pipe(
tap(() => this.loadMembersPage()),
)
.subscribe();
}
private loadMembersPage(): void {
this.dataSource.loadMembers(
this.paginator.pageIndex,
this.paginator.pageSize,
);
}
public removeProjectMemberSelection(): void {
Promise.all(this.selection.selected.map(member => {
return this.adminService.RemoveIamMember(member.userId).then(() => {
this.toast.showInfo('Removed successfully');
}).catch(error => {
this.toast.showError(error.message);
});
}));
}
public removeMember(member: ProjectMember.AsObject): void {
this.adminService.RemoveIamMember(member.userId).then(() => {
this.toast.showInfo('Member removed successfully');
}).catch(error => {
this.toast.showError(error.message);
});
}
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 => this.selection.select(row));
}
public openAddMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
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.adminService.AddIamMember(user.id, roles);
})).then(() => {
this.toast.showError('members added');
}).catch(error => {
this.toast.showError(error.message);
});
}
}
});
}
}

View File

@@ -0,0 +1,40 @@
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 { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { IamMembersRoutingModule } from './iam-members-routing.module';
import { IamMembersComponent } from './iam-members.component';
@NgModule({
declarations: [IamMembersComponent],
imports: [
IamMembersRoutingModule,
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatTooltipModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
})
export class IamMembersModule { }

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IamComponent } from './iam.component';
const routes: Routes = [
{
path: '',
component: IamComponent,
},
{
path: 'members',
loadChildren: () => import('./iam-members/iam-members.module').then(m => m.IamMembersModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class IamRoutingModule { }

View File

@@ -0,0 +1,25 @@
<app-meta-layout>
<div class="enlarged-container">
<h1>{{'IAM.DETAIL.TITLE' | translate}}</h1>
<p class="sub">{{'IAM.DETAIL.DESCRIPTION' | translate}}
</p>
<app-card title="{{ 'ORG.DOMAINS.TITLE' | translate }}"
description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}">
</app-card>
</div>
<metainfo class="side">
<!-- <div class="details">
</div> -->
<!-- <mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details"> -->
<app-iam-contributors>
</app-iam-contributors>
<!-- </mat-tab>
<mat-tab label="{{ 'CHANGES.ORG.TITLE' | translate }}" class="flex-col">
</mat-tab>
</mat-tab-group> -->
</metainfo>
</app-meta-layout>

View File

@@ -0,0 +1,151 @@
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;
}
}
.domain {
display: flex;
align-items: center;
padding: .5rem 0;
flex-wrap: wrap;
.title {
font-size: 16px;
margin-right: 1rem;
}
.verified, .primary{
color: #5282c1;
margin-right: 1rem;
}
.fill-space {
flex: 1;
}
}
.new-desc {
font-size: 14px;
color: #818a8a;
}
.new-row {
display: flex;
flex-wrap: wrap;
align-items: center;
mat-form-field {
flex: 1;
}
}
.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;
text-align: right;
}
a {
&:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
.side-section {
color: #81868a;
}
}
}

View File

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

View File

@@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-iam',
templateUrl: './iam.component.html',
styleUrls: ['./iam.component.scss'],
})
export class IamComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { NgModule, NO_ERRORS_SCHEMA } 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 { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { IamContributorsModule } from './iam-contributors/iam-contributors.module';
import { IamRoutingModule } from './iam-routing.module';
import { IamComponent } from './iam.component';
@NgModule({
declarations: [IamComponent],
imports: [
CommonModule,
IamRoutingModule,
ChangesModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
HasRoleModule,
MatCheckboxModule,
MetaLayoutModule,
MatIconModule,
MatTabsModule,
MatTableModule,
MatPaginatorModule,
MatFormFieldModule,
MatSortModule,
MatTooltipModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatDialogModule,
IamContributorsModule,
],
schemas: [NO_ERRORS_SCHEMA],
})
export class IamModule { }

View File

@@ -10,9 +10,9 @@
<i matTooltip="verified" *ngIf="domain.verified" class="verified las la-check-circle"></i>
<i matTooltip="primary" *ngIf="domain.primary" class="primary las la-chess-queen"></i>
<span class="fill-space"></span>
<button disabled mat-icon-button
<!-- <button disabled mat-icon-button
matTooltip="download /.well-known/caos-developer-domain-association.txt and deploy it on your domain. Then verify"><i
class="las la-file-download"></i></button>
class="las la-file-download"></i></button> -->
<button matTooltip="Remove domain" color="warn" mat-icon-button (click)="removeDomain(domain.domain)"><i
class="las la-trash"></i></button>
</div>
@@ -28,10 +28,10 @@
<mat-icon>check</mat-icon>
</button>
<button disabled mat-icon-button
<!-- <button disabled mat-icon-button
matTooltip="download /.well-known/caos-developer-domain-association.txt and deploy it on your domain. Then verify"><i
class="las la-file-download"></i></button>
<button disabled mat-icon-button matTooltip="Verify"><i class=" las la-check-circle"></i></button>
class="las la-file-download"></i></button> -->
<!-- <button disabled mat-icon-button matTooltip="Verify"><i class=" las la-check-circle"></i></button> -->
</div>
</app-card>

View File

@@ -76,6 +76,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
public saveNewOrgDomain(): void {
this.orgService.AddMyOrgDomain(this.newDomain).then(domain => {
this.domains.push(domain.toObject());
this.newDomain = '';
});
}

View File

@@ -14,14 +14,7 @@
<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),active: activeOrg?.id === org?.id }">
<!-- <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="description">{{org.id}}</span>
<span class="name" *ngIf="org.name">{{ org.name }}</span>
@@ -39,9 +32,6 @@
<button (click)="routeToOrg(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>

View File

@@ -1,7 +1,6 @@
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';
@@ -49,9 +48,4 @@ export class OrgGridComponent {
public routeToOrg(item: Org.AsObject): void {
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;
}
}

View File

@@ -26,8 +26,8 @@
<ng-template appHasRole [appHasRole]="['org.member.delete:'+org.id,'org.member.delete']">
<button (click)="removeProjectMemberSelection()"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
*ngIf="selection.hasValue()">
<mat-icon>remove_circle</mat-icon>
*ngIf="selection.hasValue()" color="warn">
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['org.member.write:'+org.id,'org.member.write']">

View File

@@ -24,6 +24,7 @@ const routes: Routes = [
action: PolicyComponentAction.CREATE,
},
},
/// TODO: add roleguard for iam policy
{
path: 'policy/:policytype',
component: PasswordPolicyComponent,

View File

@@ -123,6 +123,20 @@
</div>
</div>
<div class="content" *ngIf="policyType === PolicyComponentType?.IAM_POLICY">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="iamData.description"
required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle name="hasNumber" ngDefaultControl [(ngModel)]="iamData.userLoginMustBeDomain">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="accent" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>

View File

@@ -3,8 +3,15 @@ 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 {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { OrgService } from 'src/app/services/org.service';
import { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
export enum PolicyComponentAction {
@@ -16,6 +23,7 @@ export enum PolicyComponentType {
LOCKOUT = 'lockout',
AGE = 'age',
COMPLEXITY = 'complexity',
IAM_POLICY = 'iam_policy',
}
@Component({
@@ -24,13 +32,15 @@ export enum PolicyComponentType {
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;
policyData!: PasswordLockoutPolicy.AsObject |
PasswordAgePolicy.AsObject |
PasswordComplexityPolicy.AsObject |
OrgIamPolicy.AsObject;
policyType: PolicyComponentType = PolicyComponentType.COMPLEXITY;
public PolicyComponentType: any = PolicyComponentType;
@@ -60,19 +70,25 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
maxAgeDays: 90,
};
public iamData: any = {
description: '',
userLoginMustBeDomain: false,
};
private sub: Subscription = new Subscription();
constructor(
private route: ActivatedRoute,
private adminService: AdminService,
private orgService: OrgService,
private router: Router,
private toast: ToastService,
private sessionStorage: StorageService,
) {
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) {
@@ -88,6 +104,10 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.titleSub.next('ORG.POLICY.PWD_COMPLEXITY.TITLECREATE');
this.descSub.next('ORG.POLICY.PWD_COMPLEXITY.DESCRIPTIONCREATE');
break;
case PolicyComponentType.IAM_POLICY:
this.titleSub.next('ORG.POLICY.IAM_POLICY.TITLECREATE');
this.descSub.next('ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE');
break;
}
if (this.componentAction === PolicyComponentAction.MODIFY) {
@@ -102,6 +122,10 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
case PolicyComponentType.COMPLEXITY:
this.complexityData = data.toObject();
break;
case PolicyComponentType.IAM_POLICY:
this.iamData = data.toObject();
console.log(this.iamData);
break;
}
});
}
@@ -129,6 +153,10 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.titleSub.next('ORG.POLICY.PWD_COMPLEXITY.TITLE');
this.descSub.next('ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION');
return this.orgService.GetPasswordComplexityPolicy();
case PolicyComponentType.IAM_POLICY:
this.titleSub.next('ORG.POLICY.IAM_POLICY.TITLECREATE');
this.descSub.next('ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE');
return this.orgService.GetMyOrgIamPolicy();
}
}
@@ -189,7 +217,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
@@ -201,7 +229,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
@@ -217,11 +245,27 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.IAM_POLICY:
console.log(this.complexityData);
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.CreateOrgIamPolicy(
orgId,
this.complexityData.description,
this.complexityData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
}
break;
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
switch (this.policyType) {
@@ -231,7 +275,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
@@ -243,7 +287,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
@@ -259,11 +303,27 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['orgs', this.orgId]);
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
break;
case PolicyComponentType.IAM_POLICY:
console.log(this.complexityData);
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.UpdateOrgIamPolicy(
orgId,
this.complexityData.description,
this.complexityData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error.message);
});
}
break;
}
}
}

View File

@@ -16,7 +16,7 @@
<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>
<i class="las la-trash"></i>
</button>
</ng-template>
</div>
@@ -46,12 +46,11 @@
<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>
<i *ngIf="complexityPolicy" class="icon las la-check-circle"></i>
</button>
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}"
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn"
(click)="deletePolicy(PolicyComponentType.COMPLEXITY)" mat-icon-button>
<mat-icon>delete_outline</mat-icon>
<i class="las la-trash"></i>
</button>
</div>
@@ -81,7 +80,7 @@
</button>
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" (click)="deletePolicy(PolicyComponentType.LOCKOUT)"
mat-icon-button>
<mat-icon>delete_outline</mat-icon>
<i class="las la-trash"></i>
</button>
</div>
@@ -99,4 +98,39 @@
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div>
</div> -->
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<div class="p-item card">
<div class="avatar"><i class="icon las la-gem"></i>
</div>
<div class="title">
<span>{{'ORG.POLICY.IAM_POLICY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i *ngIf="iamPolicy" class="icon las la-check-circle"></i>
</button>
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn"
(click)="deletePolicy(PolicyComponentType.IAM_POLICY)" mat-icon-button>
<i class="las la-trash"></i>
</button>
</ng-template>
</div>
<p *ngIf="iamPolicy?.description; else showDescIAM" class="desc">
{{ iamPolicy.description }}</p>
<ng-template #showDescIAM>
<p class="desc">
{{'ORG.POLICY.IAM_POLICY.DESCRIPTION' | translate}}</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button [disabled]="iamPolicy" [routerLink]="[ 'policy', PolicyComponentType.IAM_POLICY,'create' ]"
color="primary" mat-raised-button>{{'ORG.POLICY.BTN_INSTALL' | translate}}</button>
<button [disabled]="!iamPolicy" [routerLink]="[ 'policy', PolicyComponentType.IAM_POLICY ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
</div>

View File

@@ -30,7 +30,7 @@ h1 {
justify-content: center;
margin-bottom: .5rem;
mat-icon {
mat-icon, i {
font-size: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;

View File

@@ -1,10 +1,12 @@
import { Component, OnInit } from '@angular/core';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
PolicyState,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
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';
@@ -20,11 +22,17 @@ export class PolicyGridComponent implements OnInit {
public lockoutPolicy!: PasswordLockoutPolicy.AsObject;
public agePolicy!: PasswordAgePolicy.AsObject;
public complexityPolicy!: PasswordComplexityPolicy.AsObject;
public iamPolicy!: OrgIamPolicy.AsObject;
public PolicyState: any = PolicyState;
public PolicyComponentType: any = PolicyComponentType;
constructor(private orgService: OrgService, public authUserService: AuthUserService, private toast: ToastService) {
constructor(
private orgService: OrgService,
private adminService: AdminService,
public authUserService: AuthUserService,
private toast: ToastService,
) {
this.getData();
}
@@ -36,6 +44,8 @@ export class PolicyGridComponent implements OnInit {
// this.orgService.GetPasswordAgePolicy().then(data => this.agePolicy = data.toObject()).catch(error => { });
this.orgService.GetPasswordComplexityPolicy().then(data => this.complexityPolicy = data.toObject())
.catch(error => { });
this.orgService.GetMyOrgIamPolicy().then(data => this.iamPolicy = data.toObject())
.catch(error => { });
}
public deletePolicy(type: PolicyComponentType): void {

View File

@@ -26,7 +26,7 @@
<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>
{{org.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</div>
</ng-container>

View File

@@ -1,7 +1,6 @@
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';
@@ -76,12 +75,6 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
});
}
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);
}

View File

@@ -12,6 +12,7 @@ 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 { PipesModule } from 'src/app/pipes/pipes.module';
import { ProjectRolesModule } from '../../modules/project-roles/project-roles.module';
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
@@ -32,6 +33,7 @@ import { ProjectGrantCreateComponent } from './project-grant-create.component';
ProjectRolesModule,
MatIconModule,
MatTooltipModule,
PipesModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,

View File

@@ -29,8 +29,9 @@
<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 mat-icon-button (click)="removeEntry(i)" color="warn"
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}">
<i class="las la-trash"></i>
</button>
</ng-container>
</div>

View File

@@ -24,17 +24,17 @@
<div class="text-part">
<span *ngIf="item.changeDate" class="top">last modified on
{{
dateFromTimestamp(item.changeDate) | date: 'EEE dd. MMM, HH:mm'
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span>
<span class="description" *ngIf="item.grantedOrgName">{{item.grantedOrgName}}</span>
<!-- <span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span> -->
<span *ngIf="item.changeDate" class="created">created on
{{
dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm'
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
<div class="icons">
<mat-icon class="icon">apps</mat-icon>
</div>
</div>
<button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button>
@@ -53,4 +53,4 @@
</mat-menu>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
</div>

View File

@@ -2,7 +2,6 @@ import { animate, animateChild, query, stagger, style, transition, trigger } fro
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { ProjectGrantView, ProjectState, ProjectType, ProjectView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -57,9 +56,4 @@ export class GrantedProjectGridComponent {
public addItem(): void {
this.newClicked.emit(true);
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
}

View File

@@ -78,7 +78,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{dateFromTimestamp(project.creationDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
@@ -87,7 +87,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{dateFromTimestamp(project.changeDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>

View File

@@ -5,7 +5,6 @@ import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { ProjectGrantView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
@@ -99,11 +98,6 @@ export class GrantedProjectListComponent implements OnInit, OnDestroy {
});
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.ReactivateProject(project.id);

View File

@@ -25,17 +25,17 @@
<div class="text-part">
<span *ngIf="item.changeDate" class="top">last modified on
{{
dateFromTimestamp(item.changeDate) | date: 'EEE dd. MMM, HH:mm'
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span>
<span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span>
<!-- <span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span> -->
<span *ngIf="item.changeDate" class="created">created on
{{
dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm'
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
<div class="icons">
<mat-icon class="icon">apps</mat-icon>
</div>
</div>
<button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button>
@@ -56,8 +56,10 @@
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
<div class="add-project-button card" (click)="addItem()">
<mat-icon class="icon">add</mat-icon>
<span>Add new project</span>
</div>
</div>
<ng-template appHasRole [appHasRole]="['project.write']">
<div class="add-project-button card" (click)="addItem()">
<mat-icon class="icon">add</mat-icon>
<span>Add new project</span>
</div>
</ng-template>
</div>

View File

@@ -2,7 +2,6 @@ import { animate, animateChild, query, stagger, style, transition, trigger } fro
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { ProjectState, ProjectType, ProjectView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -58,11 +57,6 @@ export class OwnedProjectGridComponent {
this.newClicked.emit(true);
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
public reactivateProjects(selected: ProjectView.AsObject[]): void {
Promise.all([selected.map(proj => {
return this.projectService.ReactivateProject(proj.projectId);

View File

@@ -30,9 +30,11 @@
<mat-icon svgIcon="mdi_light_on"></mat-icon>
</button>
</div>
<a class="add-button" [routerLink]="[ '/projects', 'create']" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
<ng-template appHasRole [appHasRole]="['project.write']">
<a class="add-button" [routerLink]="[ '/projects', '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="(loading$ | async) || (loading$ | async)">
@@ -69,7 +71,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{dateFromTimestamp(project.creationDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
@@ -78,7 +80,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{dateFromTimestamp(project.changeDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
@@ -90,4 +92,4 @@
<mat-paginator class="background-style" [length]="totalResult" [pageSize]="10" [pageSizeOptions]="[5, 10, 20]"
(page)="changePage($event)"></mat-paginator>
</div>
</div>
</div>

View File

@@ -5,7 +5,6 @@ import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { ProjectView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
@@ -103,11 +102,6 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
this.ownedProjectList = [];
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.ReactivateProject(project.projectId);

View File

@@ -12,7 +12,7 @@
<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>
<i class="las la-trash"></i>
</button> -->
<ng-template appHasRole [appHasRole]="['project.app.write']">
<a [disabled]="disabled" class="add-button" [routerLink]="[ '/projects', projectId, 'apps', 'create']"

View File

@@ -14,8 +14,8 @@
</div>
<span class="fill-space"></span>
<button (click)="removeProjectMemberSelection()" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()">
<mat-icon>remove_circle</mat-icon>
class="icon-button" color="warn" mat-icon-button *ngIf="selection.hasValue()">
<i class="las la-trash"></i>
</button>
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary"
mat-raised-button>

View File

@@ -12,8 +12,8 @@
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']">
<button [disabled]="disabled" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()">
<mat-icon>delete_outline</mat-icon>
mat-icon-button *ngIf="selection.hasValue()" color="warn">
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.grant.member.write:'+projectId,'project.grant.member.write']">
@@ -59,13 +59,13 @@
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{dateFromTimestamp(grant.creationDate) | date: 'dd. MMM, HH:mm' }} </td>
{{grant.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{dateFromTimestamp(grant.changeDate) | date: 'dd. MMM, HH:mm' }} </td>
{{grant.changeDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
</ng-container>

View File

@@ -4,7 +4,6 @@ import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/cor
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { tap } from 'rxjs/operators';
import { ProjectGrant, ProjectMemberView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
@@ -131,9 +130,4 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
this.toast.showInfo(error.message);
});
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
}

View File

@@ -25,10 +25,10 @@
<span class="fill-space"></span>
<ng-template appHasRole
[appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']">
<button (click)="removeProjectMemberSelection()"
<button (click)="removeProjectMemberSelection()" color="warn"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
*ngIf="selection.hasValue()">
<mat-icon>remove_circle</mat-icon>
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole

View File

@@ -1,5 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { OwnedProjectDetailComponent } from './owned-project-detail/owned-project-detail.component';
@@ -14,6 +16,10 @@ const routes: Routes = [
{
path: 'create',
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['project.write'],
},
},
{
path: ':id/grant/:grantId',

View File

@@ -25,6 +25,7 @@ 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 { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
import { PipesModule } from 'src/app/pipes/pipes.module';
import { ChangesModule } from '../../modules/changes/changes.module';
import { ProjectRolesModule } from '../../modules/project-roles/project-roles.module';
@@ -95,6 +96,7 @@ import { ProjectsComponent } from './projects.component';
CardModule,
MatTooltipModule,
MatSortModule,
PipesModule,
OrgContributorsModule,
TranslateModule.forChild({
loader: {

View File

@@ -167,8 +167,8 @@
<button (click)="phoneEditState = true" mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="phone?.phone" (click)="deletePhone()" mat-icon-button>
<mat-icon>delete_outline</mat-icon>
<button color="warn" *ngIf="phone?.phone" (click)="deletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
</div>
</ng-container>

View File

@@ -3,8 +3,9 @@
<div class="row" *ngFor="let mfa of mfaSubject | async">
<span>{{'USER.MFA.TYPE.'+ mfa.type | translate}}</span>
<span>{{'USER.MFA.STATE.'+ mfa.state | translate}}</span>
<button mat-icon-button (click)="deleteMFA(mfa.type)" matTooltip="{{'ACTIONS.DELETE' | translate}}">
<mat-icon>delete_outline</mat-icon>
<button mat-icon-button (click)="deleteMFA(mfa.type)" color="warn"
matTooltip="{{'ACTIONS.DELETE' | translate}}">
<i class="las la-trash"></i>
</button>
</div>
<p class="row" *ngIf="error">{{error}}</p>

View File

@@ -153,8 +153,8 @@
<button (click)="phoneEditState = true" mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="user?.phone" (click)="deletePhone()" mat-icon-button>
<mat-icon>delete_outline</mat-icon>
<button *ngIf="user?.phone" (click)="deletePhone()" color="warn" mat-icon-button>
<i class="las la-trash"></i>
</button>
</div>
</ng-container>

View File

@@ -11,9 +11,9 @@
</div>
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['user.grant.delete:'+userId,'user.grant.delete']">
<button matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button
*ngIf="selection.hasValue()">
<mat-icon>delete_outline</mat-icon>
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.grant.write:'+userId,'user.grant.write']">
@@ -59,13 +59,13 @@
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{dateFromTimestamp(grant.creationDate) | date: 'dd. MMM, HH:mm' }} </td>
{{grant.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{dateFromTimestamp(grant.changeDate) | date: 'dd. MMM, HH:mm' }} </td>
{{grant.changeDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
</ng-container>

View File

@@ -2,7 +2,6 @@ import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { tap } from 'rxjs/operators';
import { ProjectGrant, UserGrant } from 'src/app/proto/generated/management_pb';
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
@@ -59,9 +58,4 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.selection.clear() :
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row));
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
}

View File

@@ -11,6 +11,7 @@ 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 { PipesModule } from 'src/app/pipes/pipes.module';
import { UserGrantsComponent } from './user-grants.component';
@@ -31,6 +32,7 @@ import { UserGrantsComponent } from './user-grants.component';
MatCheckboxModule,
MatTooltipModule,
TranslateModule,
PipesModule,
],
exports: [
UserGrantsComponent,