You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dlab.apache.org by an...@apache.org on 2019/05/20 11:45:57 UTC
[incubator-dlab] 01/09: added administration as a top level on
structure
This is an automated email from the ASF dual-hosted git repository.
ankovalyshyn pushed a commit to branch project_grid
in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git
commit eeb4793a52a2d7970654734528c6e1955467017b
Author: Andriana Kovalyshyn <An...@epam.com>
AuthorDate: Mon May 20 13:02:34 2019 +0300
added administration as a top level on structure
---
.../app/administration/administration.module.ts | 31 +++
.../backup-dilog/backup-dilog.component.html | 82 ++++++
.../backup-dilog/backup-dilog.component.scss | 26 ++
.../backup-dilog/backup-dilog.component.ts | 75 ++++++
.../src/app/administration/management/index.ts | 82 ++++++
.../manage-environment-dilog.component.html | 73 ++++++
.../manage-environment-dilog.component.scss | 87 ++++++
.../manage-environment-dilog.component.ts | 129 +++++++++
.../group-name-validarion.directive.ts | 37 +++
.../manage-roles-groups.component.html | 154 +++++++++++
.../manage-roles-groups.component.scss | 292 +++++++++++++++++++++
.../manage-roles-groups.component.ts | 192 ++++++++++++++
.../management-grid/management-grid.component.html | 161 ++++++++++++
.../management-grid/management-grid.component.scss | 40 +++
.../management-grid/management-grid.component.ts | 172 ++++++++++++
.../management/management.component.html | 55 ++++
.../management/management.component.scss | 37 +++
.../management/management.component.ts | 275 +++++++++++++++++++
.../administration/management/management.model.ts | 58 ++++
.../ssn-monitor/ssn-monitor.component.html | 117 +++++++++
.../ssn-monitor/ssn-monitor.component.scss | 50 ++++
.../ssn-monitor/ssn-monitor.component.ts | 59 +++++
.../webapp/src/app/administration/project/index.ts | 39 +++
.../administration/project/project.component.html | 19 ++
.../administration/project/project.component.scss | 18 ++
.../administration/project/project.component.ts | 34 +++
26 files changed, 2394 insertions(+)
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
new file mode 100644
index 0000000..2ae70a4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ManagenementModule } from './management';
+import { ProjectModule } from './project';
+
+@NgModule({
+ imports: [CommonModule, ManagenementModule, ProjectModule],
+ declarations: [],
+ exports: [ManagenementModule, ProjectModule]
+})
+export class AdministrationModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html
new file mode 100644
index 0000000..9c797ad
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html
@@ -0,0 +1,82 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<modal-dialog #bindDialog modalClass="backup-dialog modal-sm">
+ <modal-header>
+ <h4 class="modal-title">Backup options</h4>
+ </modal-header>
+ <modal-content>
+ <div class="content-box" *ngIf="backupOptions">
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.configFiles[0] === 'all'"
+ (change)="onHoldChanged($event, 'configFiles')">
+ <span class="hold-label">Configuration files</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.keys[0] === 'all'"
+ (change)="onHoldChanged($event, 'keys')">
+ <span class="hold-label">User keys</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.databaseBackup"
+ (change)="onHoldChanged($event, 'databaseBackup')">
+ <span class="hold-label">Database</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.certificates[0] === 'all'"
+ (change)="onHoldChanged($event, 'certificates')">
+ <span class="hold-label">SSL Certificates</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.jars[0] === 'all'"
+ (change)="onHoldChanged($event, 'jars')">
+ <span class="hold-label">JAR files</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="hold-block">
+ <mat-slide-toggle
+ labelPosition="before"
+ [checked]="backupOptions.logsBackup"
+ (change)="onHoldChanged($event, 'logsBackup')">
+ <span class="hold-label">Log files</span>
+ </mat-slide-toggle>
+ </div>
+ <div class="text-center m-top-30 m-bott-10">
+ <button mat-raised-button type="button" class="butt" (click)="bindDialog.close(); backupOptions.setDegault()">
+ Cancel
+ </button>
+ <button mat-raised-button type="button" (click)="applyOptions()" class="butt butt-success" [disabled]="!valid">Apply</button>
+ </div>
+ </div>
+ </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss
new file mode 100644
index 0000000..e27a7a4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss
@@ -0,0 +1,26 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.hold-block {
+ padding: 5px 10px;
+ transition: all 0.4s ease-out;
+ &:hover {
+ color: #35afd5;
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts
new file mode 100644
index 0000000..8b8ed61
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, ViewChild, Output, EventEmitter } from '@angular/core';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+import { BackupOptionsModel } from '../management.model';
+
+@Component({
+ selector: 'dlab-backup-dilog',
+ templateUrl: './backup-dilog.component.html',
+ styleUrls: ['./backup-dilog.component.scss']
+})
+export class BackupDilogComponent implements OnInit {
+ readonly DICTIONARY = DICTIONARY;
+ public backupOptions: BackupOptionsModel = new BackupOptionsModel([], [], [], [], false, false);
+ public valid: boolean = true;
+
+ @ViewChild('bindDialog') bindDialog;
+ @Output() backupOpts: EventEmitter<{}> = new EventEmitter();
+
+ ngOnInit() {
+ this.backupOptions.setDegault();
+ this.bindDialog.onClosing = () => this.backupOptions.setDegault();
+ }
+
+ public open(param): void {
+ this.valid = true;
+ this.bindDialog.open(param);
+ }
+
+ public onHoldChanged($event, key): void {
+ this.backupOptions[key] instanceof Array
+ ? (this.backupOptions[key][0] = $event.checked ? 'all' : 'skip')
+ : (this.backupOptions[key] = !this.backupOptions[key]);
+
+ this.checkValidity();
+ }
+
+ public applyOptions(): void {
+ this.backupOpts.emit(this.backupOptions);
+ this.backupOptions.setDegault();
+ this.bindDialog.close();
+ }
+
+ private checkValidity(): void {
+ const items = [];
+
+ Object.keys(this.backupOptions).forEach(el => {
+ if (this.backupOptions[el] instanceof Array) {
+ if (this.backupOptions[el][0] && this.backupOptions[el][0] !== 'skip') items.push(this.backupOptions[el][0]);
+ } else {
+ if (this.backupOptions[el]) items.push(this.backupOptions[el]) ;
+ }
+ });
+
+ this.valid = items.length > 0;
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts
new file mode 100644
index 0000000..7d15b50
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import {
+ ModalModule,
+ UploadKeyDialogModule,
+ ProgressDialogModule,
+ BubbleModule,
+ ConfirmationDialogModule
+} from '../../shared';
+import { MaterialModule } from '../../shared/material.module';
+
+import { ManagementComponent } from './management.component';
+import { ManagementGridComponent, ConfirmationDialogComponent } from './management-grid/management-grid.component';
+import { ComputationalResourcesModule } from '../../resources/computational/computational-resources-list';
+
+import { FormControlsModule } from '../../shared/form-controls';
+import { BackupDilogComponent } from './backup-dilog/backup-dilog.component';
+import { ManageEnvironmentComponent, ConfirmActionDialogComponent } from './manage-environment/manage-environment-dilog.component';
+
+import { GroupNameValidationDirective } from './manage-roles-groups/group-name-validarion.directive';
+import { DirectivesModule } from '../../core/directives';
+
+import { SsnMonitorComponent } from './ssn-monitor/ssn-monitor.component';
+import { ManageRolesGroupsComponent, ConfirmDeleteUserAccountDialogComponent } from './manage-roles-groups/manage-roles-groups.component';
+import { ProjectModule } from '../project';
+
+export * from './management.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ProjectModule,
+ ModalModule,
+ UploadKeyDialogModule,
+ ProgressDialogModule,
+ BubbleModule,
+ ConfirmationDialogModule,
+ ComputationalResourcesModule,
+ FormControlsModule,
+ DirectivesModule,
+ MaterialModule
+ ],
+ declarations: [
+ ManagementComponent,
+ ManagementGridComponent,
+
+ GroupNameValidationDirective,
+ BackupDilogComponent,
+ ManageEnvironmentComponent,
+ ConfirmationDialogComponent,
+ ConfirmActionDialogComponent,
+ ConfirmDeleteUserAccountDialogComponent,
+ SsnMonitorComponent,
+ ManageRolesGroupsComponent
+ ],
+ entryComponents: [ConfirmationDialogComponent, ConfirmActionDialogComponent, ConfirmDeleteUserAccountDialogComponent],
+ exports: [ManagementComponent]
+})
+export class ManagenementModule {}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
new file mode 100644
index 0000000..094ec9b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
@@ -0,0 +1,73 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<modal-dialog #bindDialog modalClass="manage-env-dialog modal-xl-s">
+ <modal-header>
+ <h4 class="modal-title">Manage environment</h4>
+ </modal-header>
+ <modal-content>
+ <div class="content-box">
+ <div *ngIf="usersList.length">
+ <form [formGroup]="manageUsersForm" (submit)="setBudgetLimits(manageUsersForm.value)" novalidate>
+ <mat-list>
+ <mat-list-item class="list-header">
+ <div class="username">User</div>
+ <div class="quotes" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">Limit</div>
+ <div class="action">Actions</div>
+ </mat-list-item>
+ <div class="scrolling-content" id="scrolling" formArrayName="users">
+ <mat-list-item *ngFor="let item of usersEnvironments.controls; let i=index" [formGroupName]="i" class="list-item">
+ <div class="username ellipsis">{{ manageUsersForm.controls['users'].controls[i].value['name'] }}</div>
+ <div class="quotes" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+ <input type="number" min="0" placeholder="Enter limit, in USD" formControlName="budget">
+ <span class="danger_color" *ngIf="!manageUsersForm?.controls['users'].controls[i].controls['budget'].valid && !manageUsersForm?.controls['users'].controls[i].controls['budget'].hasError('overrun')">Only positive integers are allowed</span>
+ <span class="danger_color" *ngIf="manageUsersForm?.controls['users'].controls[i].controls['budget'].hasError('overrun')">Per-user quotes cannot be greater than total budget</span>
+ </div>
+ <div class="action">
+ <span *ngIf=" manageUsersForm.controls['users'].controls[i].value['status'] === 'ACTIVE'; else not_active" matTooltip="Stop" matTooltipPosition="above" (click)="applyAction('stop', item)"><i class="material-icons">pause_circle_outline</i></span>
+
+ <ng-template #not_active>
+ <span class="disabled" matTooltip="User's environment is not active" matTooltipPosition="above">
+ <i class="material-icons">pause_circle_outline</i>
+ </span>
+ </ng-template>
+
+ <span matTooltip="Terminate" matTooltipPosition="above" (click)="applyAction('terminate', item)"><i class="material-icons">phonelink_off</i></span>
+ </div>
+ </mat-list-item>
+ </div>
+ <div class="control-group total-budget" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+ <label class="label">Total budget</label>
+ <div class="control">
+ <input type="number" formControlName="total" placeholder="Enter total budget, in USD">
+ <span class="danger_color" *ngIf="manageUsersForm?.controls['total'].hasError('overrun')">Total budget cannot be lower than a sum of users quotes</span>
+ </div>
+ </div>
+ <div class="text-center m-top-30" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+ <button mat-raised-button type="button" (click)="bindDialog.close()" class="butt action">Cancel</button>
+ <button mat-raised-button type="submit" [disabled]="!manageUsersForm.valid"
+ class="butt butt-success" [ngClass]="{'not-allowed': !manageUsersForm.valid}">Apply</button>
+ </div>
+ </mat-list>
+ </form>
+ </div>
+ <div class="info message" *ngIf="!usersList.length">No active users environments</div>
+ </div>
+ </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss
new file mode 100644
index 0000000..b268e6f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss
@@ -0,0 +1,87 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.manage-env-dialog {
+ .mat-list {
+ .mat-list-item {
+ height: 60px;
+ position: relative;
+ .mat-list-item-content {
+ padding: 0 !important;
+ justify-content: space-between;
+ color: #577289;
+ }
+ }
+ }
+ .scrolling-content {
+ max-height: 300px;
+ min-height: 85px;
+ overflow-y: auto;
+ }
+ .username {
+ width: 45%;
+ }
+ .quotes {
+ width: 40%;
+ margin-right: 10px;
+ position: relative;
+ .danger_color {
+ position: absolute;
+ left: 0;
+ top: 36px;
+ }
+ }
+ .action {
+ width: 15%;
+ span {
+ padding: 3px;
+ cursor: pointer;
+ &:hover {
+ color: #35afd5;
+ }
+ i {
+ font-size: 20px;
+ }
+ }
+ .disabled {
+ cursor: not-allowed !important;
+ pointer-events: all;
+ opacity: .6;
+ &:hover {
+ color: #6b8299;
+ }
+ }
+ }
+ .total-budget {
+ border-top: 1px solid #edf1f5;
+ padding-top: 15px;
+ .control {
+ position: relative;
+ width: 56%;
+ .danger_color {
+ position: absolute;
+ left: 0;
+ bottom: -20px;
+ }
+ }
+ .label {
+ width: 44%;
+ }
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
new file mode 100644
index 0000000..ae7d7d1
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, ViewChild, Output, EventEmitter, ViewEncapsulation, Inject } from '@angular/core';
+import { Validators, FormBuilder, FormGroup, FormArray } from '@angular/forms';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+ selector: 'dlab-manage-env-dilog',
+ templateUrl: './manage-environment-dilog.component.html',
+ styleUrls: ['./manage-environment-dilog.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class ManageEnvironmentComponent {
+ readonly DICTIONARY = DICTIONARY;
+ public usersList: Array<string> = [];
+ public manageUsersForm: FormGroup;
+ public manageTotalsForm: FormGroup;
+
+ @ViewChild('bindDialog') bindDialog;
+ @Output() manageEnv: EventEmitter<{}> = new EventEmitter();
+ @Output() setBudget: EventEmitter<{}> = new EventEmitter();
+
+ constructor(
+ private _fb: FormBuilder,
+ public dialog: MatDialog
+ ) { }
+
+ get usersEnvironments(): FormArray {
+ return <FormArray>this.manageUsersForm.get('users');
+ }
+
+ public open(param, data, settings): void {
+ this.usersList = data;
+ !this.manageUsersForm && this.initForm();
+
+ this.manageUsersForm.setControl('users',
+ this._fb.array((this.usersList || []).map((x: any) => this._fb.group({
+ name: x.name, budget: [x.budget, [Validators.min(0), this.userValidityCheck.bind(this)]], status: x.status
+ }))));
+ this.manageUsersForm.controls['total'].setValue(settings.conf_max_budget || null);
+ this.bindDialog.open(param);
+ }
+
+ public setBudgetLimits(value) {
+ this.setBudget.emit(value);
+ this.bindDialog.close();
+ }
+
+ public applyAction(action, user) {
+ const dialogRef: MatDialogRef<ConfirmActionDialogComponent> = this.dialog.open(
+ ConfirmActionDialogComponent, { data: {action, user: user.value.name}, width: '550px', panelClass: 'error-modalbox' });
+ dialogRef.afterClosed().subscribe(result => {
+ if (result) this.manageEnv.emit({action, user: user.value.name});
+ });
+ }
+
+ private initForm(): void {
+ this.manageUsersForm = this._fb.group({
+ total: [null, [Validators.min(0), this.totalValidityCheck.bind(this)]],
+ users: this._fb.array([this._fb.group({ name: '', budget: null, status: ''})])
+ });
+ }
+
+ private getCurrentUsersTotal(): number {
+ return this.manageUsersForm.value.users.reduce((memo, el) => memo += el.budget, 0);
+ }
+
+ private getCurrentTotalValue(): number {
+ return this.manageUsersForm.value.total;
+ }
+
+ private totalValidityCheck(control) {
+ return (control && control.value)
+ ? (control.value >= this.getCurrentUsersTotal() ? null : { overrun: true })
+ : null;
+ }
+
+ private userValidityCheck(control) {
+ if (control && control.value) {
+ return (this.getCurrentTotalValue() && this.getCurrentTotalValue() < this.getCurrentUsersTotal()) ? { overrun: true } : null;
+ }
+ }
+}
+
+@Component({
+ selector: 'dialog-result-example-dialog',
+ template: `
+ <div class="dialog-header">
+ <h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> resource</h4>
+ <button type="button" class="close" (click)="dialogRef.close()">×</button>
+ </div>
+ <div mat-dialog-content class="content">
+ <p>Environment of <b>{{ data.user }}</b> will be
+ <span *ngIf="data.action === 'terminate'"> terminated.</span>
+ <span *ngIf="data.action === 'stop'">stopped.</span>
+ </p>
+ <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+ </div>
+ <div class="text-center">
+ <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+ <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+ </div>
+ `,
+ styles: []
+})
+export class ConfirmActionDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef<ConfirmActionDialogComponent>,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) { }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts
new file mode 100644
index 0000000..e3f7207
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Directive, forwardRef, Input } from '@angular/core';
+import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
+
+@Directive({
+ selector: '[validator][ngModel],[group-dir][ngFormControl]',
+ providers: [{
+ multi: true,
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => GroupNameValidationDirective)
+ }]
+})
+export class GroupNameValidationDirective implements Validator {
+ @Input() validator: Function;
+
+ validate(control: AbstractControl): { [key: string]: any; } {
+ return this.validator(control);
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html
new file mode 100644
index 0000000..85b751b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html
@@ -0,0 +1,154 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<modal-dialog #bindDialog modalClass="manage-roles-dialog modal-xxl">
+ <modal-header>
+ <h4 class="modal-title">Manage roles</h4>
+ </modal-header>
+ <modal-content>
+ <div class="content-box">
+ <button mat-raised-button class="butt add-group" (click)="stepperView = !stepperView">
+ <i class="material-icons">people_outline</i>Add group
+ </button>
+ <mat-horizontal-stepper #stepper *ngIf="stepperView" class="stepper ani">
+ <mat-step>
+ <ng-template matStepLabel>Groups</ng-template>
+ <div class="inner-step mat-reset">
+ <input [validator]="groupValidarion()" type="text" placeholder="Enter group name" [(ngModel)]="setupGroup" #setupGroupName="ngModel">
+ <div class="danger_color" *ngIf="setupGroupName.errors?.patterns && setupGroupName.dirty">Group name can only contain letters, numbers, hyphens and '_'</div>
+ <div class="danger_color" *ngIf="setupGroupName.errors?.duplicate && setupGroupName.dirty">Group name already exists</div>
+ </div>
+ <div class="text-center m-bott-10">
+ <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
+ <button mat-raised-button matStepperNext class="butt">Next<i class="material-icons">keyboard_arrow_right</i></button>
+ </div>
+ </mat-step>
+ <mat-step>
+ <ng-template matStepLabel>Roles</ng-template>
+ <div class="inner-step mat-reset">
+ <div class="selector-wrapper">
+ <!-- <multi-select-dropdown (selectionChange)="onUpdate($event)" [type]="'role'" [items]="rolesList" [model]="setupRoles"></multi-select-dropdown> -->
+ <mat-form-field>
+ <mat-select multiple [compareWith]="compareObjects" name="roles" [(value)]="setupRoles" placeholder="Select roles">
+ <mat-option class="multiple-select" disabled>
+ <a class="select ani" (click)="selectAllOptions(setupRoles, rolesList)">
+ <i class="material-icons">playlist_add_check</i> All
+ </a>
+ <a class="deselect ani" (click)="selectAllOptions(setupRoles)">
+ <i class="material-icons">clear</i> None
+ </a>
+ </mat-option>
+ <mat-option *ngFor="let role of rolesList" [value]="role">
+ {{ role }}
+ </mat-option>
+ </mat-select>
+ <button class="caret">
+ <i class="material-icons">keyboard_arrow_down</i>
+ </button>
+ </mat-form-field>
+ </div>
+ </div>
+ <div class="text-center m-bott-10">
+ <button mat-raised-button matStepperPrevious class="butt"><i class="material-icons">keyboard_arrow_left</i>Back</button>
+ <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
+ <button mat-raised-button matStepperNext class="butt">Next<i class="material-icons">keyboard_arrow_right</i></button>
+ </div>
+ </mat-step>
+ <mat-step>
+ <ng-template matStepLabel>Users</ng-template>
+ <div class="inner-step mat-reset">
+ <input type="text" placeholder="Enter user login" [(ngModel)]="setupUser">
+ </div>
+ <div class="text-center m-bott-10">
+ <button mat-raised-button matStepperPrevious class="butt"><i class="material-icons">keyboard_arrow_left</i>Back</button>
+ <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
+ <button mat-raised-button (click)="manageAction('create', 'group')" class="butt butt-success"
+ [disabled]="!setupGroup || setupGroupName.errors?.pattern || !setupRoles.length > 0">Create</button>
+ </div>
+ </mat-step>
+ </mat-horizontal-stepper>
+ <mat-divider></mat-divider>
+ <div *ngIf="groupsData.length" class="ani">
+ <table class="dashboard_table">
+ <tr>
+ <th class="th_name groups">Name</th>
+ <th class="roles">Roles</th>
+ <th class="users">Users</th>
+ <th class="th_actions">Action</th>
+ </tr>
+ <tr *ngFor="let item of groupsData" class="dashboard_table_body filter-row">
+ <td>{{ item.group }}</td>
+ <td class="roles mat-reset">
+ <div class="selector-wrapper-edit">
+ <mat-form-field>
+ <mat-select multiple [compareWith]="compareObjects" name="selected_roles" [(value)]="item.selected_roles" placeholder="Select roles">
+ <mat-option class="multiple-select" disabled>
+ <a class="select ani" (click)="selectAllOptions(item, rolesList, 'selected_roles')">
+ <i class="material-icons">playlist_add_check</i> All
+ </a>
+ <a class="deselect ani" (click)="selectAllOptions(item, null, 'selected_roles')">
+ <i class="material-icons">clear</i> None
+ </a>
+ </mat-option>
+ <mat-option *ngFor="let role of rolesList" [value]="role">
+ {{ role }}
+ </mat-option>
+ </mat-select>
+ <button class="caret ani">
+ <i class="material-icons">keyboard_arrow_down</i>
+ </button>
+ </mat-form-field>
+ </div>
+ </td>
+ <td class="users-list ani">
+ <mat-form-field class="chip-list">
+ <input #user matInput placeholder="Enter user login" pattern="[@.-_0-9a-zA-Z]" (keydown.enter)="addUser(user.value, item); user.value = ''">
+ <button mat-icon-button matSuffix (click)="addUser(user.value, item); user.value = ''">
+ <mat-icon>person_add</mat-icon>
+ </button>
+ </mat-form-field>
+ <div class="list-selected list-container ani">
+ <mat-chip-list>
+ <mat-chip *ngFor="let user of item.users">
+ {{ user }}
+ <a class="material-icons" (click)="removeUser(item.users, user)">clear</a>
+ </mat-chip>
+ </mat-chip-list>
+ </div>
+ </td>
+ <td class="actions">
+ <button mat-icon-button class="reset ani" (click)="manageAction('delete', 'group', item)">
+ <i class="material-icons">close</i>
+ </button>
+
+ <button mat-icon-button class="apply ani" matTooltip="Group cannot be updated without any selected role"
+ matTooltipPosition="above"
+ [matTooltipDisabled]="item.selected_roles.length > 0"
+ [ngClass]="{ 'not-allowed' : !item.selected_roles.length }"
+ (click)="manageAction('update', 'group', item)">
+ <i class="material-icons">done</i>
+ </button>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </modal-content>
+</modal-dialog>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss
new file mode 100644
index 0000000..b28c061
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss
@@ -0,0 +1,292 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+::ng-deep .mat-option:first-child .mat-pseudo-checkbox { display: none; }
+
+.caret {
+ width: 40px;
+ color: #4ab8dc;
+ border: none;
+ border-left: 1px solid #ececec;
+ background-color: #fff;
+ position: absolute;
+ right: 0;
+ top: 0px;
+ height: 36px;
+ cursor: pointer;
+ &.not-allowed {
+ background-color: #dcdcdc;
+ }
+}
+
+
+.content-box {
+ padding: 20px 30px 35px;
+ height: 85vh;
+ overflow-y: auto;
+}
+.no-details {
+ color: #d8d8d8;
+}
+.mat-divider {
+ margin: 10px 0;
+}
+.stepper {
+ height: 190px;
+ margin-top: 10px;
+ .inner-step {
+ height: 70px;
+ padding: 5px;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
+ input {
+ width: 490px;
+ align-self: center;
+ }
+ .caret {
+ i {
+ margin-top: 3px;
+ }
+ }
+ }
+ .text-center {
+ button {
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+ }
+}
+.selector-wrapper {
+ display: flex;
+ align-self: center;
+ width: 490px;
+ height: 36px;
+ padding-left: 10px;
+ font-family: 'Open Sans', sans-serif;
+ font-size: 15px;
+ font-weight: 300;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
+ mat-form-field {
+ width: 100%;
+ .mat-form-field-wrapper {
+ padding-bottom: 0;
+ }
+ .mat-icon {
+ font-size: 20px;
+ }
+ }
+ .dropdown-multiselect {
+ width: 100% !important;
+ > button {
+ padding: 6px 22px;
+ }
+ }
+}
+.list-header {
+ padding: 0 15px;
+}
+.scrolling-content {
+ max-height: 450px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ display: block;
+ padding: 15px 5px;
+ &.stepper-opened {
+ height: 250px;
+ }
+}
+.roles {
+ width: 30%;
+ padding: 0 10px;
+ .selector-wrapper-edit {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ height: 36px;
+ padding-left: 10px;
+ background: #fff;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
+ multi-select-dropdown {
+ width: 100%;
+ .dropdown-multiselect {
+ > button {
+ padding: 8px 22px 5px;
+ }
+ }
+ }
+ .caret {
+ &:hover {
+ box-shadow: 0 3px 1px -10px rgba(0, 0, 0, 0.2), 0 2px 1px 0 rgba(0, 0, 0, 0.17), 0 1px 5px 0 rgba(0, 0, 0, 0.12)
+ }
+ }
+ }
+}
+.groups {
+ width: 20%;
+}
+.users {
+ width: 30%;
+ padding: 0 10px;
+}
+.users-list {
+ padding: 5px 10px;
+ font-family: 'Open Sans', sans-serif;
+ font-size: 15px;
+ font-weight: 300;
+ color: #577289;
+ position: relative;
+ i {
+ color: #FF5722;
+ font-size: 18px;
+ cursor: pointer;
+ }
+ .list-selected {
+ width: 100%;
+ margin-top: 50px;
+ height: inherit;
+ min-height: 0;
+ overflow-y: hidden;
+ }
+}
+.expanded-panel {
+ display: flex;
+ align-items: flex-end;
+ .add-input-block {
+ display: flex;
+ height: 36px;
+ padding-right: 0;
+ outline: none;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
+ input {
+ height: 36px;
+ padding: 0;
+ padding-left: 10px;
+ width: 170px;
+ }
+ .caret {
+ width: 50px;
+ }
+ }
+}
+
+
+mat-expansion-panel-header {
+ &.mat-expansion-panel-header {
+ font-family: 'Open Sans', sans-serif;
+ font-size: 15px;
+ font-weight: 300;
+ color: #577289;
+ }
+}
+
+.mat-step-header {
+ .mat-step-icon {
+ background-color: #36afd5 !important;
+ }
+ .mat-step-label {
+ font-family: 'Open Sans', sans-serif;
+ font-size: 16px;
+ font-weight: 300;
+ }
+}
+
+.dashboard_table {
+ .dashboard_table_body {
+ .actions {
+ padding: 10px 0;
+ }
+ &.filter-row {
+ .actions {
+ button {
+ background: none;
+ .mid {
+ vertical-align: sub;
+ }
+ }
+ }
+ }
+ }
+ .th_actions {
+ width: 10%;
+ }
+}
+
+.mat-chip:not(.mat-basic-chip) {
+ transition: box-shadow 280ms cubic-bezier(.4,0,.2,1);
+ padding: 7px 0 7px 10px;
+ border-radius: 24px;
+ cursor: default;
+ display: inline-block;
+ position: relative;
+ padding-right: 25px;
+ white-space: nowrap;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 2px !important;
+}
+mat-chip.mat-chip a {
+ position: absolute;
+ right: 15px;
+}
+mat-form-field.chip-list {
+ &.mat-form-field {
+ position: absolute;
+ top: 2px;
+ width: 94%;
+ font-size: 16px;
+ font-weight: 300;
+ }
+}
+
+.mat-raised-button {
+ &.butt {
+ &.butt-success {
+ margin-left: 0;
+ }
+ }
+}
+.multiple-select {
+ border-bottom: 1px solid #dedbdb;
+ padding: 0;
+ a {
+ display: inline-block;
+ width: 50%;
+ padding: 0 15px;
+ vertical-align: middle;
+ color: #575757;
+ cursor: pointer;
+ i {
+ vertical-align: sub;
+ font-size: 20px;
+ }
+ &:hover {
+ color: #4eaf3e;
+ background: #f9fafb;
+ }
+ &.deselect {
+ &:hover {
+ color: #f1696e;
+ }
+ }
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts
new file mode 100644
index 0000000..bc91c94
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, ViewChild, Output, EventEmitter, Inject } from '@angular/core';
+import { ValidatorFn, FormControl, NgModel } from '@angular/forms';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+ selector: 'dlab-manage-roles-groups',
+ templateUrl: './manage-roles-groups.component.html',
+ styleUrls: ['../../../resources/resources-grid/resources-grid.component.css', './manage-roles-groups.component.scss']
+})
+export class ManageRolesGroupsComponent implements OnInit {
+ readonly DICTIONARY = DICTIONARY;
+
+ public groupsData: Array<any> = [];
+ public roles: Array<any> = [];
+ public rolesList: Array<string> = [];
+ public setupGroup: string = '';
+ public setupUser: string = '';
+ public manageUser: string = '';
+ public setupRoles: Array<string> = [];
+ public updatedRoles: Array<string> = [];
+ public delimitersRegex = /[-_]?/g;
+ public groupnamePattern = new RegExp(/^[a-zA-Z0-9_\-]+$/);
+
+ @ViewChild('bindDialog') bindDialog;
+ @Output() manageRolesGroupAction: EventEmitter<{}> = new EventEmitter();
+ stepperView: boolean = false;
+
+ constructor(public dialog: MatDialog) { }
+
+ ngOnInit() {
+ this.bindDialog.onClosing = () => this.resetDialog();
+ }
+
+ public open(param, groups, roles): void {
+ this.roles = roles;
+ this.rolesList = roles.map(role => role.description);
+ this.updateGroupData(groups);
+
+ this.stepperView = false;
+ this.bindDialog.open(param);
+ }
+
+ public onUpdate($event) {
+ if ($event.type === 'role') {
+ this.setupRoles = $event.model;
+ } else {
+ this.updatedRoles = $event.model;
+ }
+ $event.$event.preventDefault();
+ }
+
+ public selectAllOptions(item, values, byKey?) {
+ byKey ? (item[byKey] = values ? values : []) : this.setupRoles = values ? values : [];
+ }
+
+ public manageAction(action: string, type: string, item?: any, value?) {
+ if (action === 'create') {
+ this.manageRolesGroupAction.emit(
+ { action, type, value: {
+ name: this.setupGroup,
+ users: this.setupUser ? this.setupUser.split(',').map(elem => elem.trim()) : [],
+ roleIds: this.extractIds(this.roles, this.setupRoles)
+ }
+ });
+ this.stepperView = false;
+ } else if (action === 'delete') {
+ const data = (type === 'users') ? {group: item.group, user: value} : {group: item.group, id: item};
+ const dialogRef: MatDialogRef<ConfirmDeleteUserAccountDialogComponent> = this.dialog.open(
+ ConfirmDeleteUserAccountDialogComponent,
+ { data: data, width: '550px', panelClass: 'error-modalbox' }
+ );
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (result) {
+ const emitValue = (type === 'users')
+ ? {action, type, id: item.name, value: { user: value, group: item.group }}
+ : {action, type, id: item.name, value: item.group} ;
+ this.manageRolesGroupAction.emit(emitValue);
+ }
+ });
+ } else if (action === 'update') {
+ this.manageRolesGroupAction.emit({action, type, value: {
+ name: item.group,
+ roleIds: this.extractIds(this.roles, item.selected_roles),
+ users: item.users || [] }});
+ }
+ this.resetDialog();
+ }
+
+ public extractIds(sourceList, target) {
+ return sourceList.reduce((acc, item) => {
+ target.includes(item.description) && acc.push(item._id);
+ return acc;
+ }, []);
+ }
+
+ public updateGroupData(groups) {
+ this.groupsData = groups;
+
+ this.groupsData.forEach(item => {
+ item.selected_roles = item.roles.map(role => role.description);
+ });
+ }
+
+ public groupValidarion(): ValidatorFn {
+
+ const duplicateList: any = this.groupsData.map(item => item.group);
+ return <ValidatorFn>((control: FormControl) => {
+ if (control.value && duplicateList.includes(this.delimitersFiltering(control.value)))
+ return { duplicate: true };
+
+ if (control.value && !this.groupnamePattern.test(control.value))
+ return { patterns: true };
+
+ return null;
+ });
+ }
+
+ public compareObjects(o1: any, o2: any): boolean {
+ return o1.toLowerCase() === o2.toLowerCase();
+ }
+
+ public delimitersFiltering(resource): string {
+ return resource.replace(this.delimitersRegex, '').toString().toLowerCase();
+ }
+
+ public resetDialog() {
+ this.stepperView = false;
+ this.setupGroup = '';
+ this.setupUser = '';
+ this.manageUser = '';
+ this.setupRoles = [];
+ this.updatedRoles = [];
+ }
+
+ public removeUser(list, item): void {
+ list.splice(list.indexOf(item), 1);
+ }
+
+ public addUser(value: string, item): void {
+ if (value && value.trim()) {
+ item.users instanceof Array ? item.users.push(value.trim()) : item.users = [value.trim()];
+ }
+ }
+}
+
+
+@Component({
+ selector: 'dialog-result-example-dialog',
+ template: `
+ <div class="dialog-header">
+ <h4 class="modal-title"><i class="material-icons">priority_high</i>Warning</h4>
+ <button type="button" class="close" (click)="dialogRef.close()">×</button>
+ </div>
+ <div mat-dialog-content class="content">
+ <p *ngIf="data.user">User <strong>{{ data.user }}</strong> will be deleted from <strong>{{ data.group }}</strong> group.</p>
+ <p *ngIf="data.id">Group <strong>{{ data.group }}</strong> will be decommissioned.</p>
+ <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+ </div>
+ <div class="text-center">
+ <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+ <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+ </div>
+ `,
+ styles: []
+})
+export class ConfirmDeleteUserAccountDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef<ConfirmDeleteUserAccountDialogComponent>,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) { }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
new file mode 100644
index 0000000..6b8bb95
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
@@ -0,0 +1,161 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<table class="dashboard_table" *ngIf="isAdmin; else users">
+ <tr>
+ <th class="th_user">User</th>
+ <th class="th_name">Name</th>
+ <th class="th_status">Status</th>
+ <th class="th_shape">Shape / Resource id</th>
+ <th class="th_resources">Computational resources</th>
+ <th class="th_actions">Action</th>
+ </tr>
+
+ <tr *ngFor="let env of allEnvironmentData" class="dashboard_table_body">
+ <td>{{ env.user }}</td>
+ <td [ngClass]="{'capitalize': env.name === 'edge node'}">{{ env.name }}</td>
+ <td class="status" ngClass="{{env.status || ''}}">{{env.status}}</td>
+ <td>{{ env.shape || env.ip }}</td>
+ <td>
+ <div class="source" *ngIf="env.resources">
+ <div *ngIf="!env.resources?.length">
+ <span *ngIf="!env.resources.length" class="no-details">no details</span>
+ </div>
+ <div *ngIf="env.resources?.length">
+ <div *ngFor="let resource of env.resources" class="resource-wrap">
+ <div class="resource-name">
+ <a class="detailed-link">
+ {{ resource.computational_name }}
+ </a>
+ </div>
+ <span ngClass="{{resource.status || ''}}" class="resource-status">{{ resource.status }}</span>
+ <div class="resource-actions">
+ <a class="start-stop-action" *ngIf="resource.image === 'docker.dlab-dataengine'">
+ <i class="material-icons" (click)="toggleResourceAction(env, 'stop', resource)"
+ [ngClass]="{'not-allowed' : resource.status !== 'running' || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' }">pause_circle_outline</i>
+ </a>
+
+ <a class="remove_butt" (click)="toggleResourceAction(env, 'terminate', resource)"
+ [ngClass]="{ 'disabled' : env.status !== 'running' || resource.status !== 'running'
+ && resource.status !== 'stopped' || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' }">
+ <i class="material-icons">highlight_off</i>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </td>
+ <td class="settings">
+ <span #settings class="actions" (click)="actions.toggle($event, settings)" [ngClass]="{ 'disabled'
+ : (!uploadKey && env.type === 'notebook' && env.user.toLowerCase() === currentUser)
+ || (env.status !== 'running' && env.status !== 'stopped' && env.status !== 'stopping' && env.status !== 'failed' )
+ || (env.type === 'edge node' && env.user.toLowerCase() === currentUser && env.status === 'stopping')
+ || env.type === 'edge node' && env.user.toLowerCase() !== currentUser && env.status !== 'running'
+ || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' && env.type !== 'edge node' }"></span>
+ <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+ <ul class="list-unstyled">
+ <li
+ matTooltip="{{ env.type !== 'edge node' ? 'Unable to stop notebook because at least one computational resource is in progress' : 'Unable to stop edge node because at least one resource of this user is in progress' }}"
+ matTooltipPosition="above" [matTooltipDisabled]="!isResourcesInProgress(env)"
+ [hidden]="env.name === 'edge node' && env.status === 'stopped'">
+ <div (click)="toggleResourceAction(env, 'stop')"
+ [ngClass]="{'not-allowed' : env.status === 'stopped' || env.status === 'stopping' || env.status === 'starting' || env.status === 'creating image' || env.status === 'failed' || isResourcesInProgress(env)}">
+ <i class="material-icons">pause_circle_outline</i>
+ <span>Stop</span>
+ </div>
+ </li>
+ <li *ngIf="env.name !== 'edge node'"
+ matTooltip="Unable to terminate notebook because at least one computational resource is in progress"
+ matTooltipPosition="above" [matTooltipDisabled]="!isResourcesInProgress(env)">
+ <div (click)="toggleResourceAction(env, 'terminate')"
+ [ngClass]="{'not-allowed' : env.status !== 'running' && env.status !== 'stopped' || isResourcesInProgress(env)}">
+ <i class="material-icons">phonelink_off</i>
+ <span>Terminate</span>
+ </div>
+ </li>
+
+ <div *ngIf="env.name === 'edge node' && env.user.toLowerCase() === currentUser">
+ <li (click)="toggleResourceAction(env, 'run')" *ngIf="env.status === 'stopped'">
+ <i class="material-icons">play_circle_outline</i>
+ <span>Start</span>
+ </li>
+ <li (click)="toggleResourceAction(env, 'recreate')" *ngIf="env.status === 'terminated'">
+ <i class="material-icons">refresh</i>
+ <span>Recreate</span>
+ </li>
+ <li *ngIf="!isResourcesInProgress(env)" (click)="showReuploaKeydDialog()">
+ <i class="material-icons">sync_problem</i>
+ <span>Reupload key</span>
+ </li>
+ </div>
+ </ul>
+ </bubble-up>
+ </td>
+ </tr>
+</table>
+
+<ng-template #users>
+ <table class="dashboard_table">
+ <tr>
+ <th class="th_name">Type</th>
+ <th>Resource id</th>
+ <th class="th_status">Status</th>
+ <th class="th_actions">Action</th>
+ </tr>
+ <tr *ngFor="let env of environmentsHealthStatuses" class="dashboard_table_body">
+ <td>{{env.type}}</td>
+ <td>{{env.resource_id}}</td>
+ <td class="status" ngClass="{{env.status || ''}}">{{env.status}}</td>
+ <td class="settings">
+ <span #settings (click)="actions.toggle($event, settings)" class="actions" [ngClass]="{'disabled': env.status !== 'running'
+ && env.status !== 'stopped'
+ && env.status !== 'terminated'
+ || notebookInProgress
+ || !uploadKey && env.status !== 'failed' }">
+ </span>
+
+ <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+ <ul class="list-unstyled">
+ <li (click)="toggleResourceAction(env, 'stop')" *ngIf="env.status === 'running'">
+ <i class="material-icons">pause_circle_outline</i>
+ <span>Stop</span>
+ </li>
+ <li (click)="toggleResourceAction(env, 'run')" *ngIf="env.status === 'stopped'">
+ <i class="material-icons">play_circle_outline</i>
+ <span>Start</span>
+ </li>
+ <li (click)="toggleResourceAction(env, 'recreate')" *ngIf="env.status === 'terminated'">
+ <i class="material-icons">refresh</i>
+ <span>Recreate</span>
+ </li>
+ <li *ngIf="!anyEnvInProgress" (click)="showReuploaKeydDialog()">
+ <i class="material-icons">sync_problem</i>
+ <span>Reupload key</span>
+ </li>
+ </ul>
+ </bubble-up>
+ </td>
+ </tr>
+ </table>
+</ng-template>
+
+<confirmation-dialog #confirmationDialog [manageAction]="isAdmin" (buildGrid)="buildGrid()"></confirmation-dialog>
+<key-upload-dialog #keyReuploadDialog [primaryUploading]="false" (checkInfrastructureCreationProgress)="buildGrid()"
+ (generateUserKey)="generateUserKey()">
+</key-upload-dialog>
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
new file mode 100644
index 0000000..6198578
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
@@ -0,0 +1,40 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.dashboard_table {
+ .th_user,
+ .th_name {
+ width: 20%;
+ }
+ .th_shape,
+ .th_status {
+ width: 12% !important;
+ }
+ .th_resources {
+ width: 33%;
+ }
+ .dashboard_table_body {
+ td:first-child {
+ cursor: default;
+ }
+ }
+}
+.source .resource-wrap .resource-name .detailed-link {
+ cursor: default !important;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
new file mode 100644
index 0000000..7cc28e8
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, ViewChild, Input, Output, EventEmitter, Inject } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { ToastrService } from 'ngx-toastr';
+
+import { HealthStatusService, UserAccessKeyService } from '../../../core/services';
+import { ConfirmationDialogType } from '../../../shared';
+import { FileUtils } from '../../../core/util';
+
+export interface ManageAction {
+ action: string;
+ environment: string;
+ resource: string;
+}
+
+@Component({
+ selector: 'management-grid',
+ templateUrl: 'management-grid.component.html',
+ styleUrls: [
+ './management-grid.component.scss',
+ '../../../resources/resources-grid/resources-grid.component.css',
+ '../../../resources/computational/computational-resources-list/computational-resources-list.component.scss'
+ ]
+})
+export class ManagementGridComponent implements OnInit {
+ @Input() allEnvironmentData: Array<any>;
+ @Input() environmentsHealthStatuses: Array<any>;
+ @Input() healthStatus: string;
+ @Input() resources: Array<any>;
+ @Input() uploadKey: boolean;
+ @Input() isAdmin: boolean;
+ @Input() currentUser: string = '';
+ @Output() refreshGrid: EventEmitter<{}> = new EventEmitter();
+ @Output() actionToggle: EventEmitter<ManageAction> = new EventEmitter();
+
+ @ViewChild('confirmationDialog') confirmationDialog;
+ @ViewChild('keyReuploadDialog') keyReuploadDialog;
+
+ constructor(
+ private healthStatusService: HealthStatusService,
+ private userAccessKeyService: UserAccessKeyService,
+ public toastr: ToastrService,
+ public dialog: MatDialog
+ ) {}
+
+ ngOnInit() {
+ }
+
+ buildGrid(): void {
+ this.refreshGrid.emit();
+ }
+
+ toggleResourceAction(environment, action: string, resource?) {
+ if (resource) {
+ const resource_name = resource ? resource.computational_name : environment.name;
+ const dialogRef: MatDialogRef<ConfirmationDialogComponent> = this.dialog.open(ConfirmationDialogComponent, {
+ data: { action, resource_name, user: environment.user },
+ width: '550px',
+ panelClass: 'error-modalbox'
+ });
+ dialogRef.afterClosed().subscribe(result => {
+ result && this.actionToggle.emit({ action, environment, resource });
+ });
+ } else {
+ if (action === 'stop') {
+ this.confirmationDialog.open(
+ { isFooter: false },
+ environment,
+ (environment.name === 'edge node' || environment.type.toLowerCase() === 'edge node')
+ ? ConfirmationDialogType.StopEdgeNode
+ : ConfirmationDialogType.StopExploratory,
+ );
+ } else if (action === 'terminate') {
+ this.confirmationDialog.open({ isFooter: false }, environment, ConfirmationDialogType.TerminateExploratory);
+ } else if (action === 'run') {
+ this.healthStatusService
+ .runEdgeNode()
+ .subscribe(() => {
+ this.buildGrid();
+ this.toastr.success('Edge node is starting!', 'Processing!');
+ }, error => this.toastr.error('Edge Node running failed!', 'Oops!'));
+ } else if (action === 'recreate') {
+ this.healthStatusService
+ .recreateEdgeNode()
+ .subscribe(() => {
+ this.buildGrid();
+ this.toastr.success('Edge Node recreation is processing!', 'Processing!');
+ }, error => this.toastr.error('Edge Node recreation failed!', 'Oops!'));
+ }
+ }
+ }
+
+ isResourcesInProgress(notebook) {
+ if (notebook) {
+ if (notebook.name === 'edge node') {
+ return this.allEnvironmentData
+ .filter(env => env.user === notebook.user)
+ .some(el => this.inProgress([el]) || this.inProgress(el.resources));
+ } else if (notebook.resources.length) {
+ return this.inProgress(notebook.resources);
+ }
+ }
+ return false;
+ }
+
+ inProgress(resources) {
+ return resources.filter(resource => (
+ resource.status !== 'failed'
+ && resource.status !== 'terminated'
+ && resource.status !== 'running'
+ && resource.status !== 'stopped')).length > 0;
+ }
+
+ showReuploaKeydDialog() {
+ this.keyReuploadDialog.open({ isFooter: false });
+ }
+
+ public generateUserKey() {
+ this.userAccessKeyService.regenerateAccessKey().subscribe(data => {
+ FileUtils.downloadFile(data);
+ this.buildGrid();
+ });
+ }
+}
+
+
+@Component({
+ selector: 'confirm-dialog',
+ template: `
+ <div class="dialog-header">
+ <h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> resource</h4>
+ <button type="button" class="close" (click)="dialogRef.close()">×</button>
+ </div>
+ <div mat-dialog-content class="content">
+ <p>Resource <strong> {{ data.resource_name }}</strong> of user <strong> {{ data.user }} </strong> will be
+ <span *ngIf="data.action === 'terminate'"> decommissioned.</span>
+ <span *ngIf="data.action === 'stop'">stopped.</span>
+ </p>
+ <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+ </div>
+ <div class="text-center">
+ <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+ <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+ </div>
+ `,
+ styles: [
+ ]
+})
+export class ConfirmationDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) {}
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html
new file mode 100644
index 0000000..b0b2788
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html
@@ -0,0 +1,55 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<div class="sub-nav base-retreat">
+ <div *ngIf="healthStatus?.admin" class="admin-group">
+ <button mat-raised-button class="butt ssn" (click)="openManageRolesDialog()">
+ <i class="material-icons"></i>Manage roles
+ </button>
+ <button mat-raised-button class="butt ssn" (click)="openSsnMonitorDialog()">
+ <i class="material-icons"></i>SSN Monitor
+ </button>
+ <button mat-raised-button class="butt env" (click)="openManageEnvironmentDialog()">
+ <i class="material-icons"></i>Manage environment
+ </button>
+ <button mat-raised-button class="butt" (click)="showBackupDialog()" [disabled]="creatingBackup">
+ <i class="material-icons">backup</i>Backup
+ </button>
+ </div>
+ <button mat-raised-button class="butt" (click)="buildGrid()">
+ <i class="material-icons">autorenew</i>Refresh
+ </button>
+</div>
+
+<management-grid [currentUser]="user.toLowerCase()" [uploadKey]="uploadKey" [healthStatus]="healthStatus?.status" [isAdmin]="healthStatus?.admin" [allEnvironmentData]="allEnvironmentData" [environmentsHealthStatuses]="healthStatus?.list_resources" (refreshGrid)="buildGrid()"
+ (actionToggle)="manageEnvironmentAction($event)">
+</management-grid>
+
+
+<dlab-backup-dilog #backupDialog (backupOpts)="createBackup($event)">
+</dlab-backup-dilog>
+
+<dlab-manage-env-dilog #manageEnvDialog (manageEnv)="manageEnvironment($event)" (setBudget)="setBudgetLimits($event)">
+</dlab-manage-env-dilog>
+
+<progress-dialog #preloaderModal></progress-dialog>
+<dlab-ssn-monitor #ssnMonitor></dlab-ssn-monitor>
+
+<dlab-manage-roles-groups #rolesGroupsModal (manageRolesGroupAction)="manageRolesGroups($event)">
+</dlab-manage-roles-groups>
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss
new file mode 100644
index 0000000..ee18db9
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss
@@ -0,0 +1,37 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.sub-nav {
+ justify-content: flex-end;
+ .admin-group {
+ margin-right: 10px;
+ }
+ button {
+ margin-right: 10px;
+ &:last-child {
+ margin-right: 0;
+ }
+ &.env {
+ width: 210px;
+ }
+ &.ssn {
+ width: inherit;
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts
new file mode 100644
index 0000000..d898b84
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
+import { ToastrService } from 'ngx-toastr';
+import { Subscription } from 'rxjs';
+
+import {
+ HealthStatusService,
+ ManageEnvironmentsService,
+ UserAccessKeyService,
+ BackupService,
+ UserResourceService,
+ RolesGroupsService,
+ StorageService
+} from '../../core/services';
+
+import { EnvironmentModel, GeneralEnvironmentStatus } from './management.model';
+import { HTTP_STATUS_CODES } from '../../core/util';
+
+@Component({
+ selector: 'environments-management',
+ templateUrl: './management.component.html',
+ styleUrls: ['./management.component.scss']
+})
+export class ManagementComponent implements OnInit, OnDestroy {
+ public user: string = '';
+ public healthStatus: GeneralEnvironmentStatus;
+ public allEnvironmentData: Array<EnvironmentModel>;
+ public uploadKey: boolean = true;
+ public anyEnvInProgress: boolean = false;
+ public notebookInProgress: boolean = false;
+
+ private subscriptions: Subscription = new Subscription();
+ private clear = undefined;
+
+ @ViewChild('backupDialog') backupDialog;
+ @ViewChild('manageEnvDialog') manageEnvironmentDialog;
+ @ViewChild('keyUploadModal') keyUploadDialog;
+ @ViewChild('preloaderModal') preloaderDialog;
+ @ViewChild('ssnMonitor') ssnMonitorDialog;
+ @ViewChild('rolesGroupsModal') rolesGroupsDialog;
+
+ constructor(
+ private healthStatusService: HealthStatusService,
+ private backupService: BackupService,
+ private manageEnvironmentsService: ManageEnvironmentsService,
+ private userAccessKeyService: UserAccessKeyService,
+ private userResourceService: UserResourceService,
+ private rolesService: RolesGroupsService,
+ private storageService: StorageService,
+ public toastr: ToastrService
+ ) {}
+
+ ngOnInit() {
+ this.buildGrid();
+ this.user = this.storageService.getUserName();
+ this.subscriptions.add(this.userAccessKeyService.accessKeyEmitter
+ .subscribe(result => this.uploadKey = (result && result.status === 200)));
+
+ this.subscriptions.add(this.userAccessKeyService.keyUploadProccessEmitter.subscribe(response => {
+ if (response) this.buildGrid();
+ }));
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.unsubscribe();
+ }
+
+ public buildGrid() {
+ this.getEnvironmentHealthStatus();
+ }
+
+ public manageEnvironmentAction($event) {
+ this.manageEnvironmentsService
+ .environmentManagement(
+ $event.environment.user,
+ $event.action,
+ $event.environment.name === 'edge node' ? 'edge' : $event.environment.name,
+ $event.resource ? $event.resource.computational_name : null
+ ).subscribe(
+ () => this.buildGrid(),
+ error => this.toastr.error('Environment management failed!', 'Oops!'));
+ }
+
+ showBackupDialog() {
+ if (!this.backupDialog.isOpened) this.backupDialog.open({ isFooter: false });
+ }
+
+ getActiveUsersList() {
+ return this.healthStatusService.getActiveUsers();
+ }
+
+ getTotalBudgetData() {
+ return this.healthStatusService.getTotalBudgetData();
+ }
+
+ openManageEnvironmentDialog() {
+ this.getActiveUsersList().subscribe(usersList => {
+ this.getTotalBudgetData().subscribe(total => this.manageEnvironmentDialog.open({ isFooter: false }, usersList, total));
+ },
+ () => this.toastr.error('Failed users list loading!', 'Oops!'));
+ }
+
+ openSsnMonitorDialog() {
+ this.healthStatusService.getSsnMonitorData()
+ .subscribe(data => this.ssnMonitorDialog.open({ isFooter: false }, data),
+ () => this.toastr.error('Failed ssn data loading!', 'Oops!'));
+ }
+
+ openManageRolesDialog() {
+ this.rolesService.getGroupsData().subscribe(group => {
+ this.rolesService.getRolesData().subscribe(
+ roles => this.rolesGroupsDialog.open({ isFooter: false }, group, roles),
+ error => this.toastr.error(error.message, 'Oops!'));
+ },
+ error => this.toastr.error(error.message, 'Oops!'));
+ }
+
+ isEnvironmentsInProgress(data): boolean {
+ return data.exploratory.some(el => {
+ return el.status === 'creating' || el.status === 'starting' ||
+ el.computational_resources.some(elem => elem.status === 'creating' || elem.status === 'starting' || elem.status === 'configuring');
+ });
+ }
+
+ isNotebookInProgress(data): boolean {
+ return data.exploratory.some(el => el.status === 'creating');
+ }
+
+ createBackup($event) {
+ this.backupService.createBackup($event).subscribe(result => {
+ this.getBackupStatus(result);
+ this.toastr.success('Backup configuration is processing!', 'Processing!');
+ this.clear = window.setInterval(() => this.getBackupStatus(result), 3000);
+ },
+ error => this.toastr.error(error.message, 'Oops!'));
+ }
+
+ manageRolesGroups($event) {
+ switch ($event.action) {
+ case 'create':
+ this.rolesService.setupNewGroup($event.value).subscribe(res => {
+ this.toastr.success('Group creation success!', 'Created!');
+ this.getGroupsData();
+ }, () => this.toastr.error('Group creation failed!', 'Oops!'));
+ break;
+ case 'update':
+ this.rolesService.updateGroup($event.value).subscribe(res => {
+ this.toastr.success('Group data successfully updated!', 'Success!');
+ this.getGroupsData();
+ }, () => this.toastr.error('Failed group data updating!', 'Oops!'));
+ break;
+ case 'delete':
+ if ($event.type === 'users') {
+ this.rolesService.removeUsersForGroup($event.value).subscribe(res => {
+ this.toastr.success('Users was successfully deleted!', 'Success!');
+ this.getGroupsData();
+ }, () => this.toastr.error('Failed users deleting!', 'Oops!'));
+ } else if ($event.type === 'group') {
+ console.log('delete group');
+ this.rolesService.removeGroupById($event.value).subscribe(res => {
+ this.toastr.success('Group was successfully deleted!', 'Success!');
+ this.getGroupsData();
+ }, () => this.toastr.error('Failed group deleting!', 'Oops!'));
+ }
+ break;
+ default:
+ }
+ }
+
+ setBudgetLimits($event) {
+ this.healthStatusService.updateUsersBudget($event.users).subscribe((result: any) => {
+ this.healthStatusService.updateTotalBudgetData($event.total).subscribe((res: any) => {
+ result.status === HTTP_STATUS_CODES.OK
+ && res.status === HTTP_STATUS_CODES.NO_CONTENT
+ && this.toastr.success('Budget limits updated!', 'Success!');
+ this.buildGrid();
+ });
+ }, error => this.toastr.error(error.message, 'Oops!'));
+ }
+
+ getGroupsData() {
+ this.rolesService.getGroupsData().subscribe(
+ list => this.rolesGroupsDialog.updateGroupData(list),
+ error => this.toastr.error(error.message, 'Oops!'));
+ }
+
+ manageEnvironment(event: {action: string, user: string}) {
+ this.healthStatusService
+ .manageEnvironment(event.action, event.user)
+ .subscribe(res => {
+ this.getActiveUsersList().subscribe(usersList => {
+ this.manageEnvironmentDialog.usersList = usersList;
+ this.toastr.success(`Action ${ event.action } is processing!`, 'Processing!');
+ this.buildGrid();
+ });
+ },
+ error => this.toastr.error(error.message, 'Oops!'));
+ }
+
+ private getExploratoryList() {
+ this.userResourceService.getUserProvisionedResources()
+ .subscribe((result) => {
+ this.anyEnvInProgress = this.isEnvironmentsInProgress(result);
+ this.notebookInProgress = this.isNotebookInProgress(result);
+ });
+ }
+
+ private getBackupStatus(result) {
+ const uuid = result.body;
+ this.backupService.getBackupStatus(uuid)
+ .subscribe((backupStatus: any) => {
+ if (!this.creatingBackup) {
+ backupStatus.status === 'FAILED'
+ ? this.toastr.error('Backup configuration failed!', 'Oops!')
+ : this.toastr.success('Backup configuration completed!', 'Success!');
+ clearInterval(this.clear);
+ }
+ }, () => {
+ clearInterval(this.clear);
+ this.toastr.error('Backup configuration failed!', 'Oops!');
+ });
+ }
+
+ get creatingBackup(): boolean {
+ return this.backupService.inProgress;
+ }
+
+ private getAllEnvironmentData() {
+ this.manageEnvironmentsService
+ .getAllEnvironmentData()
+ .subscribe((result: Array<EnvironmentModel>) => this.allEnvironmentData = this.loadEnvironmentList(result));
+ }
+
+ private loadEnvironmentList(data): Array<EnvironmentModel> {
+ if (data)
+ return data.map(value => new EnvironmentModel(
+ value.resource_name || value.resource_type,
+ value.status,
+ value.shape,
+ value.computational_resources,
+ value.user,
+ value.public_ip,
+ value.resource_type
+ ));
+ }
+
+ private getEnvironmentHealthStatus() {
+ this.healthStatusService
+ .getEnvironmentStatuses()
+ .subscribe((status: GeneralEnvironmentStatus) => {
+ this.healthStatus = status;
+ this.healthStatus.admin && this.getAllEnvironmentData();
+ this.userAccessKeyService.initialUserAccessKeyCheck();
+ this.getExploratoryList();
+ });
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
new file mode 100644
index 0000000..51ab721
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export class EnvironmentModel {
+ constructor(
+ public name: string,
+ public status: string,
+ public shape: string,
+ public resources: Array<any>,
+ public user: string,
+ public ip: string,
+ public type?: string
+ ) { }
+}
+
+export class BackupOptionsModel {
+ constructor(
+ public configFiles: Array<string>,
+ public keys: Array<string>,
+ public certificates: Array<string>,
+ public jars: Array<string>,
+ public databaseBackup: boolean,
+ public logsBackup: boolean
+ ) { }
+
+ setDegault(): void {
+ this.configFiles = ['all'];
+ this.keys = ['all'];
+ this.certificates = ['skip'];
+ this.jars = ['skip'];
+ this.databaseBackup = true;
+ this.logsBackup = false;
+ }
+}
+
+export interface GeneralEnvironmentStatus {
+ admin: boolean;
+ billingEnabled: boolean;
+ billingQuoteUsed: number;
+ list_resources: any;
+ status: string;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html
new file mode 100644
index 0000000..3d13266
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html
@@ -0,0 +1,117 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<modal-dialog #bindDialog modalClass="ssn-monitor-dialog modal-lg">
+ <modal-header>
+ <h4 class="modal-title">SSN Monitor</h4>
+ </modal-header>
+ <modal-content>
+ <div class="content-box" *ngIf="monitorData">
+ <div class="ssn-info">
+ <mat-tab-group *ngIf="monitorData?.processorInfo" [dynamicHeight]="true">
+ <mat-tab label="CPU">
+ <div class="scrolling-content" id="scrolling">
+ <mat-list-item class="list-header">
+ <div class="col">Name</div>
+ <div class="col">{{ monitorData.processorInfo.name }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Vendor</div>
+ <div class="col">{{ monitorData.processorInfo.vendor }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Logical Core Count</div>
+ <div class="col">{{ monitorData.processorInfo.logicalCoreCount }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Physical Core Count</div>
+ <div class="col">{{ monitorData.processorInfo.physicalCoreCount }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Current System Load</div>
+ <div class="col">{{ monitorData.processorInfo.currentSystemLoad /100 | percent:'1.0-2' }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">System Load Average</div>
+ <div class="col">{{ monitorData.processorInfo.systemLoadAverage /100 | percent:'1.0-2' }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">CPU 64 Bit</div>
+ <div class="col">{{ monitorData.processorInfo.cpu64Bit }}</div>
+ </mat-list-item>
+ </div>
+ </mat-tab>
+
+ <mat-tab label="Memory">
+ <div class="scrolling-content" id="scrolling">
+ <mat-list-item class="list-header">
+ <div class="col">Available Memory</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.availableMemory) }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Total Memory</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.totalMemory) }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Swap Total</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.swapTotal) }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Swap Used</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.swapUsed) }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Pages Page In</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.pagesPageIn) }}</div>
+ </mat-list-item>
+ <mat-list-item class="list-header">
+ <div class="col">Pages Page Out</div>
+ <div class="col">{{ convertSize(monitorData.memoryInfo.pagesPageOut) }}</div>
+ </mat-list-item>
+ </div>
+
+ </mat-tab>
+ <mat-tab label="HDD">
+ <div class="scrolling-content" id="scrolling">
+ <div *ngFor="let disk of monitorData.disksInfo; let i = index">
+ <mat-list-item>
+ <div class="col"><strong>Disk {{i +1}}</strong></div>
+ </mat-list-item>
+ <mat-list-item>
+ <div class="col">Used Space</div>
+ <div class="col">{{ convertSize(disk.usedByteSpace) }}</div>
+ </mat-list-item>
+ <mat-list-item>
+ <div class="col">Total Space</div>
+ <div class="col">{{ convertSize(disk.totalByteSpace) }}</div>
+ </mat-list-item>
+ </div>
+ </div>
+ </mat-tab>
+ </mat-tab-group>
+ <div class="text-center">
+ <button type="button" class="butt" mat-raised-button (click)="close()">Close</button>
+ </div>
+ </div>
+ <div class="info message" *ngIf="isEmpty(monitorData)">
+ No ssn monitor data available
+ </div>
+ </div>
+ </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss
new file mode 100644
index 0000000..769fefe
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss
@@ -0,0 +1,50 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.ssn-monitor-dialog {
+ .content-box {
+ padding-top: 10px !important;
+ }
+ .ssn-info {
+ min-height: 400px;
+ max-height: 500px;
+ .scrolling-content {
+ max-height: 310px;
+ overflow-y: auto;
+ }
+ .text-center {
+ position: absolute;
+ bottom: 20px;
+ left: 0;
+ right: 0;
+ }
+ }
+ .mat-list-item-content {
+ display: flex;
+ justify-content: initial;
+ color: #577289;
+ padding: 15px 5px;
+ border-bottom: 1px solid #f3f2f2;
+ font-size: 15px;
+ .col {
+ width: 50%;
+ font-weight: 300;
+ }
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts
new file mode 100644
index 0000000..3d95499
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, ViewChild, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+ selector: 'dlab-ssn-monitor',
+ templateUrl: './ssn-monitor.component.html',
+ styleUrls: ['./ssn-monitor.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class SsnMonitorComponent implements OnInit {
+ readonly DICTIONARY = DICTIONARY;
+
+ public errorMessage: string = '';
+ public monitorData = {};
+
+ @ViewChild('bindDialog') bindDialog;
+ @Output() manageEnv: EventEmitter<{}> = new EventEmitter();
+
+ ngOnInit() {}
+
+ public open(param, data): void {
+ this.monitorData = data || {};
+ this.bindDialog.open(param);
+ }
+ public close(param, data): void {
+ this.bindDialog.close();
+ }
+
+ public isEmpty(obj) {
+ if (obj) return Object.keys(obj).length === 0;
+ }
+
+ public convertSize(bytes) {
+ if (Number(bytes) === 0) return '0 Byte';
+
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return parseFloat((bytes / Math.pow(1024, i)).toFixed(3)) + ' ' + sizes[i];
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
new file mode 100644
index 0000000..6b8e0f4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
+
+import { ProjectComponent } from './project.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ MaterialModule,
+ FormControlsModule,
+ ],
+ declarations: [ProjectComponent],
+ exports: [ProjectComponent]
+})
+export class ProjectModule { }
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html
new file mode 100644
index 0000000..4d4b0d8
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html
@@ -0,0 +1,19 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss
new file mode 100644
index 0000000..7220975
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
new file mode 100644
index 0000000..75c222f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'dlab-project',
+ templateUrl: './project.component.html',
+ styleUrls: ['./project.component.scss']
+})
+export class ProjectComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@dlab.apache.org
For additional commands, e-mail: commits-help@dlab.apache.org