mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-03 13:31:22 +01:00
Support backend server pagination in repo list (#2778)
* refine the test case of scheduler * Fix bug * Support root cert downloaded * Fix code conflicts * support clair db timestamps * Support backend pagination in repository list
This commit is contained in:
parent
920c41c204
commit
a1a36a4cc4
@ -50,7 +50,7 @@ export const VULNERABILITY_CONFIG_STYLES: string = `
|
||||
}
|
||||
|
||||
.btn-font {
|
||||
font-size: 14px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
|
@ -7,7 +7,7 @@ import { Router } from '@angular/router';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { ListRepositoryComponent } from './list-repository.component';
|
||||
import { Repository } from '../service/interface';
|
||||
import { Repository, RepositoryItem } from '../service/interface';
|
||||
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
|
||||
@ -19,7 +19,7 @@ describe('ListRepositoryComponent (inline template)', () => {
|
||||
let comp: ListRepositoryComponent;
|
||||
let fixture: ComponentFixture<ListRepositoryComponent>;
|
||||
|
||||
let mockData: Repository[] = [
|
||||
let mockData: RepositoryItem[] = [
|
||||
{
|
||||
"id": 11,
|
||||
"name": "library/busybox",
|
||||
|
@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetect
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
import { Repository } from '../service/interface';
|
||||
import { RepositoryItem } from '../service/interface';
|
||||
|
||||
import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html';
|
||||
|
||||
@ -17,7 +17,7 @@ export class ListRepositoryComponent {
|
||||
|
||||
@Input() urlPrefix: string;
|
||||
@Input() projectId: number;
|
||||
@Input() repositories: Repository[];
|
||||
@Input() repositories: RepositoryItem[];
|
||||
|
||||
@Output() delete = new EventEmitter<string>();
|
||||
@Output() paginate = new EventEmitter<State>();
|
||||
@ -26,9 +26,9 @@ export class ListRepositoryComponent {
|
||||
|
||||
pageOffset: number = 1;
|
||||
|
||||
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
|
||||
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
|
||||
|
||||
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
|
||||
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
|
@ -23,7 +23,12 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template';
|
||||
import { DEFAULT_PAGE_SIZE } from '../utils';
|
||||
import {
|
||||
DEFAULT_PAGE_SIZE,
|
||||
calculatePage,
|
||||
doFiltering,
|
||||
doSorting
|
||||
} from '../utils';
|
||||
|
||||
import { Comparator, State } from 'clarity-angular';
|
||||
|
||||
@ -93,7 +98,7 @@ export class RecentLogComponent implements OnInit {
|
||||
//Keep it for future filter
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = this._calculatePage(state);
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber !== this.currentPagePvt) {
|
||||
//load data
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
@ -110,10 +115,10 @@ export class RecentLogComponent implements OnInit {
|
||||
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//To display
|
||||
|
||||
//Do customized filter
|
||||
this._doFilter(state);
|
||||
this.recentLogs = doFiltering<AccessLogItem>(this.recentLogs, state);
|
||||
|
||||
//Do customized sorting
|
||||
this._doSorting(state);
|
||||
this.recentLogs = doSorting<AccessLogItem>(this.recentLogs, state);
|
||||
|
||||
this.currentPagePvt = pageNumber;
|
||||
|
||||
@ -129,10 +134,10 @@ export class RecentLogComponent implements OnInit {
|
||||
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data
|
||||
|
||||
//Do customized filter
|
||||
this._doFilter(state);
|
||||
this.recentLogs = doFiltering<AccessLogItem>(this.recentLogs, state);
|
||||
|
||||
//Do customized sorting
|
||||
this._doSorting(state);
|
||||
this.recentLogs = doSorting<AccessLogItem>(this.recentLogs, state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,68 +148,4 @@ export class RecentLogComponent implements OnInit {
|
||||
reg.test(log.operation) ||
|
||||
reg.test(log.repo_tag);
|
||||
}
|
||||
|
||||
_calculatePage(state: State): number {
|
||||
if (!state || !state.page) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.ceil((state.page.to + 1) / state.page.size);
|
||||
}
|
||||
|
||||
_doFilter(state: State): void {
|
||||
if (!this.recentLogs || this.recentLogs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state || !state.filters || state.filters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.filters.forEach((filter: {
|
||||
property: string;
|
||||
value: string;
|
||||
}) => {
|
||||
this.recentLogs = this.recentLogs.filter(logItem => this._regexpFilter(filter["value"], logItem[filter["property"]]));
|
||||
});
|
||||
}
|
||||
|
||||
_regexpFilter(terms: string, testedValue: any): boolean {
|
||||
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||
return reg.test(testedValue);
|
||||
}
|
||||
|
||||
_doSorting(state: State): void {
|
||||
if (!this.recentLogs || this.recentLogs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state || !state.sort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.recentLogs = this.recentLogs.sort((a: AccessLogItem, b: AccessLogItem) => {
|
||||
let comp: number = 0;
|
||||
if (typeof state.sort.by !== "string") {
|
||||
comp = state.sort.by.compare(a, b);
|
||||
} else {
|
||||
let propA = a[state.sort.by.toString()], propB = b[state.sort.by.toString()];
|
||||
if (typeof propA === "string") {
|
||||
comp = propA.localeCompare(propB);
|
||||
} else {
|
||||
if (propA > propB) {
|
||||
comp = 1;
|
||||
} else if (propA < propB) {
|
||||
comp = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sort.reverse) {
|
||||
comp = -comp;
|
||||
}
|
||||
|
||||
return comp;
|
||||
});
|
||||
}
|
||||
}
|
@ -31,4 +31,10 @@ export const REPOSITORY_STACKVIEW_STYLES: string = `
|
||||
:host >>> .datagrid .datagrid-placeholder-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.db-status-warning {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
|
@ -5,29 +5,34 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)" [currentValue]="lastFilteredRepoName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading">
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let r of repositories">
|
||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate}}</clr-dg-placeholder>
|
||||
<clr-dg-row *ngFor="let r of repositories">
|
||||
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
|
||||
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true"></hbr-tag>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="showDBStatusWarning" class="db-status-warning">
|
||||
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
||||
{{'CONFIG.SCANNING.DB_NOT_READY' | translate }}
|
||||
</span>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
|
||||
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import { TagComponent } from '../tag/tag.component';
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Repository, Tag, SystemInfo } from '../service/interface';
|
||||
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
@ -44,7 +44,7 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||
};
|
||||
|
||||
let mockRepoData: Repository[] = [
|
||||
let mockRepoData: RepositoryItem[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "library/busybox",
|
||||
@ -65,6 +65,11 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
}
|
||||
];
|
||||
|
||||
let mockRepo: Repository = {
|
||||
metadata: {xTotalCount: 2},
|
||||
data: mockRepoData
|
||||
};
|
||||
|
||||
let mockTagData: Tag[] = [
|
||||
{
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
@ -117,7 +122,7 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
|
||||
systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService);
|
||||
|
||||
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepoData));
|
||||
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
fixtureRepo.detectChanges();
|
||||
});
|
||||
|
@ -18,7 +18,9 @@ import {
|
||||
Repository,
|
||||
SystemInfo,
|
||||
SystemInfoService,
|
||||
RepositoryService
|
||||
RepositoryService,
|
||||
RequestQueryParams,
|
||||
RepositoryItem
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
|
||||
@ -32,6 +34,14 @@ import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Tag, TagClickEvent } from '../service/interface';
|
||||
|
||||
import { State } from "clarity-angular";
|
||||
import {
|
||||
DEFAULT_PAGE_SIZE,
|
||||
calculatePage,
|
||||
doFiltering,
|
||||
doSorting
|
||||
} from '../utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-repository-stackview',
|
||||
template: REPOSITORY_STACKVIEW_TEMPLATE,
|
||||
@ -48,9 +58,11 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||
|
||||
lastFilteredRepoName: string;
|
||||
repositories: Repository[];
|
||||
repositories: RepositoryItem[];
|
||||
systemInfo: SystemInfo;
|
||||
|
||||
loading: boolean = true;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
@ -58,6 +70,11 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
|
||||
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
|
||||
|
||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||
currentPage: number = 1;
|
||||
totalCount: number = 0;
|
||||
currentState: State;
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private translateService: TranslateService,
|
||||
@ -77,6 +94,16 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||
}
|
||||
|
||||
public get isClairDBReady(): boolean {
|
||||
return this.systemInfo &&
|
||||
this.systemInfo.clair_vulnerability_status &&
|
||||
this.systemInfo.clair_vulnerability_status.overall_last_update > 0;
|
||||
}
|
||||
|
||||
public get showDBStatusWarning(): boolean {
|
||||
return this.withClair && !this.isClairDBReady;
|
||||
}
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.REPOSITORY &&
|
||||
@ -87,6 +114,12 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
.then(
|
||||
response => {
|
||||
this.refresh();
|
||||
let st: State = this.getStateAfterDeletion();
|
||||
if (!st) {
|
||||
this.refresh();
|
||||
} else {
|
||||
this.clrLoad(st);
|
||||
}
|
||||
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
|
||||
.subscribe(res => this.errorHandler.info(res));
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
@ -104,22 +137,20 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
|
||||
this.lastFilteredRepoName = '';
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
retrieve() {
|
||||
toPromise<Repository[]>(this.repositoryService
|
||||
.getRepositories(this.projectId, this.lastFilteredRepoName))
|
||||
.then(
|
||||
repos => this.repositories = repos,
|
||||
error => this.errorHandler.error(error));
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
doSearchRepoNames(repoName: string) {
|
||||
this.lastFilteredRepoName = repoName;
|
||||
this.retrieve();
|
||||
this.currentPage = 1;
|
||||
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
deleteRepo(repoName: string) {
|
||||
@ -134,10 +165,70 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.retrieve();
|
||||
this.doSearchRepoNames("");
|
||||
}
|
||||
|
||||
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
||||
this.tagClickEvent.emit(tagClickEvt);
|
||||
}
|
||||
|
||||
clrLoad(state: State): void {
|
||||
//Keep it for future filtering and sorting
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber <= 0) { pageNumber = 1; }
|
||||
|
||||
//Pagination
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
params.set("page", '' + pageNumber);
|
||||
params.set("page_size", '' + this.pageSize);
|
||||
|
||||
this.loading = true;
|
||||
|
||||
toPromise<Repository>(this.repositoryService.getRepositories(
|
||||
this.projectId,
|
||||
this.lastFilteredRepoName,
|
||||
params))
|
||||
.then((repo: Repository) => {
|
||||
this.totalCount = repo.metadata.xTotalCount;
|
||||
this.repositories = repo.data;
|
||||
|
||||
//Do filtering and sorting
|
||||
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
||||
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
|
||||
//Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 5000);
|
||||
}
|
||||
|
||||
getStateAfterDeletion(): State {
|
||||
let total: number = this.totalCount - 1;
|
||||
if (total <= 0) { return null; }
|
||||
|
||||
let totalPages: number = Math.floor(total / this.pageSize);
|
||||
let targetPageNumber: number = this.currentPage;
|
||||
|
||||
if (this.currentPage > totalPages) {
|
||||
targetPageNumber = totalPages;//Should == currentPage -1
|
||||
}
|
||||
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = (targetPageNumber - 1) * this.pageSize;
|
||||
st.page.to = targetPageNumber * this.pageSize - 1;
|
||||
|
||||
return st;
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import { ListRepositoryComponent } from '../list-repository/list-repository.comp
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Repository } from '../service/interface';
|
||||
import { Repository, RepositoryItem } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||
@ -19,14 +19,14 @@ class RouterStub {
|
||||
navigateByUrl(url: string) { return url; }
|
||||
}
|
||||
|
||||
describe('RepositoryComponent (inline template)', ()=> {
|
||||
describe('RepositoryComponent (inline template)', () => {
|
||||
|
||||
let comp: RepositoryComponent;
|
||||
let fixture: ComponentFixture<RepositoryComponent>;
|
||||
let repositoryService: RepositoryService;
|
||||
let spy: jasmine.Spy;
|
||||
|
||||
let mockData: Repository[] = [
|
||||
let mockData: RepositoryItem[] = [
|
||||
{
|
||||
"id": 11,
|
||||
"name": "library/busybox",
|
||||
@ -46,12 +46,16 @@ describe('RepositoryComponent (inline template)', ()=> {
|
||||
"tags_count": 1
|
||||
}
|
||||
];
|
||||
let mockRepo: Repository = {
|
||||
metadata: { xTotalCount: 2 },
|
||||
data: mockData
|
||||
};
|
||||
|
||||
let config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: '/api/repository/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(()=>{
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
@ -64,7 +68,7 @@ describe('RepositoryComponent (inline template)', ()=> {
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue : config },
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
|
||||
{ provide: Router, useClass: RouterStub }
|
||||
@ -72,20 +76,20 @@ describe('RepositoryComponent (inline template)', ()=> {
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(()=>{
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RepositoryComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.projectId = 1;
|
||||
comp.hasProjectAdminRole = true;
|
||||
repositoryService = fixture.debugElement.injector.get(RepositoryService);
|
||||
|
||||
spy = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockData));
|
||||
spy = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should load and render data', async(()=>{
|
||||
it('should load and render data', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell'));
|
||||
fixture.detectChanges();
|
||||
@ -96,9 +100,9 @@ describe('RepositoryComponent (inline template)', ()=> {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should filter data by keyword', async(()=>{
|
||||
it('should filter data by keyword', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
comp.doSearchRepoNames('nginx');
|
||||
fixture.detectChanges();
|
||||
|
@ -14,7 +14,7 @@
|
||||
import { Component, OnInit, ViewChild, Input } from '@angular/core';
|
||||
|
||||
import { RepositoryService } from '../service/repository.service';
|
||||
import { Repository } from '../service/interface';
|
||||
import { Repository, RepositoryItem } from '../service/interface';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@ -39,7 +39,7 @@ import { REPOSITORY_STYLE } from './repository.component.css';
|
||||
styles: [REPOSITORY_STYLE]
|
||||
})
|
||||
export class RepositoryComponent implements OnInit {
|
||||
changedRepositories: Repository[];
|
||||
changedRepositories: RepositoryItem[];
|
||||
|
||||
@Input() projectId: number;
|
||||
@Input() urlPrefix: string;
|
||||
@ -82,11 +82,11 @@ export class RepositoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
retrieve(state?: State) {
|
||||
toPromise<Repository[]>(this.repositoryService
|
||||
toPromise<Repository>(this.repositoryService
|
||||
.getRepositories(this.projectId, this.lastFilteredRepoName))
|
||||
.then(
|
||||
response => {
|
||||
this.changedRepositories = response;
|
||||
this.changedRepositories = response.data;
|
||||
},
|
||||
error => this.errorHandler.error(error));
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ export abstract class AccessLogService {
|
||||
* @abstract
|
||||
* @param {(number | string)} projectId
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
|
||||
* @returns {(Observable<AccessLog> | Promise<AccessLog> | AccessLog)}
|
||||
*
|
||||
* @memberOf AccessLogService
|
||||
*/
|
||||
abstract getAuditLogs(projectId: number | string, queryParams?: RequestQueryParams): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[];
|
||||
abstract getAuditLogs(projectId: number | string, queryParams?: RequestQueryParams): Observable<AccessLog> | Promise<AccessLog> | AccessLog;
|
||||
|
||||
/**
|
||||
* Get the recent logs.
|
||||
@ -57,8 +57,8 @@ export class AccessLogDefaultService extends AccessLogService {
|
||||
super();
|
||||
}
|
||||
|
||||
public getAuditLogs(projectId: number | string, queryParams?: RequestQueryParams): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[] {
|
||||
return Observable.of([]);
|
||||
public getAuditLogs(projectId: number | string, queryParams?: RequestQueryParams): Observable<AccessLog> | Promise<AccessLog> | AccessLog {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
public getRecentLogs(queryParams?: RequestQueryParams): Observable<AccessLog> | Promise<AccessLog> | AccessLog {
|
||||
|
@ -12,13 +12,14 @@ export interface Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Repository
|
||||
* Interface for Repository Info
|
||||
*
|
||||
* @export
|
||||
* @interface Repository
|
||||
* @extends {Base}
|
||||
*/
|
||||
export interface Repository extends Base {
|
||||
export interface RepositoryItem extends Base {
|
||||
[key: string]: any | any[]
|
||||
name: string;
|
||||
tags_count: number;
|
||||
owner_id?: number;
|
||||
@ -28,6 +29,17 @@ export interface Repository extends Base {
|
||||
pull_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for repository
|
||||
*
|
||||
* @export
|
||||
* @interface Repository
|
||||
*/
|
||||
export interface Repository {
|
||||
metadata?: Metadata;
|
||||
data: RepositoryItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the tag of repository
|
||||
*
|
||||
@ -124,7 +136,7 @@ export interface AccessLog {
|
||||
* @interface AccessLogItem
|
||||
*/
|
||||
export interface AccessLogItem {
|
||||
[key: string]: any
|
||||
[key: string]: any | any[]
|
||||
log_id: number;
|
||||
project_id: number;
|
||||
repo_name: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { RequestQueryParams } from './RequestQueryParams';
|
||||
import { Repository } from './interface';
|
||||
import { Repository, RepositoryItem } from './interface';
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import 'rxjs/add/observable/of';
|
||||
import { Http } from '@angular/http';
|
||||
@ -27,11 +27,11 @@ export abstract class RepositoryService {
|
||||
* @param {(number | string)} projectId
|
||||
* @param {string} repositoryName
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Repository[]> | Promise<Repository[]> | Repository[])}
|
||||
* @returns {(Observable<Repository> | Promise<Repository> | Repository)}
|
||||
*
|
||||
* @memberOf RepositoryService
|
||||
*/
|
||||
abstract getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository[]> | Promise<Repository[]> | Repository[];
|
||||
abstract getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository> | Promise<Repository> | Repository;
|
||||
|
||||
/**
|
||||
* DELETE the specified repository.
|
||||
@ -61,7 +61,7 @@ export class RepositoryDefaultService extends RepositoryService {
|
||||
super();
|
||||
}
|
||||
|
||||
public getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository[]> | Promise<Repository[]> | Repository[] {
|
||||
public getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository> | Promise<Repository> | Repository {
|
||||
if (!projectId) {
|
||||
return Promise.reject("Bad argument");
|
||||
}
|
||||
@ -71,14 +71,29 @@ export class RepositoryDefaultService extends RepositoryService {
|
||||
}
|
||||
|
||||
queryParams.set('project_id', "" + projectId);
|
||||
queryParams.set('detail', '1');
|
||||
if (repositoryName && repositoryName.trim() !== '') {
|
||||
queryParams.set('q', repositoryName);
|
||||
}
|
||||
|
||||
let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : "/api/repositories";
|
||||
return this.http.get(url, buildHttpRequestOptions(queryParams)).toPromise()
|
||||
.then(response => response.json() as Repository[])
|
||||
.then(response => {
|
||||
let result: Repository = {
|
||||
metadata: { xTotalCount: 0 },
|
||||
data: []
|
||||
};
|
||||
|
||||
if (response && response.headers) {
|
||||
let xHeader: string = response.headers.get("X-Total-Count");
|
||||
if (xHeader) {
|
||||
result.metadata.xTotalCount = parseInt(xHeader, 0);
|
||||
}
|
||||
}
|
||||
|
||||
result.data = response.json() as RepositoryItem[];
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import 'rxjs/add/operator/toPromise';
|
||||
import { RequestOptions, Headers } from '@angular/http';
|
||||
import { RequestQueryParams } from './service/RequestQueryParams';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Comparator } from 'clarity-angular';
|
||||
import { Comparator, State } from 'clarity-angular';
|
||||
|
||||
/**
|
||||
* Convert the different async channels to the Promise<T> type.
|
||||
@ -136,3 +136,86 @@ export const VULNERABILITY_SCAN_STATUS = {
|
||||
stopped: "stopped",
|
||||
finished: "finished"
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate page number by state
|
||||
*/
|
||||
export function calculatePage(state: State): number {
|
||||
if (!state || !state.page) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.ceil((state.page.to + 1) / state.page.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter columns via RegExp
|
||||
*
|
||||
* @export
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
export function doFiltering<T extends {[key:string]: any | any[]}>(items: T[], state: State): T[] {
|
||||
if (!items || items.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (!state || !state.filters || state.filters.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
state.filters.forEach((filter: {
|
||||
property: string;
|
||||
value: string;
|
||||
}) => {
|
||||
items = items.filter(item => regexpFilter(filter["value"], item[filter["property"]]));
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match items via RegExp
|
||||
*
|
||||
* @export
|
||||
* @param {string} terms
|
||||
* @param {*} testedValue
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function regexpFilter(terms: string, testedValue: any): boolean {
|
||||
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||
return reg.test(testedValue);
|
||||
}
|
||||
|
||||
export function doSorting<T extends {[key:string]: any | any[]}>(items: T[], state: State): T[] {
|
||||
if (!items || items.length === 0) {
|
||||
return items;
|
||||
}
|
||||
if (!state || !state.sort) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.sort((a: T, b: T) => {
|
||||
let comp: number = 0;
|
||||
if (typeof state.sort.by !== "string") {
|
||||
comp = state.sort.by.compare(a, b);
|
||||
} else {
|
||||
let propA = a[state.sort.by.toString()], propB = b[state.sort.by.toString()];
|
||||
if (typeof propA === "string") {
|
||||
comp = propA.localeCompare(propB);
|
||||
} else {
|
||||
if (propA > propB) {
|
||||
comp = 1;
|
||||
} else if (propA < propB) {
|
||||
comp = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sort.reverse) {
|
||||
comp = -comp;
|
||||
}
|
||||
|
||||
return comp;
|
||||
});
|
||||
}
|
@ -31,7 +31,7 @@
|
||||
"clarity-icons": "^0.9.8",
|
||||
"clarity-ui": "^0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "~0.2.63",
|
||||
"harbor-ui": "0.2.81",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user