Merge latest UI codes.

This commit is contained in:
kunw 2017-03-13 18:20:45 +08:00
parent 1c2fcc168f
commit d700786878
76 changed files with 1220 additions and 365 deletions

View File

@ -25,7 +25,7 @@
<div class="checkbox">
<input type="checkbox" id="rememberme">
<label for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
<a href="javascript:void(0)" class="forgot-password-link" (click)="forgotPassword()">Forgot password</a>
<a href="javascript:void(0)" class="forgot-password-link" (click)="forgotPassword()">{{'SIGN_IN.FORGOT_PWD' | translate}}</a>
</div>
<div [class.visibility-hidden]="!isError" class="error active">
{{ 'SIGN_IN.INVALID_MSG' | translate }}

View File

@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { ClarityModule } from 'clarity-angular';
@ -16,10 +16,19 @@ import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
import { SessionService } from './shared/session.service';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
}
export function initConfig(session: SessionService) {
return () => {
console.info("app init here");
return Promise.resolve(true);
};
}
@NgModule({
declarations: [
AppComponent,
@ -42,7 +51,12 @@ export function HttpLoaderFactory(http: Http) {
}
})
],
providers: [],
providers: [{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [SessionService],
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule {

View File

@ -5,13 +5,14 @@ import { RouterModule } from '@angular/router';
import { ProjectModule } from '../project/project.module';
import { UserModule } from '../user/user.module';
import { AccountModule } from '../account/account.module';
import { RepositoryModule } from '../repository/repository.module';
import { NavigatorComponent } from './navigator/navigator.component';
import { GlobalSearchComponent } from './global-search/global-search.component';
import { FooterComponent } from './footer/footer.component';
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { SearchResultComponent } from './global-search/search-result.component';
import { SearchStartComponent } from './global-search/search-start.component';
import { StartPageComponent } from './start-page/start.component';
import { SearchTriggerService } from './global-search/search-trigger.service';
@ -21,7 +22,8 @@ import { SearchTriggerService } from './global-search/search-trigger.service';
ProjectModule,
UserModule,
AccountModule,
RouterModule
RouterModule,
RepositoryModule
],
declarations: [
NavigatorComponent,
@ -29,7 +31,7 @@ import { SearchTriggerService } from './global-search/search-trigger.service';
FooterComponent,
HarborShellComponent,
SearchResultComponent,
SearchStartComponent
StartPageComponent
],
exports: [ HarborShellComponent ],
providers: [SearchTriggerService]

View File

@ -1,4 +1,4 @@
<form class="search" *ngIf="!shouldHide">
<form class="search">
<label for="search_input">
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder='{{"GLOBAL_SEARCH.PLACEHOLDER" | translate}}'>
</label>

View File

@ -31,10 +31,6 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
private searchTrigger: SearchTriggerService,
private router: Router) { }
public get shouldHide(): boolean {
return this.router.routerState.snapshot.url === harborRootRoute && !this.isResPanelOpened;
}
//Implement ngOnIni
ngOnInit(): void {
this.searchSub = this.searchTerms
@ -43,19 +39,12 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
.subscribe(term => {
this.searchTrigger.triggerSearch(term);
});
this.stateSub = this.searchTrigger.searchInputChan$.subscribe(state => {
this.isResPanelOpened = state;
});
}
ngOnDestroy(): void {
if (this.searchSub) {
this.searchSub.unsubscribe();
}
if (this.stateSub) {
this.stateSub.unsubscribe();
}
}
//Handle the term inputting event

View File

@ -36,6 +36,6 @@ export class GlobalSearchService {
return this.http.get(searchUrl, this.options).toPromise()
.then(response => response.json() as SearchResults)
.catch(error => error);
.catch(error => Promise.reject(error));
}
}

View File

@ -13,6 +13,8 @@
<div class="grid-header-wrapper">
<grid-filter class="grid-filter" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilterProjects($event)"></grid-filter>
</div>
<list-project [projects]="searchResults.project"></list-project>
<list-project [projects]="searchResults.project" [mode]="listMode"></list-project>
<h2>Repositories</h2>
<list-repository [repositories]="searchResults.repository" [mode]="listMode"></list-repository>
</div>
</div>

View File

@ -3,7 +3,7 @@ import { Component, Output, EventEmitter } from '@angular/core';
import { GlobalSearchService } from './global-search.service';
import { SearchResults } from './search-results';
import { errorHandler, accessErrorHandler } from '../../shared/shared.utils';
import { AlertType } from '../../shared/shared.const';
import { AlertType, ListMode } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
import { SearchTriggerService } from './search-trigger.service';
@ -52,6 +52,10 @@ export class SearchResultComponent {
return res//Empty object
}
public get listMode(): string {
return ListMode.READONLY;
}
public get state(): boolean {
return this.stateIndicator;
}

View File

@ -1,17 +0,0 @@
.search-start-wrapper {
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -230px;
}
.search-icon {
position: relative;
right: -6px;
}
.search-font {
font-weight: 600;
font-size: 18px;
}

View File

@ -1,9 +0,0 @@
<h2>Hello {{currentUsername}}, start to use harbor from search</h2>
<div class="search-start-wrapper">
<form class="search">
<label for="search_start_input">
<clr-icon shape="search" size="24" class="search-icon is-highlight"></clr-icon>
<input #startSearchBox id="search_start_input" type="text" class="search-font" (keyup)="search(startSearchBox.value)" placeholder='{{"GLOBAL_SEARCH.PLACEHOLDER" | translate}}' size="60">
</label>
</form>
</div>

View File

