-
- {{'REPLICATION.REPLICATION_JOBS' | translate}}
-
-
+
+
+
{{'REPLICATION.REPLICATION_JOBS' | translate}}
+
-
-
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication.component.ts b/src/ui_ng/src/app/replication/replication.component.ts
index 6157ef99a..3c710ebc0 100644
--- a/src/ui_ng/src/app/replication/replication.component.ts
+++ b/src/ui_ng/src/app/replication/replication.component.ts
@@ -15,6 +15,8 @@ import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
+import { State } from 'clarity-angular';
+
const ruleStatus = [
{ 'key': '', 'description': 'REPLICATION.ALL_STATUS'},
{ 'key': '1', 'description': 'REPLICATION.ENABLED'},
@@ -41,6 +43,8 @@ class SearchOption {
status: string = '';
startTime: string = '';
endTime: string = '';
+ page: number = 1;
+ pageSize: number = 5;
}
@Component({
@@ -62,10 +66,14 @@ export class ReplicationComponent implements OnInit {
changedPolicies: Policy[];
changedJobs: Job[];
+ initSelectedId: number;
policies: Policy[];
jobs: Job[];
+ jobsTotalRecordCount: number;
+ jobsTotalPage: number;
+
toggleJobSearchOption = optionalSearch;
currentJobSearchOption: number;
@@ -96,9 +104,13 @@ export class ReplicationComponent implements OnInit {
.subscribe(
response=>{
this.changedPolicies = response;
+ if(this.changedPolicies && this.changedPolicies.length > 0) {
+ this.initSelectedId = this.changedPolicies[0].id;
+ }
this.policies = this.changedPolicies;
if(this.changedPolicies && this.changedPolicies.length > 0) {
- this.fetchPolicyJobs(this.changedPolicies[0].id);
+ this.search.policyId = this.changedPolicies[0].id;
+ this.fetchPolicyJobs();
} else {
this.changedJobs = [];
}
@@ -117,14 +129,19 @@ export class ReplicationComponent implements OnInit {
this.createEditPolicyComponent.openCreateEditPolicy(policyId);
}
- fetchPolicyJobs(policyId: number) {
- this.search.policyId = policyId;
+ fetchPolicyJobs(state?: State) {
+ if(state) {
+ this.search.page = state.page.to + 1;
+ }
console.log('Received policy ID ' + this.search.policyId + ' by clicked row.');
this.replicationService
- .listJobs(this.search.policyId, this.search.status, this.search.repoName, this.search.startTime, this.search.endTime)
+ .listJobs(this.search.policyId, this.search.status, this.search.repoName,
+ this.search.startTime, this.search.endTime, this.search.page, this.search.pageSize)
.subscribe(
response=>{
- this.changedJobs = response;
+ this.jobsTotalRecordCount = response.headers.get('x-total-count');
+ this.jobsTotalPage = Math.ceil(this.jobsTotalRecordCount / this.search.pageSize);
+ this.changedJobs = response.json();
this.jobs = this.changedJobs;
},
error=>this.messageService.announceMessage(error.status, 'Failed to fetch jobs with policy ID:' + this.search.policyId, AlertType.DANGER)
@@ -133,7 +150,8 @@ export class ReplicationComponent implements OnInit {
selectOne(policy: Policy) {
if(policy) {
- this.fetchPolicyJobs(policy.id);
+ this.search.policyId = policy.id;
+ this.fetchPolicyJobs();
}
}
@@ -164,7 +182,7 @@ export class ReplicationComponent implements OnInit {
doSearchJobs(repoName: string) {
this.search.repoName = repoName;
- this.fetchPolicyJobs(this.search.policyId);
+ this.fetchPolicyJobs();
}
reloadPolicies(isReady: boolean) {
@@ -178,7 +196,7 @@ export class ReplicationComponent implements OnInit {
}
refreshJobs() {
- this.fetchPolicyJobs(this.search.policyId);
+ this.fetchPolicyJobs();
}
toggleSearchJobOptionalName(option: number) {
@@ -199,7 +217,7 @@ export class ReplicationComponent implements OnInit {
break;
}
console.log('Search jobs filtered by time range, begin: ' + this.search.startTime + ', end:' + this.search.endTime);
- this.fetchPolicyJobs(this.search.policyId);
+ this.fetchPolicyJobs();
}
}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication.service.ts b/src/ui_ng/src/app/replication/replication.service.ts
index 5cefc34c8..bec392cfd 100644
--- a/src/ui_ng/src/app/replication/replication.service.ts
+++ b/src/ui_ng/src/app/replication/replication.service.ts
@@ -62,6 +62,7 @@ export class ReplicationService extends BaseService {
.map(response=>{
return response.status;
})
+ .catch(error=>Observable.throw(error))
.flatMap((status)=>{
if(status === 201) {
return this.http
@@ -109,11 +110,11 @@ export class ReplicationService extends BaseService {
}
// /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=&repository=
- listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = ''): Observable
{
+ listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = '', page: number, pageSize: number): Observable {
console.log('Get jobs under policy ID:' + policyId);
return this.http
- .get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}`)
- .map(response=>response.json() as Job[])
+ .get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}&page=${page}&page_size=${pageSize}`)
+ .map(response=>response)
.catch(error=>Observable.throw(error));
}
diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication.component.html b/src/ui_ng/src/app/replication/total-replication/total-replication.component.html
index 8877c8a68..ac3aa0221 100644
--- a/src/ui_ng/src/app/replication/total-replication/total-replication.component.html
+++ b/src/ui_ng/src/app/replication/total-replication/total-replication.component.html
@@ -1,7 +1,7 @@
-
+
diff --git a/src/ui_ng/src/app/repository/list-repository/list-repository.component.html b/src/ui_ng/src/app/repository/list-repository/list-repository.component.html
index 35a705aa0..595363beb 100644
--- a/src/ui_ng/src/app/repository/list-repository/list-repository.component.html
+++ b/src/ui_ng/src/app/repository/list-repository/list-repository.component.html
@@ -1,18 +1,21 @@
-
- {{'REPOSITORY.NAME' | translate}}
- {{'REPOSITORY.TAGS_COUNT' | translate}}
- {{'REPOSITORY.PULL_COUNT' | translate}}
-
- {{r.name}}
- {{r.tags_count}}
- {{r.pull_count}}
-
- {{'REPOSITORY.COPY_ID' | translate}}
- {{'REPOSITORY.COPY_PARENT_ID' | translate}}
-
- {{'REPOSITORY.DELETE' | translate}}
-
-
+
+ {{'REPOSITORY.NAME' | translate}}
+ {{'REPOSITORY.TAGS_COUNT' | translate}}
+ {{'REPOSITORY.PULL_COUNT' | translate}}
+
+ {{r.name || r.repository_name}}
+ {{r.tags_count}}
+ {{r.pull_count}}
+
+ {{'REPOSITORY.COPY_ID' | translate}}
+ {{'REPOSITORY.COPY_PARENT_ID' | translate}}
+
+ {{'REPOSITORY.DELETE' | translate}}
+
+
- {{repositories ? repositories.length : 0}} {{'REPOSITORY.ITEMS' | translate}}
+
+ {{totalRecordCount || (repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
+
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts b/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts
index cf2f4b978..0c01217e8 100644
--- a/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts
+++ b/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts
@@ -1,17 +1,62 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { Router, NavigationExtras } from '@angular/router';
import { Repository } from '../repository';
+import { State } from 'clarity-angular';
+
+import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
+import { SessionService } from '../../shared/session.service';
+import { signInRoute, ListMode } from '../../shared/shared.const';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html'
})
export class ListRepositoryComponent {
-
+
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() delete = new EventEmitter();
+ @Input() totalPage: number;
+ @Input() totalRecordCount: number;
+ @Output() paginate = new EventEmitter();
+
+ @Input() mode: string = ListMode.FULL;
+
+ pageOffset: number = 1;
+
+ constructor(
+ private router: Router,
+ private searchTrigger: SearchTriggerService,
+ private session: SessionService) { }
+
deleteRepo(repoName: string) {
this.delete.emit(repoName);
}
+
+ refresh(state: State) {
+ if(this.repositories) {
+ this.paginate.emit(state);
+ }
+ }
+
+ public get listFullMode(): boolean {
+ return this.mode === ListMode.FULL;
+ }
+
+ public gotoLink(projectId: number, repoName: string): void {
+ this.searchTrigger.closeSearch(false);
+
+ let linkUrl = ['harbor', 'tags', projectId, repoName];
+ if (!this.session.getCurrentUser()) {
+ let navigatorExtra: NavigationExtras = {
+ queryParams: { "redirect_url": linkUrl.join("/") }
+ };
+
+ this.router.navigate([signInRoute], navigatorExtra);
+ } else {
+ this.router.navigate(linkUrl);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/mock-verfied-signature.ts b/src/ui_ng/src/app/repository/mock-verfied-signature.ts
new file mode 100644
index 000000000..498eac491
--- /dev/null
+++ b/src/ui_ng/src/app/repository/mock-verfied-signature.ts
@@ -0,0 +1,10 @@
+import { VerifiedSignature } from './verified-signature';
+
+export const verifiedSignatures: VerifiedSignature[] = [
+ {
+ "tag": "latest",
+ "hashes": {
+ "sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
+ }
+ }
+];
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/repository.component.html b/src/ui_ng/src/app/repository/repository.component.html
index 29ce9cdf3..91e7bd940 100644
--- a/src/ui_ng/src/app/repository/repository.component.html
+++ b/src/ui_ng/src/app/repository/repository.component.html
@@ -1,18 +1,11 @@
-
-
-
-
-
-
+
-
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/repository.component.ts b/src/ui_ng/src/app/repository/repository.component.ts
index 02147d4dc..88c3cd75e 100644
--- a/src/ui_ng/src/app/repository/repository.component.ts
+++ b/src/ui_ng/src/app/repository/repository.component.ts
@@ -12,6 +12,8 @@ import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message';
import { Subscription } from 'rxjs/Subscription';
+import { State } from 'clarity-angular';
+
const repositoryTypes = [
{ key: '0', description: 'REPOSITORY.MY_REPOSITORY' },
{ key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' }
@@ -29,6 +31,12 @@ export class RepositoryComponent implements OnInit {
currentRepositoryType: {};
lastFilteredRepoName: string;
+ page: number = 1;
+ pageSize: number = 15;
+
+ totalPage: number;
+ totalRecordCount: number;
+
subscription: Subscription;
constructor(
@@ -59,7 +67,7 @@ export class RepositoryComponent implements OnInit {
this.projectId = this.route.snapshot.parent.params['id'];
this.currentRepositoryType = this.repositoryTypes[0];
this.lastFilteredRepoName = '';
- this.retrieve(this.lastFilteredRepoName);
+ this.retrieve();
}
ngOnDestroy(): void {
@@ -68,11 +76,19 @@ export class RepositoryComponent implements OnInit {
}
}
- retrieve(repoName: string) {
+ retrieve(state?: State) {
+ if(state) {
+ this.page = state.page.to + 1;
+ }
this.repositoryService
- .listRepositories(this.projectId, repoName)
+ .listRepositories(this.projectId, this.lastFilteredRepoName, this.page, this.pageSize)
.subscribe(
- response=>this.changedRepositories=response,
+ response=>{
+ this.totalRecordCount = response.headers.get('x-total-count');
+ this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize);
+ console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage);
+ this.changedRepositories=response.json();
+ },
error=>this.messageService.announceMessage(error.status, 'Failed to list repositories.', AlertType.DANGER)
);
}
@@ -83,7 +99,7 @@ export class RepositoryComponent implements OnInit {
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
- this.retrieve(this.lastFilteredRepoName);
+ this.retrieve();
}
@@ -96,6 +112,6 @@ export class RepositoryComponent implements OnInit {
}
refresh() {
- this.retrieve('');
+ this.retrieve();
}
}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/repository.module.ts b/src/ui_ng/src/app/repository/repository.module.ts
index c6d257fff..8bf9c43f9 100644
--- a/src/ui_ng/src/app/repository/repository.module.ts
+++ b/src/ui_ng/src/app/repository/repository.module.ts
@@ -6,20 +6,22 @@ import { SharedModule } from '../shared/shared.module';
import { RepositoryComponent } from './repository.component';
import { ListRepositoryComponent } from './list-repository/list-repository.component';
import { TagRepositoryComponent } from './tag-repository/tag-repository.component';
+import { TopRepoComponent } from './top-repo/top-repo.component';
import { RepositoryService } from './repository.service';
@NgModule({
- imports: [
+ imports: [
SharedModule,
RouterModule
],
- declarations: [
+ declarations: [
RepositoryComponent,
ListRepositoryComponent,
- TagRepositoryComponent
+ TagRepositoryComponent,
+ TopRepoComponent
],
- exports: [ RepositoryComponent ],
- providers: [ RepositoryService ]
+ exports: [RepositoryComponent, ListRepositoryComponent, TopRepoComponent],
+ providers: [RepositoryService]
})
-export class RepositoryModule {}
\ No newline at end of file
+export class RepositoryModule { }
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/repository.service.ts b/src/ui_ng/src/app/repository/repository.service.ts
index 7af17dd07..ecf7c4cef 100644
--- a/src/ui_ng/src/app/repository/repository.service.ts
+++ b/src/ui_ng/src/app/repository/repository.service.ts
@@ -1,19 +1,68 @@
import { Injectable } from '@angular/core';
-import { Http } from '@angular/http';
+import { Http, URLSearchParams, Response } from '@angular/http';
import { Repository } from './repository';
+import { Tag } from './tag';
+import { VerifiedSignature } from './verified-signature';
+
+import { verifiedSignatures } from './mock-verfied-signature';
+
import { Observable } from 'rxjs/Observable'
+import 'rxjs/add/observable/of';
+import 'rxjs/add/operator/mergeMap';
@Injectable()
export class RepositoryService {
constructor(private http: Http){}
- listRepositories(projectId: number, repoName: string): Observable {
+ listRepositories(projectId: number, repoName: string, page?: number, pageSize?: number): Observable {
console.log('List repositories with project ID:' + projectId);
+ let params = new URLSearchParams();
+ params.set('page', page + '');
+ params.set('page_size', pageSize + '');
return this.http
- .get(`/api/repositories?project_id=${projectId}&q=${repoName}&detail=1`)
- .map(response=>response.json() as Repository[])
+ .get(`/api/repositories?project_id=${projectId}&q=${repoName}&detail=1`, {search: params})
+ .map(response=>response)
+ .catch(error=>Observable.throw(error));
+ }
+
+ listTags(repoName: string): Observable {
+ return this.http
+ .get(`/api/repositories/tags?repo_name=${repoName}&detail=1`)
+ .map(response=>response.json())
+ .catch(error=>Observable.throw(error));
+ }
+
+ listNotarySignatures(repoName: string): Observable {
+ return this.http
+ .get(`/api/repositories/signatures?repo_name=${repoName}`)
+ .map(response=>response.json())
+ .catch(error=>Observable.throw(error));
+ }
+
+ listTagsWithVerifiedSignatures(repoName: string): Observable {
+ return this.http
+ .get(`/api/repositories/tags?repo_name=${repoName}&detail=1`)
+ .map(response=>response.json())
+ .catch(error=>Observable.throw(error))
+ .flatMap((tags: Tag[])=>
+ this.http
+ .get(`/api/repositories/signatures?repo_name=${repoName}`)
+ .map(res=>{
+ let signatures = res.json();
+ tags.forEach(t=>{
+ for(let i = 0; i < signatures.length; i++) {
+ if(signatures[i].tag === t.tag) {
+ t.verified = true;
+ break;
+ }
+ }
+ });
+ return tags;
+ })
+ .catch(error=>Observable.throw(error))
+ )
.catch(error=>Observable.throw(error));
}
@@ -24,4 +73,13 @@ export class RepositoryService {
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
+
+ deleteRepoByTag(repoName: string, tag: string): Observable {
+ console.log('Delete repository with repo name:' + repoName + ', tag:' + tag);
+ return this.http
+ .delete(`/api/repositories?repo_name=${repoName}&tag=${tag}`)
+ .map(response=>response.status)
+ .catch(error=>Observable.throw(error));
+ }
+
}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html
index 0fa603c02..0da162479 100644
--- a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html
+++ b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html
@@ -10,13 +10,19 @@
{{'REPOSITORY.ARCHITECTURE' | translate}}
{{'REPOSITORY.OS' | translate}}
-
-
+ {{t.tag}}
+ {{t.pullCommand}}
+
+
+
+ {{t.author}}
+ {{t.created | date: 'yyyy/MM/dd'}}
+ {{t.dockerVersion}}
+ {{t.architecture}}
+ {{t.os}}
- {{'REPOSITORY.SHOW_DETAILS'}}
-
- {{'REPOSITORY.DELETE' | translate}}
+ {{'REPOSITORY.DELETE' | translate}}
diff --git a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts
index b8713e48b..d2e9316f4 100644
--- a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts
+++ b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts
@@ -1,24 +1,91 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
+import { RepositoryService } from '../repository.service';
+import { MessageService } from '../../global-message/message.service';
+import { AlertType, DeletionTargets } from '../../shared/shared.const';
+
+import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
+import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
+
+import { Subscription } from 'rxjs/Subscription';
+
+import { TagView } from '../tag-view';
+
@Component({
selector: 'tag-repository',
templateUrl: 'tag-repository.component.html'
})
-export class TagRepositoryComponent implements OnInit {
+export class TagRepositoryComponent implements OnInit, OnDestroy {
projectId: number;
repoName: string;
- constructor(private route: ActivatedRoute) {}
+ tags: TagView[];
+
+ private subscription: Subscription;
+
+ constructor(
+ private route: ActivatedRoute,
+ private messageService: MessageService,
+ private deletionDialogService: DeletionDialogService,
+ private repositoryService: RepositoryService) {
+ this.subscription = this.deletionDialogService.deletionConfirm$.subscribe(
+ message=>{
+ let tagName = message.data;
+ this.repositoryService
+ .deleteRepoByTag(this.repoName, tagName)
+ .subscribe(
+ response=>{
+ this.retrieve();
+ console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
+ },
+ error=>this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
+ );
+ }
+ )
+ }
ngOnInit() {
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
+ this.tags = [];
+ this.retrieve();
+ }
+
+ ngOnDestroy() {
+ if(this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ retrieve() {
+ this.repositoryService
+ .listTagsWithVerifiedSignatures(this.repoName)
+ .subscribe(
+ items=>{
+ items.forEach(t=>{
+ let tag = new TagView();
+ tag.tag = t.tag;
+ let data = JSON.parse(t.manifest.history[0].v1Compatibility);
+ tag.architecture = data['architecture'];
+ tag.author = data['author'];
+ tag.verified = t.verified || false;
+ tag.created = data['created'];
+ tag.dockerVersion = data['docker_version'];
+ tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag;
+ tag.os = data['os'];
+ this.tags.push(tag);
+ });
+ },
+ error=>this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
}
deleteTag(tagName: string) {
-
+ let message = new DeletionMessage(
+ 'REPOSITORY.DELETION_TITLE_TAG', 'REPOSITORY.DELETION_SUMMARY_TAG',
+ tagName, tagName, DeletionTargets.TAG);
+ this.deletionDialogService.openComfirmDialog(message);
}
}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/tag-view.ts b/src/ui_ng/src/app/repository/tag-view.ts
new file mode 100644
index 000000000..ec8722746
--- /dev/null
+++ b/src/ui_ng/src/app/repository/tag-view.ts
@@ -0,0 +1,10 @@
+export class TagView {
+ tag: string;
+ pullCommand: string;
+ verified: boolean;
+ author: string;
+ created: Date;
+ dockerVersion: string;
+ architecture: string;
+ os: string;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/tag.ts b/src/ui_ng/src/app/repository/tag.ts
new file mode 100644
index 000000000..cd8770a11
--- /dev/null
+++ b/src/ui_ng/src/app/repository/tag.ts
@@ -0,0 +1,27 @@
+/*
+ {
+ "tag": "latest",
+ "manifest": {
+ "schemaVersion": 1,
+ "name": "library/photon",
+ "tag": "latest",
+ "architecture": "amd64",
+ "history": []
+ },
+
+*/
+export class Tag {
+ tag: string;
+ manifest: {
+ schemaVersion: number;
+ name: string;
+ tag: string;
+ architecture: string;
+ history: [
+ {
+ v1Compatibility: string;
+ }
+ ];
+ };
+ verified: boolean;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/top-repo/top-repo.component.html b/src/ui_ng/src/app/repository/top-repo/top-repo.component.html
new file mode 100644
index 000000000..900d9b1ce
--- /dev/null
+++ b/src/ui_ng/src/app/repository/top-repo/top-repo.component.html
@@ -0,0 +1,4 @@
+
+
Popular Repositories
+
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts b/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts
new file mode 100644
index 000000000..cae1c831b
--- /dev/null
+++ b/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts
@@ -0,0 +1,44 @@
+import { Component, OnInit } from '@angular/core';
+
+import { errorHandler } from '../../shared/shared.utils';
+import { AlertType, ListMode } from '../../shared/shared.const';
+import { MessageService } from '../../global-message/message.service';
+import { TopRepoService } from './top-repository.service';
+import { Repository } from '../repository';
+
+@Component({
+ selector: 'top-repo',
+ templateUrl: "top-repo.component.html",
+
+ providers: [TopRepoService]
+})
+export class TopRepoComponent implements OnInit{
+ private topRepos: Repository[] = [];
+
+ constructor(
+ private topRepoService: TopRepoService,
+ private msgService: MessageService
+ ) { }
+
+ public get listMode(): string {
+ return ListMode.READONLY;
+ }
+
+ //Implement ngOnIni
+ ngOnInit(): void {
+ this.getTopRepos();
+ }
+
+ //Get top popular repositories
+ getTopRepos() {
+ this.topRepoService.getTopRepos()
+ .then(repos => repos.forEach(item => {
+ let repo: Repository = new Repository(item.name, item.count);
+ repo.pull_count = 0;
+ this.topRepos.push(repo);
+ }))
+ .catch(error => {
+ this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts b/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts
new file mode 100644
index 000000000..853a6ed34
--- /dev/null
+++ b/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts
@@ -0,0 +1,39 @@
+import { Injectable } from '@angular/core';
+import { Headers, Http, RequestOptions } from '@angular/http';
+import 'rxjs/add/operator/toPromise';
+
+import { TopRepo } from './top-repository';
+
+export const topRepoEndpoint = "/api/repositories/top";
+/**
+ * Declare service to handle the top repositories
+ *
+ *
+ * @export
+ * @class GlobalSearchService
+ */
+@Injectable()
+export class TopRepoService {
+ private headers = new Headers({
+ "Content-Type": 'application/json'
+ });
+ private options = new RequestOptions({
+ headers: this.headers
+ });
+
+ constructor(private http: Http) { }
+
+ /**
+ * Get top popular repositories
+ *
+ * @param {string} keyword
+ * @returns {Promise}
+ *
+ * @memberOf GlobalSearchService
+ */
+ getTopRepos(): Promise {
+ return this.http.get(topRepoEndpoint, this.options).toPromise()
+ .then(response => response.json() as TopRepo[])
+ .catch(error => Promise.reject(error));
+ }
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/top-repo/top-repository.ts b/src/ui_ng/src/app/repository/top-repo/top-repository.ts
new file mode 100644
index 000000000..c8196b711
--- /dev/null
+++ b/src/ui_ng/src/app/repository/top-repo/top-repository.ts
@@ -0,0 +1,4 @@
+export class TopRepo {
+ name: string;
+ count: number;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/repository/verified-signature.ts b/src/ui_ng/src/app/repository/verified-signature.ts
new file mode 100644
index 000000000..4614020fd
--- /dev/null
+++ b/src/ui_ng/src/app/repository/verified-signature.ts
@@ -0,0 +1,17 @@
+/*
+[
+ {
+ "tag": "2.0",
+ "hashes": {
+ "sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
+ }
+ }
+]
+*/
+
+export class VerifiedSignature {
+ tag: string;
+ hashes: {
+ sha256: string;
+ }
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts b/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts
index eb5b8fa09..cd3e68602 100644
--- a/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts
+++ b/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts
@@ -99,10 +99,14 @@ export class CreateEditPolicyComponent implements OnInit {
newDestination(checkedAddNew: boolean): void {
console.log('CheckedAddNew:' + checkedAddNew);
this.isCreateDestination = checkedAddNew;
- this.createEditPolicy.targetName = '';
- this.createEditPolicy.endpointUrl = '';
- this.createEditPolicy.username = '';
- this.createEditPolicy.password = '';
+ if(this.isCreateDestination) {
+ this.createEditPolicy.targetName = '';
+ this.createEditPolicy.endpointUrl = '';
+ this.createEditPolicy.username = '';
+ this.createEditPolicy.password = '';
+ } else {
+ this.prepareTargets();
+ }
}
selectTarget(): void {
diff --git a/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts b/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts
index 928e8e279..d0d971759 100644
--- a/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts
+++ b/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, Output, EventEmitter, HostBinding, OnInit, ViewChild, OnDestroy } from '@angular/core';
+import { Component, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core';
import { ReplicationService } from '../../replication/replication.service';
import { Policy } from '../../replication/policy';
@@ -17,16 +17,16 @@ import { Subscription } from 'rxjs/Subscription';
selector: 'list-policy',
templateUrl: 'list-policy.component.html',
})
-export class ListPolicyComponent implements OnInit, OnDestroy {
+export class ListPolicyComponent implements OnDestroy {
@Input() policies: Policy[];
@Input() projectless: boolean;
+ @Input() selectedId: number;
@Output() reload = new EventEmitter();
@Output() selectOne = new EventEmitter();
@Output() editOne = new EventEmitter();
- selectedId: number;
subscription: Subscription;
constructor(
@@ -53,10 +53,6 @@ export class ListPolicyComponent implements OnInit, OnDestroy {
}
- ngOnInit() {
-
- }
-
ngOnDestroy() {
if(this.subscription) {
this.subscription.unsubscribe();
diff --git a/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts
new file mode 100644
index 000000000..e03583ab7
--- /dev/null
+++ b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts
@@ -0,0 +1,45 @@
+import { Injectable } from '@angular/core';
+import {
+ CanActivate, Router,
+ ActivatedRouteSnapshot,
+ RouterStateSnapshot,
+ CanActivateChild,
+ NavigationExtras
+} from '@angular/router';
+import { SessionService } from '../../shared/session.service';
+import { harborRootRoute, signInRoute } from '../../shared/shared.const';
+
+@Injectable()
+export class AuthCheckGuard implements CanActivate, CanActivateChild {
+ constructor(private authService: SessionService, private router: Router) { }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
+ return new Promise((resolve, reject) => {
+ let user = this.authService.getCurrentUser();
+ if (!user) {
+ this.authService.retrieveUser()
+ .then(() => resolve(true))
+ .catch(error => {
+ //Session retrieving failed then redirect to sign-in
+ //no matter what status code is.
+ //Please pay attention that route 'harborRootRoute' support anonymous user
+ if (state.url != harborRootRoute) {
+ let navigatorExtra: NavigationExtras = {
+ queryParams: { "redirect_url": state.url }
+ };
+ this.router.navigate([signInRoute], navigatorExtra);
+ return resolve(false);
+ } else {
+ return resolve(true);
+ }
+ });
+ } else {
+ return resolve(true);
+ }
+ });
+ }
+
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
+ return this.canActivate(route, state);
+ }
+}
diff --git a/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts b/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts
index ff3ba02c7..c1356d878 100644
--- a/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts
+++ b/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts
@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import {
- Router,
- Resolve,
- ActivatedRouteSnapshot,
+ Router,
+ Resolve,
+ ActivatedRouteSnapshot,
RouterStateSnapshot,
NavigationExtras
} from '@angular/router';
@@ -28,7 +28,7 @@ export class BaseRoutingResolver implements Resolve {
//Please pay attention that route 'harborRootRoute' support anonymous user
if (state.url != harborRootRoute) {
let navigatorExtra: NavigationExtras = {
- queryParams: {"redirect_url": state.url}
+ queryParams: { "redirect_url": state.url }
};
this.router.navigate(['sign-in'], navigatorExtra);
}
diff --git a/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts b/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts
new file mode 100644
index 000000000..0d2b0945d
--- /dev/null
+++ b/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts
@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+import {
+ CanActivate, Router,
+ ActivatedRouteSnapshot,
+ RouterStateSnapshot,
+ CanActivateChild
+} from '@angular/router';
+import { SessionService } from '../../shared/session.service';
+import { harborRootRoute } from '../../shared/shared.const';
+
+@Injectable()
+export class SignInGuard implements CanActivate, CanActivateChild {
+ constructor(private authService: SessionService, private router: Router) { }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
+ //If user has logged in, should not login again
+ return new Promise((resolve, reject) => {
+ let user = this.authService.getCurrentUser();
+ if (!user) {
+ this.authService.retrieveUser()
+ .then(() => {
+ this.router.navigate([harborRootRoute]);
+ return resolve(false);
+ })
+ .catch(error => {
+ return resolve(true);
+ });
+ } else {
+ this.router.navigate([harborRootRoute]);
+ return resolve(false);
+ }
+ });
+ }
+
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
+ return this.canActivate(route, state);
+ }
+}
diff --git a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
index e58e14196..290236015 100644
--- a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
+++ b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
@@ -3,27 +3,57 @@ import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
- CanActivateChild
+ CanActivateChild,
+ NavigationExtras
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
-import { harborRootRoute } from '../../shared/shared.const';
+import { harborRootRoute, signInRoute } from '../../shared/shared.const';
@Injectable()
export class SystemAdminGuard implements CanActivate, CanActivateChild {
constructor(private authService: SessionService, private router: Router) { }
- canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
- let sessionUser = this.authService.getCurrentUser();
-
- let validation = sessionUser != null && sessionUser.has_admin_role > 0;
- if (!validation) {
- this.router.navigateByUrl(harborRootRoute);
- }
-
- return validation;
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
+ return new Promise((resolve, reject) => {
+ let user = this.authService.getCurrentUser();
+ if (!user) {
+ this.authService.retrieveUser()
+ .then(() => {
+ //updated user
+ user = this.authService.getCurrentUser();
+ if (user.has_admin_role > 0) {
+ return resolve(true);
+ } else {
+ this.router.navigate([harborRootRoute]);
+ return resolve(false);
+ }
+ })
+ .catch(error => {
+ //Session retrieving failed then redirect to sign-in
+ //no matter what status code is.
+ //Please pay attention that route 'harborRootRoute' support anonymous user
+ if (state.url != harborRootRoute) {
+ let navigatorExtra: NavigationExtras = {
+ queryParams: { "redirect_url": state.url }
+ };
+ this.router.navigate([signInRoute], navigatorExtra);
+ return resolve(false);
+ } else {
+ return resolve(true);
+ }
+ });
+ } else {
+ if (user.has_admin_role > 0) {
+ return resolve(true);
+ } else {
+ this.router.navigate([harborRootRoute]);
+ return resolve(false);
+ }
+ }
+ });
}
- canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
return this.canActivate(route, state);
}
}
diff --git a/src/ui_ng/src/app/shared/session.service.ts b/src/ui_ng/src/app/shared/session.service.ts
index f20b1e3c2..f2048fd56 100644
--- a/src/ui_ng/src/app/shared/session.service.ts
+++ b/src/ui_ng/src/app/shared/session.service.ts
@@ -64,10 +64,7 @@ export class SessionService {
*/
retrieveUser(): Promise {
return this.http.get(currentUserEndpint, { headers: this.headers }).toPromise()
- .then(response => {
- this.currentUser = response.json() as SessionUser;
- return this.currentUser;
- })
+ .then(response => this.currentUser = response.json() as SessionUser)
.catch(error => this.handleError(error))
}
diff --git a/src/ui_ng/src/app/shared/shared.const.ts b/src/ui_ng/src/app/shared/shared.const.ts
index cfb3ba673..8528de840 100644
--- a/src/ui_ng/src/app/shared/shared.const.ts
+++ b/src/ui_ng/src/app/shared/shared.const.ts
@@ -14,10 +14,16 @@ export const httpStatusCode = {
"Forbidden": 403
};
export const enum DeletionTargets {
- EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET, REPOSITORY
+ EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET, REPOSITORY, TAG
};
-export const harborRootRoute = "/harbor";
+export const harborRootRoute = "/harbor/dashboard";
+export const signInRoute = "/sign-in";
export const enum ActionType {
ADD_NEW, EDIT
+};
+
+export const ListMode = {
+ READONLY: "readonly",
+ FULL: "full"
};
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/shared.module.ts b/src/ui_ng/src/app/shared/shared.module.ts
index aa51e5190..efed71dca 100644
--- a/src/ui_ng/src/app/shared/shared.module.ts
+++ b/src/ui_ng/src/app/shared/shared.module.ts
@@ -28,6 +28,12 @@ import { PortValidatorDirective } from './port.directive';
import { PageNotFoundComponent } from './not-found/not-found.component';
import { AboutDialogComponent } from './about-dialog/about-dialog.component';
+import { AuthCheckGuard } from './route/auth-user-activate.service';
+
+import { StatisticsComponent } from './statictics/statistics.component';
+import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
+import { SignInGuard } from './route/sign-in-guard-activate.service';
+
@NgModule({
imports: [
CoreModule,
@@ -46,7 +52,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component';
CreateEditPolicyComponent,
PortValidatorDirective,
PageNotFoundComponent,
- AboutDialogComponent
+ AboutDialogComponent,
+ StatisticsComponent,
+ StatisticsPanelComponent
],
exports: [
CoreModule,
@@ -62,7 +70,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component';
CreateEditPolicyComponent,
PortValidatorDirective,
PageNotFoundComponent,
- AboutDialogComponent
+ AboutDialogComponent,
+ StatisticsComponent,
+ StatisticsPanelComponent
],
providers: [
SessionService,
@@ -70,7 +80,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component';
CookieService,
DeletionDialogService,
BaseRoutingResolver,
- SystemAdminGuard]
+ SystemAdminGuard,
+ AuthCheckGuard,
+ SignInGuard]
})
export class SharedModule {
diff --git a/src/ui_ng/src/app/shared/shared.utils.ts b/src/ui_ng/src/app/shared/shared.utils.ts
index 5d6cde0a1..e00dd2f67 100644
--- a/src/ui_ng/src/app/shared/shared.utils.ts
+++ b/src/ui_ng/src/app/shared/shared.utils.ts
@@ -50,10 +50,10 @@ export const isEmptyForm = function (ngForm: NgForm): boolean {
export const accessErrorHandler = function (error: any, msgService: MessageService): boolean {
if (error && error.status && msgService) {
if (error.status === httpStatusCode.Unauthorized) {
- this.msgService.announceAppLevelMessage(error.status, "UNAUTHORIZED_ERROR", AlertType.DANGER);
+ msgService.announceAppLevelMessage(error.status, "UNAUTHORIZED_ERROR", AlertType.DANGER);
return true;
} else if (error.status === httpStatusCode.Forbidden) {
- this.msgService.announceAppLevelMessage(error.status, "FORBIDDEN_ERROR", AlertType.DANGER);
+ msgService.announceAppLevelMessage(error.status, "FORBIDDEN_ERROR", AlertType.DANGER);
return true;
}
}
diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html
new file mode 100644
index 000000000..e7a860ed4
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html
@@ -0,0 +1,25 @@
+
+
{{'STATISTICS.TITLE' | translate }}
+
+
+
+{{'STATISTICS.PRO_ITEM' | translate }}
+
+
+
+
+
+
+
+
+
+ {{'STATISTICS.REPO_ITEM' | translate }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts
new file mode 100644
index 000000000..bd8d47371
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts
@@ -0,0 +1,47 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+import { StatisticsService } from './statistics.service';
+import { errorHandler } from '../../shared/shared.utils';
+import { AlertType } from '../../shared/shared.const';
+
+import { MessageService } from '../../global-message/message.service';
+
+import { Statistics } from './statistics';
+
+import { SessionService } from '../session.service';
+
+@Component({
+ selector: 'statistics-panel',
+ templateUrl: "statistics-panel.component.html",
+ styleUrls: ['statistics.component.css'],
+ providers: [StatisticsService]
+})
+
+export class StatisticsPanelComponent implements OnInit {
+
+ private originalCopy: Statistics = new Statistics();
+
+ constructor(
+ private statistics: StatisticsService,
+ private msgService: MessageService,
+ private session: SessionService) { }
+
+ ngOnInit(): void {
+ if (this.session.getCurrentUser()) {
+ this.getStatistics();
+ }
+ }
+
+ getStatistics(): void {
+ this.statistics.getStatistics()
+ .then(statistics => this.originalCopy = statistics)
+ .catch(error => {
+ this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
+ })
+ }
+
+ public get isValidSession(): boolean {
+ let user = this.session.getCurrentUser();
+ return user && user.has_admin_role > 0;
+ }
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.css b/src/ui_ng/src/app/shared/statictics/statistics.component.css
new file mode 100644
index 000000000..51b62a853
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics.component.css
@@ -0,0 +1,30 @@
+.statistic-wrapper {
+ padding: 12px;
+ margin: 12px;
+ text-align: center;
+ vertical-align: middle;
+ height: 72px;
+ min-width: 108px;
+ max-width: 216px;
+ display: inline-block;
+}
+
+.statistic-data {
+ font-size: 48px;
+ font-weight: bolder;
+ font-family: "Metropolis";
+ line-height: 48px;
+}
+
+.statistic-text {
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 24px;
+ text-transform: uppercase;
+ font-family: "Metropolis";
+}
+
+.statistic-column-title {
+ position: relative;
+ top: 40%;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.html b/src/ui_ng/src/app/shared/statictics/statistics.component.html
new file mode 100644
index 000000000..642ed916a
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics.component.html
@@ -0,0 +1,4 @@
+
+ {{data.number}}
+ {{data.label}}
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.ts b/src/ui_ng/src/app/shared/statictics/statistics.component.ts
new file mode 100644
index 000000000..f1e5563fb
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics.component.ts
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'statistics',
+ templateUrl: "statistics.component.html",
+ styleUrls: ['statistics.component.css']
+})
+
+export class StatisticsComponent {
+ @Input() data: any;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics.service.ts b/src/ui_ng/src/app/shared/statictics/statistics.service.ts
new file mode 100644
index 000000000..da2b0bafa
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics.service.ts
@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core';
+import { Headers, Http, RequestOptions } from '@angular/http';
+import 'rxjs/add/operator/toPromise';
+
+import { Statistics } from './statistics';
+
+export const statisticsEndpoint = "/api/statistics";
+/**
+ * Declare service to handle the top repositories
+ *
+ *
+ * @export
+ * @class GlobalSearchService
+ */
+@Injectable()
+export class StatisticsService {
+ private headers = new Headers({
+ "Content-Type": 'application/json'
+ });
+ private options = new RequestOptions({
+ headers: this.headers
+ });
+
+ constructor(private http: Http) { }
+
+ getStatistics(): Promise {
+ return this.http.get(statisticsEndpoint, this.options).toPromise()
+ .then(response => response.json() as Statistics)
+ .catch(error => Promise.reject(error));
+ }
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/shared/statictics/statistics.ts b/src/ui_ng/src/app/shared/statictics/statistics.ts
new file mode 100644
index 000000000..405f0e80e
--- /dev/null
+++ b/src/ui_ng/src/app/shared/statictics/statistics.ts
@@ -0,0 +1,10 @@
+export class Statistics {
+ constructor() {}
+
+ my_project_count: number;
+ my_repo_count: number;
+ public_project_count: number;
+ public_repo_count: number;
+ total_project_count: number;
+ total_repo_count: number;
+}
\ No newline at end of file
diff --git a/src/ui_ng/src/images/harbor-logo.png b/src/ui_ng/src/images/harbor-logo.png
new file mode 100644
index 000000000..73cf7df2d
Binary files /dev/null and b/src/ui_ng/src/images/harbor-logo.png differ
diff --git a/src/ui_ng/src/index.html b/src/ui_ng/src/index.html
index bfd2a9a91..37df927a2 100644
--- a/src/ui_ng/src/index.html
+++ b/src/ui_ng/src/index.html
@@ -3,11 +3,11 @@
Clarity Seed App
-
+
Loading...
-
+