Merge pull request #12988 from AllForNothing/test-4

Fix UI issues found on testing day
This commit is contained in:
Will Sun 2020-09-08 01:12:10 +08:00 committed by GitHub
commit 5586fe86bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 91 additions and 48 deletions

View File

@ -265,7 +265,7 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
} }
changeAutoOnBoard() { changeAutoOnBoard() {
if (!this.currentConfig.oidc_auto_onboard.value) { if (!this.currentConfig.oidc_auto_onboard.value) {
this.currentConfig.oidc_user_claim.value = null; this.currentConfig.oidc_user_claim.value = "";
} }
} }
} }

View File

@ -91,7 +91,7 @@
<label for="create_project_storage_limit" class="clr-control-label"></label> <label for="create_project_storage_limit" class="clr-control-label"></label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<label class="clr-control-label endpoint">{{ 'PROJECT.ENDPOINT' | translate }}</label> <label class="clr-control-label endpoint required">{{ 'PROJECT.ENDPOINT' | translate }}</label>
<input placeholder="http(s)://192.168.1.1" [value]="getEndpoint()" readonly class="clr-input" type="text" id="endpoint" <input placeholder="http(s)://192.168.1.1" [value]="getEndpoint()" readonly class="clr-input" type="text" id="endpoint"
autocomplete="off"> autocomplete="off">
</div> </div>

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Subscription, forkJoin } from "rxjs"; import {Subscription, forkJoin, of} from "rxjs";
import { import {
Component, Component,
Output, Output,
@ -35,6 +35,7 @@ import { calculatePage, CustomComparator, doFiltering, doSorting } from "../../.
import { OperationService } from "../../../lib/components/operation/operation.service"; import { OperationService } from "../../../lib/components/operation/operation.service";
import { operateChanges, OperateInfo, OperationState } from "../../../lib/components/operation/operate"; import { operateChanges, OperateInfo, OperationState } from "../../../lib/components/operation/operate";
import { errorHandler } from "../../../lib/utils/shared/shared.utils"; import { errorHandler } from "../../../lib/utils/shared/shared.utils";
import {HttpErrorResponse} from "@angular/common/http";
@Component({ @Component({
selector: "list-project", selector: "list-project",
@ -204,10 +205,22 @@ export class ListProjectComponent implements OnDestroy {
projects.forEach(data => { projects.forEach(data => {
observableLists.push(this.delOperate(data)); observableLists.push(this.delOperate(data));
}); });
forkJoin(...observableLists).subscribe(item => { forkJoin(...observableLists).subscribe(resArr => {
this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => { let error;
this.msgHandler.showSuccess(res); if (resArr && resArr.length) {
}); resArr.forEach(item => {
if (item instanceof HttpErrorResponse) {
error = errorHandler(item);
}
});
}
if (error) {
this.msgHandler.handleError(error);
} else {
this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => {
this.msgHandler.showSuccess(res);
});
}
let st: State = this.getStateAfterDeletion(); let st: State = this.getStateAfterDeletion();
this.selectedRow = []; this.selectedRow = [];
if (!st) { if (!st) {
@ -216,8 +229,6 @@ export class ListProjectComponent implements OnDestroy {
this.clrLoad(st); this.clrLoad(st);
this.statisticHandler.refresh(); this.statisticHandler.refresh();
} }
}, error => {
this.msgHandler.handleError(error);
}); });
} }
} }
@ -237,10 +248,10 @@ export class ListProjectComponent implements OnDestroy {
}), catchError( }), catchError(
error => { error => {
const message = errorHandler(error); const message = errorHandler(error);
this.translateService.get(message).subscribe(res => this.translateService.get(message).subscribe(res => {
operateChanges(operMessage, OperationState.failure, res) operateChanges(operMessage, OperationState.failure, res);
); });
return observableThrowError(error); return of(error);
})); }));
} }

View File