@ -1,62 +0,0 @@
import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';;
import { Subscription } from 'rxjs/Subscription';
import { SessionService } from '../../shared/session.service';
import { SessionUser } from '../../shared/session-user';
import { SearchTriggerService } from './search-trigger.service';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
const deBounceTime = 500; //ms
@Component({
selector: 'search-start',
templateUrl: "search-start.component.html",
styleUrls: ['search-start.component.css']
})
export class SearchStartComponent implements OnInit, OnDestroy {
//Keep search term as Subject
private searchTerms = new Subject<string>();
private searchSub: Subscription;
private currentUser: SessionUser = null;
constructor(
private session: SessionService,
private searchTrigger: SearchTriggerService){}
public get currentUsername(): string {
return this.currentUser?this.currentUser.username: "";
}
//Implement ngOnIni
ngOnInit(): void {
this.currentUser = this.session.getCurrentUser();
this.searchSub = this.searchTerms
.debounceTime(deBounceTime)
.distinctUntilChanged()
.subscribe(term => {
this.searchTrigger.triggerSearch(term);
});
}
ngOnDestroy(): void {
if(this.searchSub){
this.searchSub.unsubscribe();
}
}
//Handle the term inputting event
search(term: string): void {
//Send event only when term is not empty
this.searchTerms.next(term);
}
}

View File

@ -4,4 +4,10 @@
.container-override {
position: relative !important;
}
.start-content-padding {
padding-top: 0px !important;
padding-bottom: 0px !important;
padding-left: 0px !important;
}

View File

@ -2,7 +2,7 @@
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch">
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="isStartPage">
<global-message [isAppLevel]="false"></global-message>
<!-- Only appear when searching -->
<search-result></search-result>
@ -10,15 +10,16 @@
</div>
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
<section class="sidenav-content">
<a routerLink="/harbor/dashboard" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.DASHBOARD' | translate}}</a>
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.PROJECTS' | translate}}</a>
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.LOGS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin">
<input id="tabsystem" type="checkbox">
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
<ul class="nav-list">
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/replications/endpoints" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIGS' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/replications/endpoints" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}</a></li>
</ul>
</section>
</section>

View File

@ -11,12 +11,14 @@ import { NavigatorComponent } from '../navigator/navigator.component';
import { SessionService } from '../../shared/session.service';
import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component';
import { SearchStartComponent } from '../global-search/search-start.component';
import { StartPageComponent } from '../start-page/start.component';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { Subscription } from 'rxjs/Subscription';
import { harborRootRoute } from '../../shared/shared.const';
@Component({
selector: 'harbor-shell',
templateUrl: 'harbor-shell.component.html',
@ -40,8 +42,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
@ViewChild(AboutDialogComponent)
private aboutDialog: AboutDialogComponent;
@ViewChild(SearchStartComponent)
private searchSatrt: SearchStartComponent;
@ViewChild(StartPageComponent)
private searchSatrt: StartPageComponent;
//To indicator whwther or not the search results page is displayed
//We need to use this property to do some overriding work
@ -52,6 +54,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService,
private searchTrigger: SearchTriggerService) { }
@ -79,6 +82,10 @@ export class HarborShellComponent implements OnInit, OnDestroy {
}
}
public get isStartPage(): boolean {
return this.router.routerState.snapshot.url.toString() === harborRootRoute;
}
public get showSearch(): boolean {
return this.isSearchResultsOpened;
}

View File

@ -0,0 +1,30 @@
.start-card {
border-right: 1px solid #cccccc;
padding: 24px;
background-color: white;
height: 100%;
}
.row-fill-height {
height: 100%;
}
.row-margin {
margin-left: 24px;
}
.column-fill-height {
height: 100%;
}
.my-card-img {
background-image: url('../../../images/harbor-logo.png');
background-repeat: no-repeat;
background-size: contain;
height: 160px;
}
.my-card-footer {
float: right;
margin-top: 100px;
}

View File

@ -0,0 +1,29 @@
<!-- Authenticated-->
<div class="row row-fill-height row-margin" *ngIf="isSessionValid">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
<statistics-panel></statistics-panel>
<top-repo></top-repo>
</div>
</div>
<!-- Guest -->
<div class="row row-fill-height" *ngIf="!isSessionValid">
<div class="col-xs-12 col-sm-12 col-md-5 col-lg-5 col-xl-5 column-fill-height">
<div class="start-card">
<div class="card-img my-card-img">
</div>
<div class="card-block">
<h3 class="card-title">Getting Start</h3>
<p class="card-text">
{{'START_PAGE.GETTING_START' | translate}}
</p>
</div>
<div class="card-footer my-card-footer">
<a href="http://vmware.github.io/harbor/" target="_blank" class="btn btn-sm btn-link">Learn More</a>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-7 col-lg-7 col-xl-7">
<top-repo></top-repo>
</div>
</div>

View File

@ -0,0 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { SessionService } from '../../shared/session.service';
import { SessionUser } from '../../shared/session-user';
@Component({
selector: 'start-page',
templateUrl: "start.component.html",
styleUrls: ['start.component.css']
})
export class StartPageComponent implements OnInit{
private isSessionValid: boolean = false;
constructor(
private session: SessionService
) { }
ngOnInit(): void {
this.isSessionValid = this.session.getCurrentUser() != null;
}
}

View File

