Update project summary page (#14874)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Will Sun 2021-05-17 10:39:33 +08:00 committed by GitHub
parent e553cbe795
commit 0a8ff4c1f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 633 additions and 193 deletions

View File

@ -1,18 +1,15 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { SignInComponent } from './sign-in.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AppConfigService } from '../../services/app-config.service';
import { SessionService } from '../../shared/services/session.service';
import { CookieService } from 'ngx-cookie';
import { SkinableConfig } from "../../services/skinable-config.service";
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { ClarityModule } from "@clr/angular";
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { of } from "rxjs";
import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError';
import { HttpErrorResponse } from '@angular/common/http';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { SharedTestingModule } from "../../shared/shared.module";
import { UserPermissionService } from "../../shared/services";
describe('SignInComponent', () => {
let component: SignInComponent;
@ -20,21 +17,23 @@ describe('SignInComponent', () => {
const mockedSessionService = {
signIn() {
return of(true);
},
getCurrentUser() {
return {};
}
};
const mockedUserPermissionService = {
clearPermissionCache() {
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule,
ClarityModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule
SharedTestingModule
],
declarations: [SignInComponent],
providers: [
TranslateService,
{ provide: UserPermissionService, useValue: mockedUserPermissionService},
{ provide: SessionService, useValue: mockedSessionService},
{
provide: AppConfigService, useValue: {
@ -42,6 +41,11 @@ describe('SignInComponent', () => {
return of({
});
},
isIntegrationMode() {
},
getConfig() {
return {};
}
}
},

View File

@ -28,6 +28,7 @@ import {modalEvents} from "../../base/modal-events.const";
import {AboutDialogComponent} from "../../shared/components/about-dialog/about-dialog.component";
import { CommonRoutes, CONFIG_AUTH_MODE } from "../../shared/entities/shared.const";
import { SignInCredential } from "./sign-in-credential";
import { UserPermissionService } from "../../shared/services";
// Define status flags for signing in states
export const signInStatusNormal = 0;
@ -74,7 +75,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private cookie: CookieService,
private skinableConfig: SkinableConfig) { }
private skinableConfig: SkinableConfig,
private userPermissionService: UserPermissionService) { }
ngOnInit(): void {
// custom skin
@ -244,7 +246,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// Set status
// Keep it ongoing to keep the button 'disabled'
// this.signInStatus = signInStatusNormal;
// clear permissions cache
this.userPermissionService.clearPermissionCache();
// Remeber me
this.remeberMe();

View File

@ -69,7 +69,7 @@ export class StatisticsPanelComponent implements OnInit, OnDestroy {
if (this.originalCopy) {
return getSizeNumber(this.originalCopy.total_storage_consumption);
}
return null;
return 0;
}
getSizeUnit(): number | string {
if (this.originalCopy) {

View File

@ -1,9 +1,10 @@
<div>
<div class="breadcrumb">
<a (click)="gotoProjectList()"> {{ 'SIDE_NAV.PROJECTS'| translate}} </a>
&lt;
<span class="back-icon"><</span>
<a (click)="gotoProjectList()">{{ 'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="gotoChartList()">{{ projectName }}</a>
&lt;
<span class="back-icon"><</span>
<a (click)="gotoChartVersion()">{{ 'HELM_CHART.CHARTVERSIONS'| translate}}</a>
</div>
<hbr-chart-detail [projectId]="projectId" [project]="project" [chartName]="chartName" [chartVersion]="chartVersion"

View File

@ -3,7 +3,12 @@
cursor: pointer;
color: #007cbb;
font-size: 16px;
margin: 5px;
}
.breadcrumb {
margin-bottom: 0.5rem;
}
.back-icon {
color: #007cbb;
font-size: 16px;
}

View File

@ -1,7 +1,8 @@
<div>
<div class="breadcrumb">
<a href="javascript:void(0)" (click)="gotoProjectList()"> {{ 'SIDE_NAV.PROJECTS'| translate}} </a>
&lt;
<span class="back-icon"><</span>
<a href="javascript:void(0)" (click)="gotoProjectList()">{{ 'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a href="javascript:void(0)" (click)="gotoChartList()">{{ projectName }}</a>
</div>
<hbr-helm-chart-version

View File

@ -3,7 +3,12 @@
cursor: pointer;
color: #007cbb;
font-size: 16px;
margin: 5px;
}
.breadcrumb {
margin-bottom: 0.5rem;
}
.back-icon {
color: #007cbb;
font-size: 16px;
}

View File

@ -1,13 +1,58 @@
<a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle"> {{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
<span class="back-icon"><</span>
<a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle">{{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']"> {{'SEARCH.BACK' | translate}}</a>
<h1 class="custom-h2" sub-header-title>
<clr-icon *ngIf="isProxyCacheProject" shape="cloud-traffic" size="30"></clr-icon>
<span class="ml-05">{{currentProject.name}}</span>
<span class="ml-05 role-label" *ngIf="isMember">{{roleName | translate}}</span>
</h1>
<div class="clr-row mt-0 line-height-10" *ngIf="isProxyCacheProject">
<span class="proxy-cache">{{ 'PROJECT.PROXY_CACHE' | translate }}</span>
<div class="clr-row">
<div class="clr-col-4">
<h1 class="custom-h2 center" sub-header-title>
<clr-icon *ngIf="isProxyCacheProject" shape="cloud-traffic" size="30"></clr-icon>
<clr-icon *ngIf="!isProxyCacheProject" shape="organization" size="30"></clr-icon>
<span class="ml-05">{{currentProject.name}}</span>
<div class="divider filter-divider"></div>
<span class="role-label" *ngIf="isMember">{{roleName | translate}}</span>
</h1>
<div class="clr-row mt-0 line-height-10" *ngIf="isProxyCacheProject">
<span class="proxy-cache">{{ 'PROJECT.PROXY_CACHE' | translate }}</span>
</div>
</div>
<div class="clr-col-8 flex-end">
<div class="card">
<div class="card-block container">
<div class="head">{{'PROJECT.ACCESS_LEVEL' | translate}}</div>
<div class="storage-used font-weight-700">
<div>
<h3 class="mt-0">{{ (currentProject?.metadata?.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</h3>
</div>
</div>
</div>
</div>
<div class="card" *ngIf="hasQuotaReadPermission">
<div class="card-block container">
<div class="head">{{'STATISTICS.STORAGE_USED' | translate }}</div>
<div class="storage-used font-weight-700">
<div *ngIf="projectQuota">
<h3 class="mt-0 clr-display-inline-block">
<span class="size-number">{{getSizeNumber()}}</span>
<span *ngIf="getSizeNumber()">{{getSizeUnit()}}</span>
</h3>
<span class="of">{{ 'QUOTA.OF' | translate }}</span>
<span>{{ projectQuota?.hard?.storage ===-1? ('QUOTA.UNLIMITED' | translate) : getIntegerAndUnit(projectQuota?.hard?.storage, projectQuota?.used?.storage).partNumberHard }}</span>
<span>{{ projectQuota?.hard?.storage ===-1? '': getIntegerAndUnit(projectQuota?.hard?.storage, projectQuota?.used?.storage).partCharacterHard }}</span>
<div>
<div class="progress-block progress-min-width progress-div">
<div class="progress success"
[class.danger]="projectQuota?.hard?.storage!==-1?projectQuota?.used?.storage/projectQuota?.hard?.storage>quotaDangerCoefficient:false"
[class.warning]="projectQuota?.hard?.storage!==-1?projectQuota?.used?.storage/projectQuota?.hard?.storage<=quotaDangerCoefficient&&projectQuota?.used?.storage/projectQuota?.hard?.storage>=quotaWarningCoefficient:false">
<progress
value="{{projectQuota?.hard?.storage===-1? 0 : projectQuota?.used?.storage}}"
max="{{projectQuota?.hard?.storage}}" data-displayval="100%"></progress>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<clr-tabs id="project-tabs" class="tabs" [class.in-overflow]="isTabLinkInOverFlow()">
<ng-container *ngFor="let tab of tabLinkNavList;let i=index">

View File

@ -12,10 +12,10 @@
}
.role-label {
color: #cccccc;
font-size: 14px;
font-style: italic;
letter-spacing: 0.01em;
margin-top: 8px;
}
.backStyle {
color: #007cbb;
@ -54,3 +54,83 @@ button {
.line-height-10 {
line-height: 10px;
}
.back-icon {
margin-right: 5px;
color: #007cbb;
font-size: 16px;
}
.flex-end {
display: flex;
justify-content: flex-end;
}
.card{
width: auto;
min-width: 10rem;
margin-right: 0.5rem;
margin-top: 0;
height: 6rem;
}
.head {
margin-top: 0;
margin-bottom: 0.25rem;
font-size: 18px;
}
.font-weight-700 {
font-weight: 700;
}
.storage-used {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 8rem;
}
.size-number {
font-size: 1.5rem;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.margin-right-5px {
margin-right: 5px;
}
.center {
display: flex;
align-items: center;
}
.divider {
height: 24px;
width: 1px;
margin-right: 24px;
margin-left: 24px;
}
.progress,
.progress-static {
progress {
max-height: 0.48rem;
}
}
:host::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
}
.of {
margin-right: 5px;
margin-left: 5px;
}

View File

@ -21,6 +21,10 @@ import { UserPermissionService, USERSTATICPERMISSION } from "../../../shared/ser
import { ErrorHandler } from "../../../shared/units/error-handler";
import { debounceTime } from 'rxjs/operators';
import { DOWN, SHOW_ELLIPSIS_WIDTH, UP } from './project-detail.const';
import { ProjectService } from "../../../../../ng-swagger-gen/services/project.service";
import { ProjectSummaryQuota } from "../../../../../ng-swagger-gen/models/project-summary-quota";
import { QUOTA_DANGER_COEFFICIENT, QUOTA_WARNING_COEFFICIENT, QuotaUnits } from "../../../shared/entities/shared.const";
import { clone, GetIntegerAndUnit, getSizeNumber, getSizeUnit } from "../../../shared/units/utils";
@Component({
@ -48,6 +52,7 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
hasWebhookListPermission: boolean;
hasScannerReadPermission: boolean;
hasP2pProviderReadPermission: boolean;
hasQuotaReadPermission: boolean = false;
tabLinkNavList = [
{
linkName: "summary",
@ -126,7 +131,11 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
private _subject = new Subject<string>();
private _subscription: Subscription;
isProxyCacheProject: boolean = false;
projectQuota: ProjectSummaryQuota;
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT;
constructor(
private projectService: ProjectService,
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService,
@ -202,14 +211,30 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.P2P_PROVIDER.KEY, USERSTATICPERMISSION.P2P_PROVIDER.VALUE.READ));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.QUOTA.KEY, USERSTATICPERMISSION.QUOTA.VALUE.READ));
forkJoin(...permissionsList).subscribe(Rules => {
[this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission, this.hasTagRetentionPermission, this.hasWebhookListPermission,
this.hasScannerReadPermission, this.hasP2pProviderReadPermission] = Rules;
this.hasScannerReadPermission, this.hasP2pProviderReadPermission, this.hasQuotaReadPermission] = Rules;
if (this.hasQuotaReadPermission) {
this.getQuotaInfo();
}
}, error => this.errorHandler.error(error));
}
getQuotaInfo() {
this.projectService.getProjectSummary({
projectNameOrId: this.projectId.toString()
}).subscribe(res => {
if (res && res.quota) {
this.projectQuota = res.quota;
}
}, error => {
this.errorHandler.error(error);
});
}
public get isSessionValid(): boolean {
return this.sessionService.getCurrentUser() != null;
@ -274,4 +299,20 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
// 3. Hide overflowed tabs
this.resetTabsForDownSize();
}
getIntegerAndUnit(hardValue, usedValue) {
return GetIntegerAndUnit(hardValue, clone(QuotaUnits), usedValue, clone(QuotaUnits));
}
getSizeNumber(): number | string {
if (this.projectQuota && this.projectQuota.used && this.projectQuota.used.storage) {
return getSizeNumber(this.projectQuota.used.storage);
}
return 0;
}
getSizeUnit(): number | string {
if (this.projectQuota) {
return getSizeUnit(this.projectQuota.used.storage);
}
return null;
}
}

View File

@ -83,7 +83,7 @@
<clr-dg-footer>
<div class="report">
<i>{{'VULNERABILITY.REPORTED_BY' | translate: {scanner: getScannerInfo(scanner)} }}</i>
<i *ngIf="scanner">{{'VULNERABILITY.REPORTED_BY' | translate: {scanner: getScannerInfo(scanner)} }}</i>
</div>
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults?.length">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>

View File

@ -1,5 +1,6 @@
<div>
<div class="breadcrumb" *ngIf="!withAdmiral">
<span class="back-icon"><</span>
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="watchGoBackEvt(projectId)">{{projectName}}</a>

View File

@ -8,5 +8,6 @@
margin-right: 5px;
}
.back-icon {
color: gray;
color: #007cbb;
font-size: 16px;
}

View File

@ -1,4 +1,5 @@
<div class="arrow-block" *ngIf="!withAdmiral">
<span class="back-icon margin-right-5px"><</span>
<a class="pl-0" (click)="goBackPro()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="goBackRep()">{{projectName}}</a>

View File

@ -7,7 +7,8 @@
margin-right: 5px;
}
.back-icon {
color: gray;
color: #007cbb;
font-size: 16px;
}
.margin-top-5px {
@ -28,3 +29,6 @@
.margin-left-10px {
margin-left: 10px;
}
.margin-right-5px {
margin-right: 5px;
}

View File

@ -36,7 +36,7 @@ import {
CARD_VIEW_LOCALSTORAGE_KEY,
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets
ConfirmationTargets, FALSE_STR, TRUE_STR
} from "../../../shared/entities/shared.const";
import { operateChanges, OperateInfo, OperationState } from "../../../shared/components/operation/operate";
import {
@ -54,8 +54,6 @@ import { errorHandler } from "../../../shared/units/shared.utils";
import { ConfirmationAcknowledgement } from "../../global-confirmation-dialog/confirmation-state-message";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
const TRUE_STR: string = 'true';
const FALSE_STR: string = 'false';
@Component({
selector: "hbr-repository-gridview",
templateUrl: "./repository-gridview.component.html",

View File

@ -1,64 +1,138 @@
<div class="summary summary-dark display-flex" *ngIf="summaryInformation">
<div class="summary-left">
<div class="display-flex project-detail pt-05" *ngIf="summaryInformation?.registry">
<h5 class="mt-0 width-7-5">{{'PROJECT.PROXY_CACHE_ENDPOINT' | translate}}</h5>
<ul class="list-unstyled">
<li id="endpoint">{{summaryInformation?.registry?.name}}-{{summaryInformation?.registry?.url}}</li>
</ul>
</div>
<div class="display-flex project-detail pt-05">
<h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_REPOSITORY' | translate}}</h5>
<ul class="list-unstyled">
<li>{{summaryInformation?.repo_count}}</li>
</ul>
</div>
<div class="display-flex project-detail pt-05" *ngIf="withHelmChart">
<h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_HELM_CHART' | translate}}</h5>
<ul class="list-unstyled">
<li>{{summaryInformation?.chart_count}}</li>
</ul>
</div>
<div *ngIf="showProjectMemberInfo" class="display-flex project-detail pt-05">
<h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h5>
<ul class="list-unstyled">
<li>{{ summaryInformation?.project_admin_count?summaryInformation?.project_admin_count:0 }} {{'SUMMARY.ADMIN' | translate}}</li>
<li>{{ summaryInformation?.maintainer_count?summaryInformation?.maintainer_count:0 }} {{'SUMMARY.MAINTAINER' | translate}}</li>
<li>{{ summaryInformation?.developer_count?summaryInformation?.developer_count:0 }} {{'SUMMARY.DEVELOPER' | translate}}</li>
<li>{{ summaryInformation?.guest_count?summaryInformation?.guest_count:0 }} {{'SUMMARY.GUEST' | translate}}</li>
<li>{{ summaryInformation?.limited_guest_count?summaryInformation?.limited_guest_count:0 }} {{'SUMMARY.LIMITED_GUEST' | translate}}</li>
</ul>
</div>
</div>
<div *ngIf="showQuotaInfo && summaryInformation?.quota" class="summary-right pt-05">
<div class="display-flex project-detail">
<h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5>
<div class="ml-1">
<div class="display-flex quotas-progress">
<label class="mr-1">{{'SUMMARY.STORAGE_CONSUMPTION' | translate}}</label>
<label class="progress-label">
{{ summaryInformation?.quota?.hard?.storage !== -1 ?(getIntegerAndUnit(summaryInformation?.quota?.hard?.storage, summaryInformation?.quota?.used?.storage).partNumberUsed
+ getIntegerAndUnit(summaryInformation?.quota?.hard?.storage, summaryInformation?.quota?.used?.storage).partCharacterUsed) : getSuitableUnit(summaryInformation?.quota?.used?.storage)}}
<!-- {{ getSuitableUnit(summaryInformation?.quota?.used?.storage) }} -->
{{ 'QUOTA.OF' | translate }}
{{ summaryInformation?.quota?.hard?.storage ===-1? ('QUOTA.UNLIMITED' | translate) : getIntegerAndUnit(summaryInformation?.quota?.hard?.storage, summaryInformation?.quota?.used?.storage).partNumberHard }}
{{ summaryInformation?.quota?.hard?.storage ===-1? '': getIntegerAndUnit(summaryInformation?.quota?.hard?.storage, summaryInformation?.quota?.used?.storage).partCharacterHard }}
</label>
</div>
<div>
<div class="progress-block progress-min-width progress-div">
<div class="progress success"
[class.danger]="summaryInformation?.quota?.hard?.storage!==-1?summaryInformation?.quota?.used?.storage/summaryInformation?.quota?.hard?.storage>quotaDangerCoefficient:false"
[class.warning]="summaryInformation?.quota?.hard?.storage!==-1?summaryInformation?.quota?.used?.storage/summaryInformation?.quota?.hard?.storage<=quotaDangerCoefficient&&summaryInformation?.quota?.used?.storage/summaryInformation?.quota?.hard?.storage>=quotaWarningCoefficient:false">
<progress
value="{{summaryInformation?.quota?.hard?.storage===-1? 0 : summaryInformation?.quota?.used?.storage}}"
max="{{summaryInformation?.quota?.hard?.storage}}" data-displayval="100%"></progress>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="clr-row card-row mt-1">
<span class="card-btn mr-5px" (click)="showCard(true)" (mouseenter)="mouseEnter('card') "
(mouseleave)="mouseLeave('card')">
<clr-icon size="24" [ngClass]="{'is-highlight': isCardView || isHovering('card') }"
shape="view-cards"></clr-icon>
</span>
<span class="list-btn" (click)="showCard(false)" (mouseenter)="mouseEnter('list') "
(mouseleave)="mouseLeave('list')">
<clr-icon size="24" [ngClass]="{'is-highlight': !isCardView || isHovering('list') }"
shape="view-list"></clr-icon>
</span>
</div>
<div *ngIf="!summaryInformation" class="clr-row mt-2 center">
<span class="spinner spinner-md"></span>
</div>
<ng-container *ngIf="!isCardView">
<div *ngIf="summaryInformation">
<div class="pt-05 flex" *ngIf="summaryInformation?.registry">
<h4 class="mt-0 title-width">{{'PROJECT.PROXY_CACHE_ENDPOINT' | translate}}</h4>
<ul class="list-unstyled">
<li id="endpoint">{{summaryInformation?.registry?.name}}-{{summaryInformation?.registry?.url}}</li>
</ul>
</div>
<div class="pt-05 flex">
<h4 class="mt-0 title-width">{{'SUMMARY.PROJECT_REPOSITORY' | translate}}</h4>
<ul class="list-unstyled">
<li>{{summaryInformation?.repo_count ? summaryInformation?.repo_count : 0}}</li>
</ul>
</div>
<div class="pt-05 flex" *ngIf="withHelmChart">
<h4 class="mt-0 title-width">{{'SUMMARY.PROJECT_HELM_CHART' | translate}}</h4>
<ul class="list-unstyled">
<li>{{summaryInformation?.chart_count ? summaryInformation?.chart_count : 0}}</li>
</ul>
</div>
<div *ngIf="showProjectMemberInfo" class="pt-05 flex">
<h4 class="mt-0 title-width">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h4>
<ul class="list-unstyled">
<li>{{ summaryInformation?.project_admin_count ? summaryInformation?.project_admin_count : 0 }} {{'SUMMARY.ADMIN' | translate}}</li>
<li>{{ summaryInformation?.maintainer_count ? summaryInformation?.maintainer_count : 0 }} {{'SUMMARY.MAINTAINER' | translate}}</li>
<li>{{ summaryInformation?.developer_count ? summaryInformation?.developer_count : 0 }} {{'SUMMARY.DEVELOPER' | translate}}</li>
<li>{{ summaryInformation?.guest_count ? summaryInformation?.guest_count : 0 }} {{'SUMMARY.GUEST' | translate}}</li>
<li>{{ summaryInformation?.limited_guest_count ? summaryInformation?.limited_guest_count : 0 }} {{'SUMMARY.LIMITED_GUEST' | translate}}</li>
</ul>
</div>
</div>
</ng-container>
<ng-container *ngIf="isCardView && summaryInformation">
<div class="container">
<div class="card clickable" *ngIf="hasReadRepoPermission">
<div class="card-header">
<div>{{"PROJECT_DETAIL.REPOSITORIES" | translate}}</div>
<div class="clr-row number">{{summaryInformation?.repo_count ? summaryInformation?.repo_count : 0}}</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'REPOSITORY.NAME' | translate}}</div>
<div class="clr-col-2 column">{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}</div>
<div class="clr-col-2 column">{{'REPOSITORY.PULL_COUNT' | translate}}</div>
<div class="clr-col-4 column">{{'REPOSITORY.LAST_MODIFIED' | translate}}</div>
</div>
</div>
<div class="card-block">
<div class="clr-row" *ngFor="let item of repos">
<div class="clr-col-4 ellipsis">
<a href="javascript:void(0)" (click)="goIntoRepo(item)">{{item.name}}</a>
</div>
<div class="clr-col-2">{{item.artifact_count?item.artifact_count:0}}</div>
<div class="clr-col-2">{{item.pull_count?item.pull_count:0}}</div>
<div class="clr-col-4 ellipsis">{{item.update_time | harborDatetime: 'short'}}</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-link" (click)="goToRepos()">{{'SUMMARY.SEE_ALL' | translate}}</button>
</div>
</div>
<div class="card clickable" *ngIf="hasReadChartPermission">
<div class="card-header">
<div>{{"PROJECT_DETAIL.HELMCHART" | translate}}</div>
<div class="clr-row number">{{summaryInformation?.chart_count ? summaryInformation?.chart_count : 0}}</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'HELM_CHART.NAME' | translate}}</div>
<div class="clr-col-2 column">{{'HELM_CHART.STATUS' | translate}}</div>
<div class="clr-col-2 column">{{'HELM_CHART.CHARTVERSIONS' | translate}}</div>
<div class="clr-col-4 column">{{'HELM_CHART.CREATED' | translate}}</div>
</div>
</div>
<div class="card-block">
<div class="clr-row" *ngFor="let chart of charts">
<div class="clr-col-4 ellipsis chart-name">
<img class="size-24 mr-5px" [src]="chart.icon ?chart.icon:chartDefaultIcon" (error)="getDefaultIcon(chart);" />
<a href="javascript:void(0)" (click)="onChartClick(chart.name)">{{ chart.name }}</a>
</div>
<div class="clr-col-2">{{ getStatusString(chart) | translate }}</div>
<div class="clr-col-2">{{ chart.total_versions }}</div>
<div class="clr-col-4 ellipsis">{{ chart.created | harborDatetime: 'short' }}</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-link" (click)="goToCharts()">{{'SUMMARY.SEE_ALL' | translate}}</button>
</div>
</div>
<div class="card clickable" *ngIf="showProjectMemberInfo">
<div class="card-header no-underline">
<div>{{"PROJECT_DETAIL.USERS" | translate}}</div>
<div class="clr-row number">{{getTotalMembers()}}</div>
</div>
<div class="card-block">
<div class="clr-row">
<div class="clr-col-4 column">{{'SUMMARY.ADMIN' | translate}}</div>
<div class="clr-col-8">{{ summaryInformation?.project_admin_count ? summaryInformation?.project_admin_count : 0 }}</div>
</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'SUMMARY.MAINTAINER' | translate}}</div>
<div class="clr-col-8">{{ summaryInformation?.maintainer_count ? summaryInformation?.maintainer_count : 0 }}</div>
</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'SUMMARY.DEVELOPER' | translate}}</div>
<div class="clr-col-8">{{ summaryInformation?.developer_count ? summaryInformation?.developer_count : 0 }}</div>
</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'SUMMARY.GUEST' | translate}}</div>
<div class="clr-col-8">{{ summaryInformation?.guest_count ? summaryInformation?.guest_count : 0 }}</div>
</div>
<div class="clr-row">
<div class="clr-col-4 column">{{'SUMMARY.LIMITED_GUEST' | translate}}</div>
<div class="clr-col-8">{{ summaryInformation?.limited_guest_count ? summaryInformation?.limited_guest_count : 0 }}</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-link" (click)="goToMembers()">{{'SUMMARY.SEE_ALL' | translate}}</button>
</div>
</div>
</div>
</ng-container>

View File

@ -1,63 +1,67 @@
.summary {
color: #000;
padding-right: 0.3rem;
font-size: 13px;
justify-content: space-between;
.summary-left {
padding-top: .25rem;
.project-detail {
width: 17rem;
min-height: 1.45rem;
ul {
width: 8rem;
}
}
}
h5 {
font-size: 13px;
font-weight: 700;
color: #000;
}
.summary-right {
.quotas-progress {
min-width: 10rem;
justify-content: space-between;
;
}
}
}
.width-7-5 {
width: 7.5rem;
}
.display-flex {
display: flex;
}
.progress,
.progress-static {
progress {
max-height: 0.48rem;
}
}
::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
.title-width {
width: 12rem;
}
.pt-05 {
padding-top: 0.5rem;
padding-top: 1rem;
}
.flex {
display: flex;
}
.card-row {
justify-content: flex-end;
padding-right: 2rem;
}
.mr-5px {
margin-right: 5px;
}
.container {
display: flex;
flex-wrap: wrap;
}
.card {
width: 24rem;
margin-right: 1rem;
flex-grow:0;
flex-shrink:0;
}
.number {
align-items: center;
justify-content: center;
font-size: 3rem;
color: #266aac;
height: 4rem;
}
.column {
font-size: 10px;
font-weight: bolder;
text-transform: uppercase;
}
.card-header {
padding-bottom: 0;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-block {
min-height: 6rem;
}
.size-24 {
width: 20px;
height: 20px;
}
.chart-name {
display: flex;
align-items: center;
}
.no-underline {
border-bottom-style: none;
}
.center {
justify-content: center;
align-items: center;
}

View File

@ -80,13 +80,21 @@ describe('SummaryComponent', () => {
{
provide: ActivatedRoute, useValue: {
paramMap: of({ get: (key) => 'value' }),
snapshot: {
parent: {
parent: {
snapshot: {
data: {
projectResolver: {registry_id: 3}
},
}
}
},
},
parent: {
parent: {
snapshot: {
params: { id: 1 },
data: {
projectResolver: {registry_id: 3}
},
}
}},
}
@ -107,10 +115,30 @@ describe('SummaryComponent', () => {
it('should show proxy cache endpoint', async () => {
component.summaryInformation = mockedSummaryInformation;
component.isCardView = false;
fixture.detectChanges();
await fixture.whenStable();
const endpoint: HTMLElement = fixture.nativeElement.querySelector("#endpoint");
expect(endpoint).toBeTruthy();
expect(endpoint.innerText).toEqual("test-https://test.com");
});
it('should show card view', async () => {
component.summaryInformation = mockedSummaryInformation;
component.isCardView = true;
fixture.detectChanges();
await fixture.whenStable();
const container: HTMLElement = fixture.nativeElement.querySelector(".container");
expect(container).toBeTruthy();
});
it('should show three cards', async () => {
component.summaryInformation = mockedSummaryInformation;
component.isCardView = true;
component.hasReadChartPermission = true;
fixture.detectChanges();
await fixture.whenStable();
const cards = fixture.nativeElement.querySelectorAll(".card");
expect(cards.length).toEqual(3);
});
});

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { AppConfigService } from "../../../services/app-config.service";
import { QUOTA_DANGER_COEFFICIENT, QUOTA_WARNING_COEFFICIENT, QuotaUnits } from "../../../shared/entities/shared.const";
import {
Endpoint,
ProjectService,
@ -9,7 +8,18 @@ import {
USERSTATICPERMISSION
} from '../../../shared/services';
import { ErrorHandler } from "../../../shared/units/error-handler";
import { clone, GetIntegerAndUnit, getSuitableUnit as getSuitableUnitFn } from "../../../shared/units/utils";
import {
DefaultHelmIcon,
FALSE_STR,
PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY,
TRUE_STR
} from "../../../shared/entities/shared.const";
import { RepositoryService } from "../../../../../ng-swagger-gen/services/repository.service";
import { Project } from "../../../../../ng-swagger-gen/models/project";
import { Repository } from "../../../../../ng-swagger-gen/models/repository";
import { HelmChartItem } from "../helm-chart/helm-chart-detail/helm-chart.interface.service";
import { HelmChartService } from "../helm-chart/helm-chart-detail/helm-chart.service";
@Component({
selector: 'summary',
@ -17,50 +27,169 @@ import { clone, GetIntegerAndUnit, getSuitableUnit as getSuitableUnitFn } from "
styleUrls: ['./summary.component.scss']
})
export class SummaryComponent implements OnInit {
showProjectMemberInfo: boolean;
showQuotaInfo: boolean;
showProjectMemberInfo: boolean = false;
hasReadRepoPermission: boolean = false;
hasReadChartPermission: boolean = false;
projectId: number;
projectName: string;
summaryInformation: any;
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT;
endpoint: Endpoint;
isCardView: boolean = true;
cardHover: boolean = false;
listHover: boolean = false;
repos: Repository[] = [];
charts: HelmChartItem[] = [];
chartDefaultIcon: string = DefaultHelmIcon;
constructor(
private projectService: ProjectService,
private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler,
private appConfigService: AppConfigService,
private route: ActivatedRoute,
) { }
private repoService: RepositoryService,
private router: Router,
private helmChartService: HelmChartService,
) {
if (localStorage) {
if (!localStorage.getItem(PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY)) {
localStorage.setItem(PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY, FALSE_STR);
}
this.isCardView = localStorage.getItem(PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY) === TRUE_STR;
}
}
ngOnInit() {
this.projectId = this.route.parent.parent.snapshot.params['id'];
const resolverData = this.route.snapshot.parent.parent.data;
if (resolverData) {
let project = <Project>resolverData["projectResolver"];
this.projectName = project.name;
}
const permissions = [
{resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST},
{resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.LIST},
{resource: USERSTATICPERMISSION.HELM_CHART.KEY, action: USERSTATICPERMISSION.HELM_CHART.VALUE.LIST}
];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
this.showProjectMemberInfo = results[0];
this.showQuotaInfo = results[1];
this.hasReadRepoPermission = results[1];
this.hasReadChartPermission = results[2];
});
this.projectService.getProjectSummary(this.projectId).subscribe(res => {
this.summaryInformation = res;
}, error => {
this.errorHandler.error(error);
});
if (this.isCardView) {
this.getDataForCardView();
}
}
getSuitableUnit(value) {
const QuotaUnitsCopy = clone(QuotaUnits);
return getSuitableUnitFn(value, QuotaUnitsCopy);
}
getIntegerAndUnit(hardValue, usedValue) {
return GetIntegerAndUnit(hardValue, clone(QuotaUnits), usedValue, clone(QuotaUnits));
}
public get withHelmChart(): boolean {
return this.appConfigService.getConfig().with_chartmuseum;
}
showCard(cardView: boolean) {
if (this.isCardView === cardView) {
return;
}
this.isCardView = cardView;
if (localStorage) {
if (this.isCardView) {
localStorage.setItem(PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY, TRUE_STR);
} else {
localStorage.setItem(PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY, FALSE_STR);
}
}
if (this.isCardView) {
this.getDataForCardView();
}
}
mouseEnter(itemName: string) {
if (itemName === "card") {
this.cardHover = true;
} else {
this.listHover = true;
}
}
mouseLeave(itemName: string) {
if (itemName === "card") {
this.cardHover = false;
} else {
this.listHover = false;
}
}
isHovering(itemName: string) {
if (itemName === "card") {
return this.cardHover;
} else {
return this.listHover;
}
}
getDataForCardView() {
this.getTop4Repos();
this.getTop4Charts();
}
getTop4Repos() {
if (this.hasReadRepoPermission) {
this.repoService.listRepositories({
projectName: this.projectName,
page: 1,
pageSize: 4
}).subscribe(res => {
this.repos = res;
});
}
}
getTop4Charts() {
if (this.hasReadChartPermission) {
this.helmChartService.getHelmCharts(this.projectName).subscribe(res => {
if (res && res.length) {
this.charts = res.slice(0, 4);
}
});
}
}
goIntoRepo(repoEvt: Repository): void {
const linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name.substr(this.projectName.length + 1)];
this.router.navigate(linkUrl);
}
goToRepos() {
const linkUrl = ['harbor', 'projects', this.projectId, 'repositories'];
this.router.navigate(linkUrl);
}
getDefaultIcon(chart: HelmChartItem) {
chart.icon = this.chartDefaultIcon;
}
getStatusString(chart: HelmChartItem) {
if (chart.deprecated) {
return "HELM_CHART.DEPRECATED";
} else {
return "HELM_CHART.ACTIVE";
}
}
onChartClick(chartName: string) {
const linkUrl = ['harbor', 'projects', this.projectId, 'helm-charts', chartName, 'versions'];
this.router.navigate(linkUrl);
}
goToCharts() {
const linkUrl = ['harbor', 'projects', this.projectId, 'helm-charts'];
this.router.navigate(linkUrl);
}
goToMembers() {
const linkUrl = ['harbor', 'projects', this.projectId, 'members'];
this.router.navigate(linkUrl);
}
getTotalMembers(): number {
if (this.summaryInformation) {
return +(this.summaryInformation.project_admin_count ? this.summaryInformation.project_admin_count : 0) +
+(this.summaryInformation.maintainer_count ? this.summaryInformation.maintainer_count : 0) +
+(this.summaryInformation.developer_count ? this.summaryInformation.developer_count : 0) +
+(this.summaryInformation.guest_count ? this.summaryInformation.guest_count : 0) +
+(this.summaryInformation.limited_guest_count ? this.summaryInformation.limited_guest_count : 0);
}
return 0;
}
}

View File

@ -241,8 +241,13 @@ export enum ResourceType {
REPOSITORY_TAG = 3,
}
export const TRUE_STR: string = 'true';
export const FALSE_STR: string = 'false';
export const CARD_VIEW_LOCALSTORAGE_KEY = 'card-view';
export const PROJECT_SUMMARY_CARD_VIEW_LOCALSTORAGE_KEY = 'project_card-view';
export enum ScheduleType {
NONE = "None",
DAILY = "Daily",

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { Observable, forkJoin} from "rxjs";
import { map, share } from "rxjs/operators";
import { HttpClient } from '@angular/common/http';
import { CacheObservable } from "../units/cache-util";
import { CacheObservable, FlushAll } from "../units/cache-util";
import { CURRENT_BASE_HREF } from "../units/utils";
@ -90,5 +90,7 @@ export class UserPermissionDefaultService extends UserPermissionService {
}
public clearPermissionCache() {
this._sharedPermissionObservableMap = {};
FlushAll();
}
}

View File

@ -810,7 +810,8 @@
"MAINTAINER": "Maintainer(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Einige Änderungen wurden noch nicht gespeichert. Sollen diese verworfen werden?"

View File

@ -810,7 +810,8 @@
"MAINTAINER": "Maintainer(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"

View File

@ -811,7 +811,8 @@
"MAINTAINER": "Maintainer(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"

View File

@ -796,7 +796,8 @@
"MAINTAINER": "Maintainer(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"

View File

@ -807,7 +807,8 @@
"MAINTAINER": "Maintainer(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?"

View File

@ -810,7 +810,8 @@
"MAINTAINER": "Uzman(lar)",
"DEVELOPER": "Geliştirici(ler)",
"GUEST": "Misafir(ler)",
"LIMITED_GUEST": "Limited guest(s)"
"LIMITED_GUEST": "Limited guest(s)",
"SEE_ALL": "SEE ALL"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Bazı değişiklikler henüz kaydedilmedi. İptal etmek istiyor musun?"

View File

@ -812,7 +812,8 @@
"MAINTAINER": "维护人员",
"DEVELOPER": "开发者",
"GUEST": "访客",
"LIMITED_GUEST": "受限访客"
"LIMITED_GUEST": "受限访客",
"SEE_ALL": "查看全部"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"

View File

@ -808,7 +808,8 @@
"MAINTAINER": "維護人員",
"DEVELOPER": "開發者",
"GUEST": "訪客",
"LIMITED_GUEST": "受限訪客"
"LIMITED_GUEST": "受限訪客",
"SEE_ALL": "SEE ALL"
},
"ALERT":{
"FORM_CHANGE_CONFIRMATION": "表單內容改變,確認是否取消?"