@ -1,5 +1,5 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 config-top "> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 config-top ">
<hbr-project-policy-config [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn"></hbr-project-policy-config> <hbr-project-policy-config [projectId]="projectId" [projectName]="projectName" [isProxyCacheProject]="isProxyCacheProject" [hasSignedIn]="hasSignedIn"></hbr-project-policy-config>
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@ export class ProjectConfigComponent implements OnInit {
projectName: string; projectName: string;
currentUser: SessionUser; currentUser: SessionUser;
hasSignedIn: boolean; hasSignedIn: boolean;
isProxyCacheProject: boolean = false;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
@ -42,6 +42,9 @@ export class ProjectConfigComponent implements OnInit {
if (resolverData) { if (resolverData) {
let pro: Project = <Project>resolverData['projectResolver']; let pro: Project = <Project>resolverData['projectResolver'];
this.projectName = pro.name; this.projectName = pro.name;
if (pro.registry_id) {
this.isProxyCacheProject = true;
}
} }
} }
} }

View File

@ -26,7 +26,7 @@
</div> </div>
<ng-container *ngIf="!loading"> <ng-container *ngIf="!loading">
<!-- tags --> <!-- tags -->
<artifact-tag [artifactDetails]="artifact" [projectName]="projectName" [projectId]="projectId" [repositoryName]="repositoryName" <artifact-tag [artifactDetails]="artifact" [projectName]="projectName" [isProxyCacheProject]="isProxyCacheProject" [projectId]="projectId" [repositoryName]="repositoryName"
></artifact-tag> ></artifact-tag>
<!-- Overview --> <!-- Overview -->

View File

@ -30,6 +30,7 @@ export class ArtifactSummaryComponent implements OnInit {
@Output() @Output()
backEvt: EventEmitter<any> = new EventEmitter<any>(); backEvt: EventEmitter<any> = new EventEmitter<any>();
projectName: string; projectName: string;
isProxyCacheProject: boolean = false;
loading: boolean = false; loading: boolean = false;
constructor( constructor(
@ -80,6 +81,9 @@ export class ArtifactSummaryComponent implements OnInit {
if (resolverData) { if (resolverData) {
const pro: Project = <Project>(resolverData['artifactResolver'][1]); const pro: Project = <Project>(resolverData['artifactResolver'][1]);
this.projectName = pro.name; this.projectName = pro.name;
if (pro.registry_id) {
this.isProxyCacheProject = true;
}
this.artifact = <Artifact>(resolverData['artifactResolver'][0]); this.artifact = <Artifact>(resolverData['artifactResolver'][0]);
this.getIconFromBackEnd(); this.getIconFromBackEnd();
} }

View File

@ -2,7 +2,7 @@
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getCurrentArtifactTags($event)" [(clrDgSelected)]="selectedRow"> <clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getCurrentArtifactTags($event)" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar> <clr-dg-action-bar>
<button id="new-tag" type="button" [disabled]="!hasCreateTagPermission" class="btn btn-secondary" (click)="addTag()"> <button *ngIf="!isProxyCacheProject" id="new-tag" type="button" [disabled]="!hasCreateTagPermission" class="btn btn-secondary" (click)="addTag()">
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'TAG.ADD_TAG' | translate}} <clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'TAG.ADD_TAG' | translate}}
</button> </button>
<button id="delete-tag" type="button" class="btn btn-secondary" [disabled]="!(selectedRow.length>=1&& !hasImmutableOnTag() && hasDeleteTagPermission)" (click)="removeTag()"> <button id="delete-tag" type="button" class="btn btn-secondary" [disabled]="!(selectedRow.length>=1&& !hasImmutableOnTag() && hasDeleteTagPermission)" (click)="removeTag()">

View File