@ -3,16 +3,21 @@
<div class="form-group">
<label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label>
<div class="select">
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)" [(ngModel)]="currentConfig.auth_mode.value">
<option>db_auth</option>
<option>ldap</option>
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)
" [(ngModel)]="currentConfig.auth_mode.value">
<option value="db_auth">{{'CONFIG.AUTH_MODE_DB' | translate }}</option>
<option value="ldap">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.AUTH_MODE' | translate}}</span>
</a>
</div>
</section>
<section class="form-block" *ngIf="showLdap">
<div class="form-group">
<label for="ldapUrl" class="required">LDAP URL</label>
<label for="ldapUrl" class="required">{{'CONFIG.LDAP.URL' | translate}}</label>
<label for="ldapUrl" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUrlInput.invalid && (ldapUrlInput.dirty || ldapUrlInput.touched)">
<input name="ldapUrl" type="text" #ldapUrlInput="ngModel" [(ngModel)]="currentConfig.ldap_url.value"
required
@ -25,7 +30,7 @@
</label>
</div>
<div class="form-group">
<label for="ldapSearchDN" class="required">LDAP Search DN</label>
<label for="ldapSearchDN" class="required">{{'CONFIG.LDAP.SEARCH_DN' | translate}}</label>
<label for="ldapSearchDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchDNInput.invalid && (ldapSearchDNInput.dirty || ldapSearchDNInput.touched)">
<input name="ldapSearchDN" type="text" #ldapSearchDNInput="ngModel" [(ngModel)]="currentConfig.ldap_search_dn.value"
required
@ -35,9 +40,13 @@
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_SEARCH_DN' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="ldapSearchPwd" class="required">LDAP Search Password</label>
<label for="ldapSearchPwd" class="required">{{'CONFIG.LDAP.SEARCH_PWD' | translate}}</label>
<label for="ldapSearchPwd" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchPwdInput.invalid && (ldapSearchPwdInput.dirty || ldapSearchPwdInput.touched)">
<input name="ldapSearchPwd" type="password" #ldapSearchPwdInput="ngModel" [(ngModel)]="currentConfig.ldap_search_password.value"
required
@ -49,7 +58,7 @@
</label>
</div>
<div class="form-group">
<label for="ldapBaseDN" class="required">LDAP Base DN</label>
<label for="ldapBaseDN" class="required">{{'CONFIG.LDAP.BASE_DN' | translate}}</label>
<label for="ldapBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapBaseDNInput.invalid && (ldapBaseDNInput.dirty || ldapBaseDNInput.touched)">
<input name="ldapBaseDN" type="text" #ldapBaseDNInput="ngModel" [(ngModel)]="currentConfig.ldap_base_dn.value"
required
@ -59,9 +68,13 @@
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_BASE_DN' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="ldapFilter">LDAP Filter</label>
<label for="ldapFilter">{{'CONFIG.LDAP.FILTER' | translate}}</label>
<label for="ldapFilter" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right">
<input name="ldapFilter" type="text" #ldapFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_filter.value"
id="ldapFilter"
@ -72,7 +85,7 @@
</label>
</div>
<div class="form-group">
<label for="ldapUid" class="required">LDAP UID</label>
<label for="ldapUid" class="required">{{'CONFIG.LDAP.UID' | translate}}</label>
<label for="ldapUid" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUidInput.invalid && (ldapUidInput.dirty || ldapUidInput.touched)">
<input name="ldapUid" type="text" #ldapUidInput="ngModel" [(ngModel)]="currentConfig.ldap_uid.value"
required
@ -82,16 +95,24 @@
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_UID' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="ldapScope">lDAP Scope</label>
<label for="ldapScope">{{'CONFIG.LDAP.SCOPE' | translate}}</label>
<div class="select">
<select id="ldapScope" name="ldapScope" [(ngModel)]="currentConfig.ldap_scope.value" [disabled]="disabled(currentConfig.ldap_scope)">
<option value="1">Base</option>
<option value="2">OneLevel</option>
<option value="3">Subtree</option>
<option value="1">{{'CONFIG.SCOPE_BASE' | translate }}</option>
<option value="2">{{'CONFIG.SCOPE_ONE_LEVEL' | translate }}</option>
<option value="3">{{'CONFIG.SCOPE_SUBTREE' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_SCOPE' | translate}}</span>
</a>
</div>
</section>
<section class="form-block">
@ -99,17 +120,21 @@
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
<div class="select">
<select id="proCreation" name="proCreation" [(ngModel)]="currentConfig.project_creation_restriction.value" [disabled]="disabled(currentConfig.project_creation_restriction)">
<option>everyone</option>
<option>adminonly</option>
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.AUTH_MODE' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="selfReg">{{'CONFIG.SELF_REGISTRATION' | translate}}</label>
<clr-checkbox name="selfReg" id="selfReg" [(ngModel)]="currentConfig.self_registration.value" [disabled]="disabled(currentConfig.self_registration)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.SELF_REGISTRATION_TOOLTIP' | translate}}</span>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION' | translate}}</span>
</a>
</clr-checkbox>
</div>

View File

@ -17,7 +17,7 @@
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.VERIFY_REMOTE_CERT_TOOLTIP' | translate }}</span>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
@ -42,6 +42,10 @@
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>

View File

@ -35,8 +35,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
constructor(
private configService: ConfigurationService,
private msgService: MessageService,
private configService: ConfigurationService,
private confirmService: DeletionDialogService) { }
ngOnInit(): void {

View File

@ -27,23 +27,25 @@ import { ResetPasswordComponent } from './account/password/reset-password.compon
import { RecentLogComponent } from './log/recent-log.component';
import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
import { SearchStartComponent } from './base/global-search/search-start.component';
import { StartPageComponent } from './base/start-page/start.component';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent },
{ path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{ path: 'sign-up', component: SignUpComponent},
{ path: 'reset_password', component: ResetPasswordComponent},
{
path: 'harbor',
component: HarborShellComponent,
resolve: {
authResolver: BaseRoutingResolver
},
canActivateChild: [AuthCheckGuard],
children: [
{
path: '',
component: SearchStartComponent
path: 'dashboard',
component: StartPageComponent
},
{
path: 'projects',
@ -62,6 +64,7 @@ const harborRoutes: Routes = [
path: 'replications',
component: ReplicationManagementComponent,
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
children: [
{
path: 'rules',
@ -104,7 +107,8 @@ const harborRoutes: Routes = [
},
{
path: 'configs',
component: ConfigurationComponent
component: ConfigurationComponent,
canActivate: [SystemAdminGuard],
}
]
},

View File

@ -1,32 +1,30 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<div class="col-xs-3 push-md-2 flex-xs-middle">
<div class="flex-xs-middle">
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
</div>
<div class="col-xs-3 flex-xs-middle">
<div class="flex-xs-middle">
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<div class="row flex-items-xs-right advance-option" [hidden]="currentOption === 0">
<div class="col-xs-2 push-md-1">
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
<button class="btn btn-link" clrDropdownToggle>
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description | translate}}</a>
</div>
</clr-dropdown>
</div>
<div class="col-xs-5 push-md-1">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
<div class="row flex-items-xs-right" [hidden]="currentOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
<button class="btn btn-link" clrDropdownToggle>
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description | translate}}</a>
</div>
</clr-dropdown>
<div class="flex-xs-middle">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
<clr-datagrid>
<clr-datagrid (clrDgRefresh)="retrieve($event)">
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
@ -39,7 +37,10 @@
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>
{{totalRecordCount}} {{'AUDIT_LOG.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -9,10 +9,11 @@ import { SessionService } from '../shared/session.service';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
export const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'};
import { State } from 'clarity-angular';
const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'};
export class FilterOption {
class FilterOption {
key: string;
description: string;
checked: boolean;
@ -51,6 +52,11 @@ export class AuditLogComponent implements OnInit {
new FilterOption('others', 'AUDIT_LOG.OTHERS', true)
];
pageOffset: number = 1;
pageSize: number = 2;
totalRecordCount: number;
totalPage: number;
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) {
//Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['auditLogResolver']);
@ -60,24 +66,32 @@ export class AuditLogComponent implements OnInit {
this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId);
this.queryParam.project_id = this.projectId;
this.retrieve(this.queryParam);
this.queryParam.page_size = this.pageSize;
}
retrieve(queryParam: AuditLog): void {
retrieve(state?: State): void {
if(state) {
this.queryParam.page = state.page.to + 1;
}
this.auditLogService
.listAuditLogs(queryParam)
.listAuditLogs(this.queryParam)
.subscribe(
response=>this.auditLogs = 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.auditLogs = response.json();
},
error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + queryParam.project_id, AlertType.DANGER);
this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + this.queryParam.project_id, AlertType.DANGER);
}
);
}
doSearchAuditLogs(searchUsername: string): void {
this.queryParam.username = searchUsername;
this.retrieve(this.queryParam);
this.retrieve();
}
doSearchByTimeRange(strDate: string, target: string): void {
@ -91,7 +105,7 @@ export class AuditLogComponent implements OnInit {
break;
}
console.log('Search audit log filtered by time range, begin: ' + this.queryParam.begin_timestamp + ', end:' + this.queryParam.end_timestamp);
this.retrieve(this.queryParam);
this.retrieve();
}
doSearchByOptions() {
@ -109,7 +123,7 @@ export class AuditLogComponent implements OnInit {
operationFilter = [];
}
this.queryParam.keywords = operationFilter.join('/');
this.retrieve(this.queryParam);
this.retrieve();
console.log('Search option filter:' + operationFilter.join('/'));
}
@ -137,6 +151,6 @@ export class AuditLogComponent implements OnInit {
this.doSearchByOptions();
}
refresh(): void {
this.retrieve(this.queryParam);
this.retrieve();
}
}

View File

@ -25,9 +25,9 @@ export class AuditLogService extends BaseService {
super();
}
listAuditLogs(queryParam: AuditLog): Observable<AuditLog[]> {
listAuditLogs(queryParam: AuditLog): Observable<any> {
return this.http
.post(`/api/projects/${queryParam.project_id}/logs/filter`, {
.post(`/api/projects/${queryParam.project_id}/logs/filter?page=${queryParam.page}&page_size=${queryParam.page_size}`, {
begin_timestamp: queryParam.begin_timestamp,
end_timestamp: queryParam.end_timestamp,
keywords: queryParam.keywords,
@ -35,7 +35,7 @@ export class AuditLogService extends BaseService {
project_id: queryParam.project_id,
username: queryParam.username
})
.map(response => response.json() as AuditLog[])
.map(response => response)
.catch(error => this.handleError(error));
}

View File

@ -27,4 +27,6 @@ export class AuditLog {
begin_timestamp: number = 0;
end_timestamp: number = 0;
keywords: string;
page: number;
page_size: number;
}

View File

@ -1,21 +1,17 @@
<clr-datagrid>
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
<!--<clr-dg-action-overflow>
<button class="action-item" (click)="onEdit(p)">Edit</button>
<button class="action-item" (click)="onDelete(p)">Delete</button>
</clr-dg-action-overflow>-->
<clr-dg-row *ngFor="let p of projects">
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
<clr-dg-cell>
{{p.description}}
<harbor-action-overflow *ngIf="isSessionValid">
<harbor-action-overflow *ngIf="listFullMode">
<a href="javascript:void(0)" class="dropdown-item">{{'PROJECT.NEW_POLICY' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </a>
<div class="dropdown-divider"></div>
@ -23,5 +19,8 @@
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,12 +1,13 @@
import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Router, NavigationExtras } from '@angular/router';
import { Project } from '../project';
import { ProjectService } from '../project.service';
import { SessionService } from '../../shared/session.service';
import { SessionUser } from '../../shared/session-user';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { signInRoute, ListMode } from '../../shared/shared.const';
import { State } from 'clarity-angular';
@Component({
selector: 'list-project',
@ -16,10 +17,17 @@ export class ListProjectComponent implements OnInit {
@Input() projects: Project[];
@Input() totalPage: number;
@Input() totalRecordCount: number;
pageOffset: number = 1;
@Output() paginate = new EventEmitter<State>();
@Output() toggle = new EventEmitter<Project>();
@Output() delete = new EventEmitter<Project>();
private currentUser: SessionUser = null;
@Input() mode: string = ListMode.FULL;
constructor(
private session: SessionService,
@ -27,16 +35,30 @@ export class ListProjectComponent implements OnInit {
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void {
this.currentUser = this.session.getCurrentUser();
}
public get isSessionValid(): boolean {
return this.currentUser != null;
public get listFullMode(): boolean {
return this.mode === ListMode.FULL;
}
goToLink(proId: number): void {
this.router.navigate(['/harbor', 'projects', proId, 'repository']);
this.searchTrigger.closeSearch(false);
let linkUrl = ['harbor', 'projects', proId, 'repository'];
if (!this.session.getCurrentUser()) {
let navigatorExtra: NavigationExtras = {
queryParams: { "redirect_url": linkUrl.join("/") }
};
this.router.navigate([signInRoute], navigatorExtra);
} else {
this.router.navigate(linkUrl);
}
}
refresh(state: State) {
this.paginate.emit(state);
}
toggleProject(p: Project) {

View File

@ -1,11 +1,11 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4 flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon>{{'MEMBER.NEW_MEMBER' | translate }}</button>
<div class="flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.NEW_MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="col-xs-4 flex-xs-middle push-xs-1">
<div class="flex-xs-middle">
<grid-filter filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>

View File

@ -1,12 +1,12 @@
<h1>{{'PROJECT.PROJECTS' | translate}}</h1>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon>{{'PROJECT.NEW_PROJECT' | translate}}</button>
<div class="flex-items-xs-middle">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.NEW_PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="col-xs-5 push-xs-1">
<div class="flex-items-xs-middle">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
<button class="btn btn-link" clrDropdownToggle>
{{projectTypes[currentFilteredType] | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
@ -19,6 +19,6 @@
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)"></list-project>
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
</div>
</div>

View File

@ -21,6 +21,7 @@ import { DeletionTargets } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'};
@ -42,10 +43,18 @@ export class ProjectComponent implements OnInit {
listProject: ListProjectComponent;
currentFilteredType: number = 0;
lastFilteredType: number = 0;
subscription: Subscription;
projectName: string;
isPublic: number;
page: number = 1;
pageSize: number = 3;
totalPage: number;
totalRecordCount: number;
constructor(
private projectService: ProjectService,
private messageService: MessageService,
@ -58,7 +67,7 @@ export class ProjectComponent implements OnInit {
.subscribe(
response=>{
console.log('Successful delete project with ID:' + projectId);
this.retrieve('', this.lastFilteredType);
this.retrieve();
},
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
@ -67,14 +76,23 @@ export class ProjectComponent implements OnInit {
}
ngOnInit(): void {
this.retrieve('', this.lastFilteredType);
this.projectName = '';
this.isPublic = 0;
}
retrieve(name: string, isPublic: number): void {
retrieve(state?: State): void {
if(state) {
this.page = state.page.to + 1;
}
this.projectService
.listProjects(name, isPublic)
.listProjects(this.projectName, this.isPublic, this.page, this.pageSize)
.subscribe(
response => this.changedProjects = <Project[]>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.changedProjects = response.json();
},
error => this.messageService.announceAppLevelMessage(error.status, error, AlertType.WARNING)
);
}
@ -85,20 +103,20 @@ export class ProjectComponent implements OnInit {
createProject(created: boolean) {
if(created) {
this.retrieve('', this.lastFilteredType);
this.retrieve();
}
}
doSearchProjects(projectName: string): void {
console.log('Search for project name:' + projectName);
this.retrieve(projectName, this.lastFilteredType);
this.projectName = projectName;
this.retrieve();
}
doFilterProjects(filteredType: number): void {
console.log('Filter projects with type:' + types[filteredType]);
this.lastFilteredType = filteredType;
this.currentFilteredType = filteredType;
this.retrieve('', this.lastFilteredType);
this.isPublic = filteredType;
this.retrieve();
}
toggleProject(p: Project) {
@ -125,7 +143,7 @@ export class ProjectComponent implements OnInit {
}
refresh(): void {
this.retrieve('', this.lastFilteredType);
this.retrieve();
}
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { Http, Headers, RequestOptions, Response, URLSearchParams } from '@angular/http';
import { Project } from './project';
import { BaseService } from '../service/base.service';
@ -12,6 +12,8 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
@Injectable()
export class ProjectService {
@ -28,10 +30,13 @@ export class ProjectService {
.catch(error=>Observable.throw(error));
}
listProjects(name: string, isPublic: number): Observable<any>{
listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable<any>{
let params = new URLSearchParams();
params.set('page', page + '');
params.set('page_size', pageSize + '');
return this.http
.get(`/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
.map(response=>response.json())
.get(`/api/projects?project_name=${name}&is_public=${isPublic}`, {search: params})
.map(response=>response)
.catch(error=>Observable.throw(error));
}

View File

@ -1,11 +1,11 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between ">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
<div class="row flex-items-xs-between">
<div class="flex-items-xs-middle">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.NEW_ENDPOINT' | translate}}</button>
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
</div>
<div class="col-xs-4 push-xs-1">
<div class="flex-items-xs-middle">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshTargets()"><clr-icon shape="refresh"></clr-icon></a>
</div>

View File

@ -1,4 +1,4 @@
<clr-datagrid>
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
@ -13,5 +13,8 @@
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
<clr-dg-cell><a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK"><clr-icon shape="clipboard"></clr-icon></a></clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (jobs ? jobs.length : 0) }} {{'REPLICATION.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>
{{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Job } from '../job';
import { State } from 'clarity-angular';
@Component({
selector: 'list-job',
@ -7,4 +8,15 @@ import { Job } from '../job';
})
export class ListJobComponent {
@Input() jobs: Job[];
@Input() totalRecordCount: number;
@Input() totalPage: number;
@Output() paginate = new EventEmitter<State>();
pageOffset: number = 1;
refresh(state: State) {
if(this.jobs) {
this.paginate.emit(state);
}
}
}

View File

@ -1,11 +1,11 @@
<h2>{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</h2>
<h2>{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
<nav class="subnav">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="endpoints" routerLinkActive="active">{{'REPLICATION.ENDPOINTS' | translate}}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="rules" routerLinkActive="active">{{'REPLICATION.REPLICATION_RULES' | translate}}</a>
<a class="nav-link" routerLink="rules" routerLinkActive="active">{{'REPLICATION.REPLICATION_RULE' | translate}}</a>
</li>
</ul>
</nav>

View File

@ -1,11 +1,11 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<div class="flex-xs-middle">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="col-xs-5 push-xs-1">
<div class="flex-xs-middle">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentRuleStatus.description | translate}}
@ -19,12 +19,10 @@
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<list-policy [policies]="changedPolicies" [projectless]="false" (selectOne)="selectOne($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="col-xs-4">
<span>{{'REPLICATION.REPLICATION_JOBS' | translate}}</span>
</div>
<div class="col-xs-4">
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOne($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshJobs()"><clr-icon shape="refresh"></clr-icon></a>
@ -40,11 +38,11 @@
<a href="javascript:void(0)" clrDropdownItem *ngFor="let j of jobStatus" (click)="doFilterJobStatus(j.key)"> {{j.description | translate}}</a>
</div>
</clr-dropdown>
<div class="flex-items-xs-bottom">
<div class="flex-items-xs-middle">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doJobSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doJobSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
<list-job [jobs]="changedJobs"></list-job>
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
</div>
</div>

View File

@ -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();
}
}

View File

@ -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<Job[]> {
listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = '', page: number, pageSize: number): Observable<any> {
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));
}

View File

@ -1,7 +1,7 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<div class="col-xs-4 push-xs-1">
<div class="flex-items-xs-middle">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>

View File

@ -1,18 +1,21 @@
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories">
<clr-dg-cell><a [routerLink]="['/harbor', 'tags', projectId, r.name]">{{r.name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_ID' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories">
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}
<harbor-action-overflow *ngIf="listFullMode">
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_ID' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{repositories ? repositories.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>
{{totalRecordCount || (repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -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<string>();
@Input() totalPage: number;
@Input() totalRecordCount: number;
@Output() paginate = new EventEmitter<State>();
@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);
}
}
}

View File

@ -0,0 +1,10 @@
import { VerifiedSignature } from './verified-signature';
export const verifiedSignatures: VerifiedSignature[] = [
{
"tag": "latest",
"hashes": {
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
}
}
];

View File

@ -1,18 +1,11 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
{{currentRepositoryType.description | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let r of repositoryTypes" (click)="doFilterRepositoryByType(r.key)">{{r.description | translate}}</a>
</div>
</clr-dropdown>
<grid-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
<div class="flex-xs-middle">
<grid-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)"></list-repository>
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount" (paginate)="retrieve($event)"></list-repository>
</div>
</div>

View File

@ -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();
}
}

View File

@ -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 {}
export class RepositoryModule { }

View File

@ -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<Repository[]> {
listRepositories(projectId: number, repoName: string, page?: number, pageSize?: number): Observable<any> {
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<Tag[]> {
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<VerifiedSignature[]> {
return this.http
.get(`/api/repositories/signatures?repo_name=${repoName}`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
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<any> {
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));
}
}

View File

@ -10,13 +10,19 @@
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of tags">
<clr-dg-cell></clr-dg-cell>
<clr-dg-cell></clr-dg-cell>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.SHOW_DETAILS'}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTag(t.name)">{{'REPOSITORY.DELETE' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTag(t.tag)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
export class TagView {
tag: string;
pullCommand: string;
verified: boolean;
author: string;
created: Date;
dockerVersion: string;
architecture: string;
os: string;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
<div class="card card-block">
<h3 class="card-title">Popular Repositories</h3>
<list-repository [repositories]="topRepos" [mode]="listMode"></list-repository>
</div>

View File

@ -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);
})
}
}

View File

@ -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<TopRepo>}
*
* @memberOf GlobalSearchService
*/
getTopRepos(): Promise<TopRepo[]> {
return this.http.get(topRepoEndpoint, this.options).toPromise()
.then(response => response.json() as TopRepo[])
.catch(error => Promise.reject(error));
}
}

View File

@ -0,0 +1,4 @@
export class TopRepo {
name: string;
count: number;
}

View File

@ -0,0 +1,17 @@
/*
[
{
"tag": "2.0",
"hashes": {
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
}
}
]
*/
export class VerifiedSignature {
tag: string;
hashes: {
sha256: string;
}
}

View File

@ -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 {

View File

@ -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<boolean>();
@Output() selectOne = new EventEmitter<Policy>();
@Output() editOne = new EventEmitter<number>();
selectedId: number;
subscription: Subscription;
constructor(
@ -53,10 +53,6 @@ export class ListPolicyComponent implements OnInit, OnDestroy {
}
ngOnInit() {
}
ngOnDestroy() {
if(this.subscription) {
this.subscription.unsubscribe();

View File

@ -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> | 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> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -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<SessionUser> {
//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);
}

View File

@ -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> | 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> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -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> | 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> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -64,10 +64,7 @@ export class SessionService {
*/
retrieveUser(): Promise<SessionUser> {
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))
}

View File

@ -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"
};

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -0,0 +1,25 @@
<div class="card card-block">
<h3 class="card-title">{{'STATISTICS.TITLE' | translate }}</h3>
<span class="card-text">
<div class="row">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<span class="statistic-column-title">{{'STATISTICS.PRO_ITEM' | translate }}</span>
</div>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10">
<statistics [data]='{number: originalCopy.my_project_count, label: "my"}'></statistics>
<statistics [data]='{number: originalCopy.public_project_count, label: "pub"}'></statistics>
<statistics [data]='{number: originalCopy.total_project_count, label: "total"}' *ngIf="isValidSession"></statistics>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<span class="statistic-column-title">{{'STATISTICS.REPO_ITEM' | translate }}</span>
</div>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10">
<statistics [data]='{number: originalCopy.my_repo_count, label: "my"}'></statistics>
<statistics [data]='{number: originalCopy.public_repo_count, label: "pub"}'></statistics>
<statistics [data]='{number: originalCopy.total_repo_count, label: "total"}' *ngIf="isValidSession"></statistics>
</div>
</div>
</span>
</div>

View File

@ -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;
}
}

View File

@ -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%;
}

View File

@ -0,0 +1,4 @@
<div class="statistic-wrapper">
<span class="statistic-data">{{data.number}}</span>
<span class="statistic-text">{{data.label}}</span>
</div>

View File

@ -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;
}

View File

@ -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<Statistics> {
return this.http.get(statisticsEndpoint, this.options).toPromise()
.then(response => response.json() as Statistics)
.catch(error => Promise.reject(error));
}
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -3,11 +3,11 @@
<head>
<meta charset="utf-8">
<title>Clarity Seed App</title>
<base href="/">
<base href="/ng">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico?v=2">
</head>
<body>
<harbor-app>Loading...</harbor-app>
</body>
<script type="text/javascript" src="/ng/inline.bundle.js"></script><script type="text/javascript" src="/ng/scripts.bundle.js"></script><script type="text/javascript" src="/ng/styles.bundle.js"></script><script type="text/javascript" src="/ng/vendor.bundle.js"></script><script type="text/javascript" src="/ng/main.bundle.js"></script></body>
</html>

View File

@ -1,7 +1,8 @@
{
"SIGN_IN": {
"REMEMBER": "Remember me",
"INVALID_MSG": "Invalid user name or password"
"INVALID_MSG": "Invalid user name or password",
"FORGOT_PWD": "Forgot password"
},
"SIGN_UP": {
"TITLE": "Sign Up"
@ -71,12 +72,13 @@
"PLACEHOLDER": "Search Harbor..."
},
"SIDE_NAV": {
"DASHBOARD": "Dashboard",
"PROJECTS": "Projects",
"SYSTEM_MGMT": {
"NAME": "System Managements",
"USERS": "Users",
"REPLICATIONS": "Replications",
"CONFIGS": "Configurations"
"NAME": "Administration",
"USER": "Users",
"REPLICATION": "Replication",
"CONFIG": "Configuration"
},
"LOGS": "Logs"
},
@ -134,6 +136,7 @@
"NEW_MEMBER": "New Member",
"NAME": "Name",
"ROLE": "Role",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"DEVELOPER": "Developer",
"GUEST": "Guest",
@ -165,7 +168,8 @@
"FILTER_PLACEHOLDER": "Filter Logs"
},
"REPLICATION": {
"REPLICATION_RULES": "Replication Rules",
"REPLICATION_RULE": "Rules",
"NEW_REPLICATION_RULE": "New Replication Rule",
"ENDPOINTS": "Endpoints",
"FILTER_POLICIES_PLACEHOLDER": "Filter Policies",
"FILTER_JOBS_PLACEHOLDER": "Filter Jobs",
@ -182,7 +186,7 @@
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping target.",
"NAME": "Name",
"PROJECT": "Project",
"PROJECT": "Project",
"NAME_IS_REQUIRED": "Name is required.",
"DESCRIPTION": "Description",
"ENABLE": "Enable",
@ -194,7 +198,6 @@
"DESTINATION_URL_IS_REQUIRED": "Endpoint URL is required.",
"DESTINATION_USERNAME": "Username",
"DESTINATION_PASSWORD": "Password",
"REPLICATION_RULE": "Replication Rule",
"ALL_STATUS": "All Status",
"ENABLED": "Enabled",
"DISABLED": "Disabled",
@ -219,7 +222,7 @@
"ITEMS": "item(s)"
},
"DESTINATION": {
"ENDPOINT": "Endpoint",
"NEW_ENDPOINT": "New Endpoint",
"NAME": "Destination Name",
"NAME_IS_REQUIRED": "Destination name is required.",
"URL": "Endpoint URL",
@ -297,12 +300,37 @@
"MAIL_SSL": "Email SSL",
"SSL_TOOLTIP": "Enable SSL for email server connection",
"VERIFY_REMOTE_CERT": "Verify Remote Certificate",
"VERIFY_REMOTE_CERT_TOOLTIP": "Determine whether the image replication should verify the certificate of a remote Habor registry. Uncheck this box when the remote registry uses a self -signed or untrusted certificate.",
"TOKEN_EXPIRATION": "Token Expiration (Minutes)",
"AUTH_MODE": "Authentication",
"PRO_CREATION_RESTRICTION": "Project Creation Restriction",
"SELF_REGISTRATION": "Self Registration",
"SELF_REGISTRATION_TOOLTIP": "Enable sign up"
"AUTH_MODE_DB": "Database",
"AUTH_MODE_LDAP": "LDAP",
"SCOPE_BASE": "Base",
"SCOPE_ONE_LEVEL": "OneLevel",
"SCOPE_SUBTREE": "Subtree",
"PRO_CREATION_EVERYONE": "Everyone",
"PRO_CREATION_ADMIN": "Admin Only",
"TOOLTIP": {
"SELF_REGISTRATION": "Enable sign up",
"VERIFY_REMOTE_CERT": "Determine whether the image replication should verify the certificate of a remote Habor registry. Uncheck this box when the remote registry uses a self -signed or untrusted certificate.",
"AUTH_MODE": "By default the auth mode is db_auth, i.e. the credentials are stored in a local database.Set it to ldap_auth if you want to verify a user's credentials against an LDAP server.",
"LDAP_SEARCH_DN": "A user's DN who has the permission to search the LDAP/AD server.If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.",
"LDAP_BASE_DN": "The base DN from which to look up a user in LDAP/AD",
"LDAP_UID": "The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD",
"LDAP_SCOPE": "The scope to search for users",
"TOKEN_EXPIRATION": "The expiration time (in minute) of token created by token service, default is 30 minutes",
"PRO_CREATION_RESTRICTION": "The flag to control what users have permission to create projects,Be default everyone can create a project, set to 'adminonly' such that only admin can create project."
},
"LDAP": {
"URL": "LDAP URL",
"SEARCH_DN": "LDAP Search DN",
"SEARCH_PWD": "LDAP Search Password",
"BASE_DN": "LDAP Base DN",
"FILTER": "LDAP Filter",
"UID": "LDAP UID",
"SCOPE": "lDAP Scope"
}
},
"PAGE_NOT_FOUND": {
"MAIN_TITLE": "Page not found",
@ -317,6 +345,19 @@
"END_USER_LICENSE": "End User License Agreement",
"OPEN_SOURCE_LICENSE": "Open Source/Third Party License"
},
"START_PAGE": {
"GETTING_START": "Project Harbor is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security.",
"GETTING_START_TITLE": "Getting Start"
},
"TOP_REPO": "Popular Repositories",
"STATISTICS": {
"TITLE": "STATISTICS",
"PRO_ITEM": "PROJECTS",
"REPO_ITEM": "REPOSITORIES",
"INDEX_MY": "MY",
"INDEX_PUB": "PUBLIC",
"INDEX_TOTAL": "TOTAL"
},
"UNKNOWN_ERROR": "There are some unknown errors occurred, please try later",
"UNAUTHORIZED_ERROR": "Session is invalid or expired, you need to sign in to continue the operation",
"FORBIDDEN_ERROR": "You are not allowed to trigger the operation"

View File

@ -1,7 +1,8 @@
{
"SIGN_IN": {
"REMEMBER": "记住我",
"INVALID_MSG": "用户名或者密码不正确"
"INVALID_MSG": "用户名或者密码不正确",
"FORGOT_PWD": "忘记密码"
},
"SIGN_UP": {
"TITLE": "注册"
@ -71,12 +72,13 @@
"PLACEHOLDER": "搜索 Harbor..."
},
"SIDE_NAV": {
"DASHBOARD": "仪表板",
"PROJECTS": "项目",
"SYSTEM_MGMT": {
"NAME": "系统管理",
"USERS": "用户管理",
"REPLICATIONS": "复制管理",
"CONFIGS": "配置管理"
"USER": "用户管理",
"REPLICATION": "复制管理",
"CONFIG": "配置管理"
},
"LOGS": "日志"
},
@ -131,7 +133,7 @@
"PROJECTS": "项目"
},
"MEMBER": {
"NEW_MEMBER": "新成员",
"NEW_MEMBER": "新成员",
"NAME": "姓名",
"ROLE": "角色",
"SYS_ADMIN": "系统管理员",
@ -166,7 +168,8 @@
"FILTER_PLACEHOLDER": "过滤日志"
},
"REPLICATION": {
"REPLICATION_RULES": "复制",
"REPLICATION_RULE": "复制策略",
"NEW_REPLICATION_RULE": "新建策略",
"ENDPOINTS": "目标",
"FILTER_POLICIES_PLACEHOLDER": "过滤策略",
"FILTER_JOBS_PLACEHOLDER": "过滤任务",
@ -175,7 +178,7 @@
"FILTER_TARGETS_PLACEHOLDER": "过滤目标",
"DELETION_TITLE_TARGET": "删除目标确认",
"DELETION_SUMMARY_TARGET": "确认删除目标 {{param}}?",
"ADD_POLICY": "新策略",
"ADD_POLICY": "新策略",
"EDIT_POLICY": "修改策略",
"DELETE_POLICY": "删除策略",
"TEST_CONNECTION": "测试连接",
@ -195,7 +198,6 @@
"DESTINATION_URL_IS_REQUIRED": "目标URL为必填项。",
"DESTINATION_USERNAME": "用户名",
"DESTINATION_PASSWORD": "密码",
"REPLICATION_RULE": "创建策略",
"ALL_STATUS": "所有状态",
"ENABLED": "启用",
"DISABLED": "停用",
@ -220,7 +222,7 @@
"ITEMS": "条记录"
},
"DESTINATION": {
"ENDPOINT": "目标",
"NEW_ENDPOINT": "新建目标",
"NAME": "目标名",
"NAME_IS_REQUIRED": "目标名为必填项。",
"URL": "目标URL",
@ -239,7 +241,7 @@
"FAILED_TO_GET_TARGET": "获取目标失败。",
"CREATION_TIME": "创建时间",
"ITEMS": "条记录"
},
},
"REPOSITORY": {
"COPY_ID": "复制ID",
"COPY_PARENT_ID": "复制父级ID",
@ -298,12 +300,37 @@
"MAIL_SSL": "邮件 SSL",
"SSL_TOOLTIP": "应用SSL到邮件服务器连接",
"VERIFY_REMOTE_CERT": "验证远程证书",
"VERIFY_REMOTE_CERT_TOOLTIP": "确定镜像复制是否要验证远程Harbor镜像库的证书。如果远程镜像库使用的是自签或者非信任证书不要勾选此选项。",
"TOKEN_EXPIRATION": "令牌过期时间(分钟)",
"AUTH_MODE": "认证模式",
"PRO_CREATION_RESTRICTION": "项目创建限制",
"SELF_REGISTRATION": "自注册",
"SELF_REGISTRATION_TOOLTIP": "激活注册功能"
"AUTH_MODE_DB": "数据库",
"AUTH_MODE_LDAP": "LDAP",
"SCOPE_BASE": "基础",
"SCOPE_ONE_LEVEL": "单级",
"SCOPE_SUBTREE": "子树",
"PRO_CREATION_EVERYONE": "所有人",
"PRO_CREATION_ADMIN": "仅管理员",
"TOOLTIP": {
"SELF_REGISTRATION": "激活注册功能",
"VERIFY_REMOTE_CERT": "确定镜像复制是否要验证远程Harbor镜像库的证书。如果远程镜像库使用的是自签或者非信任证书不要勾选此选项。",
"AUTH_MODE": "默认认证模式为本地认证比如用户凭证存储在本地数据库。如果使用LDAP服务来认证用户则设置为LDAP服务。",
"LDAP_SEARCH_DN": "有权搜索LDAP服务器的用户的DN。如果LDAP服务器不支持匿名搜索则需要配置此DN之和搜索密码。",
"LDAP_BASE_DN": "用来在LDAP和AD中搜寻用户的基础DN。",
"LDAP_UID": "在搜索中用来匹配用户的属性可以是uid,cn,email,sAMAccountName或者其它LDAP/AD服务器支持的属性。",
"LDAP_SCOPE": "搜索用户的范围",
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间分钟默认为30分钟。",
"PRO_CREATION_RESTRICTION": "用来控制那些用户有权创建项目的标志位,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。"
},
"LDAP": {
"URL": "LDAP地址",
"SEARCH_DN": "LDAP搜索专有名称DN)",
"SEARCH_PWD": "LDAP搜索密码",
"BASE_DN": "LDAP基础专有名称DN)",
"FILTER": "LDAP过滤器",
"UID": "LDAP用户标识UID)",
"SCOPE": "lDAP范围"
}
},
"PAGE_NOT_FOUND": {
"MAIN_TITLE": "页面不存在",
@ -318,6 +345,19 @@
"END_USER_LICENSE": "终端用户许可协议",
"OPEN_SOURCE_LICENSE": "开源/第三方许可协议"
},
"START_PAGE": {
"GETTING_START": "Project Harbor is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security.",
"GETTING_START_TITLE": "从这开始"
},
"TOP_REPO": "受欢迎镜像库",
"STATISTICS": {
"TITLE": "统计",
"PRO_ITEM": "项目",
"REPO_ITEM": "镜像库",
"INDEX_MY": "私有的",
"INDEX_PUB": "公开的",
"INDEX_TOTAL": "总计"
},
"UNKNOWN_ERROR": "发生未知错误,请稍后再试",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限"