@ -34,7 +34,7 @@ class InitTag {
name = ""; name = "";
} }
const DeleteTagWithNotoryCommand1 = 'notary -s https://'; const DeleteTagWithNotoryCommand1 = 'notary -s https://';
const DeleteTagWithNotoryCommand2 = ':4443 -d ~/.docker/trust remove -p '; const DeleteTagWithNotoryCommand2 = ' -d ~/.docker/trust remove -p ';
@Component({ @Component({
selector: 'artifact-tag', selector: 'artifact-tag',
templateUrl: './artifact-tag.component.html', templateUrl: './artifact-tag.component.html',
@ -43,6 +43,7 @@ const DeleteTagWithNotoryCommand2 = ':4443 -d ~/.docker/trust remove -p ';
export class ArtifactTagComponent implements OnInit, OnDestroy { export class ArtifactTagComponent implements OnInit, OnDestroy {
@Input() artifactDetails: Artifact; @Input() artifactDetails: Artifact;
@Input() projectName: string; @Input() projectName: string;
@Input() isProxyCacheProject: boolean = false;
@Input() projectId: number; @Input() projectId: number;
@Input() repositoryName: string; @Input() repositoryName: string;
newTagName = new InitTag(); newTagName = new InitTag();

View File

@ -9,11 +9,11 @@ import {
SimpleChanges, SimpleChanges,
Inject, OnDestroy Inject, OnDestroy
} from "@angular/core"; } from "@angular/core";
import { forkJoin, Subscription } from "rxjs"; import {forkJoin, of, Subscription} from "rxjs";
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs/operators"; import { debounceTime, distinctUntilChanged, switchMap } from "rxjs/operators";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { map, catchError } from "rxjs/operators"; import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs"; import { Observable } from "rxjs";
import { ClrDatagridStateInterface } from "@clr/angular"; import { ClrDatagridStateInterface } from "@clr/angular";
import { import {
RepositoryService as NewRepositoryService RepositoryService as NewRepositoryService
@ -49,6 +49,7 @@ import { SessionService } from "../../shared/session.service";
import { GridViewComponent } from "./gridview/grid-view.component"; import { GridViewComponent } from "./gridview/grid-view.component";
import { Repository as NewRepository } from "../../../../ng-swagger-gen/models/repository"; import { Repository as NewRepository } from "../../../../ng-swagger-gen/models/repository";
import { StrictHttpResponse as __StrictHttpResponse } from '../../../../ng-swagger-gen/strict-http-response'; import { StrictHttpResponse as __StrictHttpResponse } from '../../../../ng-swagger-gen/strict-http-response';
import {HttpErrorResponse} from "@angular/common/http";
@Component({ @Component({
@ -192,8 +193,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
} }
confirmDeletion(message: ConfirmationAcknowledgement) { confirmDeletion(message: ConfirmationAcknowledgement) {
this.loading = true;
// forkJoin(...repArr).subscribe(() => {
if (message && if (message &&
message.source === ConfirmationTargets.REPOSITORY && message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) { message.state === ConfirmationState.CONFIRMED) {
@ -203,19 +202,29 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
repoLists.forEach(repo => { repoLists.forEach(repo => {
observableLists.push(this.delOperate(repo)); observableLists.push(this.delOperate(repo));
}); });
forkJoin(observableLists).subscribe((item) => { forkJoin(observableLists).subscribe(resArr => {
let error;
if (resArr && resArr.length) {
resArr.forEach(item => {
if (item instanceof HttpErrorResponse) {
error = errorHandler(item);
}
});
}
if (error) {
this.errorHandlerService.error(error);
} else {
this.translateService.get("BATCH.DELETED_SUCCESS").subscribe(res => {
this.errorHandlerService.info(res);
});
}
this.selectedRow = []; this.selectedRow = [];
this.refresh();
let st: ClrDatagridStateInterface = this.getStateAfterDeletion(); let st: ClrDatagridStateInterface = this.getStateAfterDeletion();
if (!st) { if (!st) {
this.refresh(); this.refresh();
} else { } else {
this.clrLoad(st); this.clrLoad(st);
} }
}, error => {
this.errorHandlerService.error(error);
this.loading = false;
this.refresh();
}); });
} }
} }
@ -243,10 +252,10 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
}); });
}), catchError(error => { }), catchError(error => {
const message = errorHandler(error); const message = errorHandler(error);
this.translateService.get(message).subscribe(res => this.translateService.get(message).subscribe(res => {
operateChanges(operMessage, OperationState.failure, res) operateChanges(operMessage, OperationState.failure, res);
); });
return observableThrowError(error); return of(error);
})); }));
} }

View File

@ -117,7 +117,7 @@
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.status}}</clr-dg-cell> <clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.status}}</clr-dg-cell>
<clr-dg-cell class="hand" <clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.dry_run ? 'YES' : 'NO'}}</clr-dg-cell> (click)="openDetail(i,execution.id)">{{(execution.dry_run ? 'TAG_RETENTION.YES' : 'TAG_RETENTION.NO') | translate}}</clr-dg-cell>
<clr-dg-cell class="hand" <clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.trigger}}</clr-dg-cell> (click)="openDetail(i,execution.id)">{{execution.trigger}}</clr-dg-cell>
<clr-dg-cell class="hand" <clr-dg-cell class="hand"

View File

@ -85,7 +85,7 @@ body {
} }
.color-green { .color-green {
color: #1d5100; color: #00d40f;
} }
.color-red { .color-red {

View File

@ -1330,7 +1330,9 @@
"COUNT_LARGE": "Parameter \"COUNT\" is too large", "COUNT_LARGE": "Parameter \"COUNT\" is too large",
"DAYS_LARGE": "Parameter \"DAYS\" is too large", "DAYS_LARGE": "Parameter \"DAYS\" is too large",
"EXECUTION_TYPE": "Execution Type", "EXECUTION_TYPE": "Execution Type",
"ACTION": "ACTION" "ACTION": "ACTION",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "Immutability rules", "IMMUTABLE_RULES": "Immutability rules",

View File

@ -1328,7 +1328,9 @@
"COUNT_LARGE": "Parameter \"COUNT\" is too large", "COUNT_LARGE": "Parameter \"COUNT\" is too large",
"DAYS_LARGE": "Parameter \"DAYS\" is too large", "DAYS_LARGE": "Parameter \"DAYS\" is too large",
"EXECUTION_TYPE": "Execution Type", "EXECUTION_TYPE": "Execution Type",
"ACTION": "ACTION" "ACTION": "ACTION",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "Immutability rules", "IMMUTABLE_RULES": "Immutability rules",

View File

@ -1298,7 +1298,9 @@
"COUNT_LARGE": "Parameter \"COUNT\" is too large", "COUNT_LARGE": "Parameter \"COUNT\" is too large",
"DAYS_LARGE": "Parameter \"DAYS\" is too large", "DAYS_LARGE": "Parameter \"DAYS\" is too large",
"EXECUTION_TYPE": "Execution Type", "EXECUTION_TYPE": "Execution Type",
"ACTION": "ACTION" "ACTION": "ACTION",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "Immutability rules", "IMMUTABLE_RULES": "Immutability rules",

View File

@ -1326,7 +1326,9 @@
"COUNT_LARGE": "Parameter \"COUNT\" is too large", "COUNT_LARGE": "Parameter \"COUNT\" is too large",
"DAYS_LARGE": "Parameter \"DAYS\" is too large", "DAYS_LARGE": "Parameter \"DAYS\" is too large",
"EXECUTION_TYPE": "Execution Type", "EXECUTION_TYPE": "Execution Type",
"ACTION": "ACTION" "ACTION": "ACTION",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "Immutability rules", "IMMUTABLE_RULES": "Immutability rules",

View File

@ -1330,7 +1330,9 @@
"COUNT_LARGE": "Parameter \"COUNT\" is too large", "COUNT_LARGE": "Parameter \"COUNT\" is too large",
"DAYS_LARGE": "Parameter \"DAYS\" is too large", "DAYS_LARGE": "Parameter \"DAYS\" is too large",
"EXECUTION_TYPE": "Execution Type", "EXECUTION_TYPE": "Execution Type",
"ACTION": "ACTION" "ACTION": "ACTION",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "Immutability rules", "IMMUTABLE_RULES": "Immutability rules",

View File

@ -1327,7 +1327,9 @@
"COUNT_LARGE": "参数“个数”太大", "COUNT_LARGE": "参数“个数”太大",
"DAYS_LARGE": "参数“天数”太大", "DAYS_LARGE": "参数“天数”太大",
"EXECUTION_TYPE": "执行类型", "EXECUTION_TYPE": "执行类型",
"ACTION": "操作" "ACTION": "操作",
"YES": "是",
"NO": "否"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "不可变的 Tag 规则", "IMMUTABLE_RULES": "不可变的 Tag 规则",

View File

@ -1314,7 +1314,9 @@
"COUNT_LARGE": "參數“個數”太大", "COUNT_LARGE": "參數“個數”太大",
"DAYS_LARGE": "參數“天數”太大", "DAYS_LARGE": "參數“天數”太大",
"EXECUTION_TYPE": "執行類型", "EXECUTION_TYPE": "執行類型",
"ACTION": "操作" "ACTION": "操作",
"YES": "Yes",
"NO": "No"
}, },
"IMMUTABLE_TAG": { "IMMUTABLE_TAG": {
"IMMUTABLE_RULES": "不可變的 Tag 規則", "IMMUTABLE_RULES": "不可變的 Tag 規則",

View File

@ -13,7 +13,7 @@
<clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'> <clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'>
<clr-dg-cell>{{job.id }}</clr-dg-cell> <clr-dg-cell>{{job.id }}</clr-dg-cell>
<clr-dg-cell>{{(job.type ? 'SCHEDULE.'+ job.type.toUpperCase() : '') | translate }}</clr-dg-cell> <clr-dg-cell>{{(job.type ? 'SCHEDULE.'+ job.type.toUpperCase() : '') | translate }}</clr-dg-cell>
<clr-dg-cell>{{isDryRun(job?.parameters)}}</clr-dg-cell> <clr-dg-cell>{{isDryRun(job?.parameters) | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell> <clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.createTime | date:'medium'}}</clr-dg-cell> <clr-dg-cell>{{job.createTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>{{job.updateTime | date:'medium'}}</clr-dg-cell> <clr-dg-cell>{{job.updateTime | date:'medium'}}</clr-dg-cell>

View File

@ -9,8 +9,8 @@ const JOB_STATUS = {
PENDING: "pending", PENDING: "pending",
RUNNING: "running" RUNNING: "running"
}; };
const YES: string = 'Yes'; const YES: string = 'TAG_RETENTION.YES';
const NO: string = 'No'; const NO: string = 'TAG_RETENTION.NO';
@Component({ @Component({
selector: 'gc-history', selector: 'gc-history',
templateUrl: './gc-history.component.html', templateUrl: './gc-history.component.html',

View File

@ -10,9 +10,9 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.PUBLIC_POLICY' | translate }} <clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.PUBLIC_POLICY' | translate }}
</clr-control-helper> </clr-control-helper>
</clr-checkbox-container> </clr-checkbox-container>
<clr-checkbox-container *ngIf="withNotary"> <clr-checkbox-container *ngIf="withNotary && !isProxyCacheProject">
<label><span *ngIf="withNotary">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label> <label><span *ngIf="withNotary && !isProxyCacheProject">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label>
<clr-checkbox-wrapper *ngIf="withNotary"> <clr-checkbox-wrapper *ngIf="withNotary && !isProxyCacheProject">
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ContentTrust" name="content-trust" <input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.ContentTrust" name="content-trust"
[disabled]="!hasChangeConfigRole" /> [disabled]="!hasChangeConfigRole" />
<label>{{ 'PROJECT_CONFIG.CONTENT_TRUST_TOGGLE' | translate }}</label> <label>{{ 'PROJECT_CONFIG.CONTENT_TRUST_TOGGLE' | translate }}</label>
@ -21,7 +21,7 @@
</clr-control-helper> </clr-control-helper>
</clr-checkbox-container> </clr-checkbox-container>
<clr-checkbox-container id="prevent-vulenrability-image" class="margin-top-05"> <clr-checkbox-container id="prevent-vulenrability-image" class="margin-top-05">
<label><span *ngIf="!withNotary">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label> <label><span *ngIf="!(withNotary && !isProxyCacheProject)">{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label>
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.PreventVulImg" <input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.PreventVulImg"
name="prevent-vulenrability-image-input" [disabled]="!hasChangeConfigRole" /> name="prevent-vulenrability-image-input" [disabled]="!hasChangeConfigRole" />

View File

@ -57,6 +57,7 @@ export class ProjectPolicyConfigComponent implements OnInit {
onGoing = false; onGoing = false;
@Input() projectId: number; @Input() projectId: number;
@Input() projectName = 'unknown'; @Input() projectName = 'unknown';
@Input() isProxyCacheProject: boolean = false;
@Input() hasSignedIn: boolean; @Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;