Merge pull request #1552 from vmware/feature/update_ui_ng_code

merge latest ui_ng code in
This commit is contained in:
Daniel Jiang 2017-03-09 10:56:55 +08:00 committed by GitHub
commit 46e953fd5e
50 changed files with 967 additions and 345 deletions

View File

@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { DashboardModule } from '../dashboard/dashboard.module';
import { ProjectModule } from '../project/project.module';
import { UserModule } from '../user/user.module';
import { AccountModule } from '../account/account.module';
@ -12,11 +11,13 @@ 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 { SearchTriggerService } from './global-search/search-trigger.service';
@NgModule({
imports: [
SharedModule,
DashboardModule,
ProjectModule,
UserModule,
AccountModule,
@ -27,9 +28,11 @@ import { SearchResultComponent } from './global-search/search-result.component';
GlobalSearchComponent,
FooterComponent,
HarborShellComponent,
SearchResultComponent
SearchResultComponent,
SearchStartComponent
],
exports: [ HarborShellComponent ]
exports: [ HarborShellComponent ],
providers: [SearchTriggerService]
})
export class BaseModule {

View File

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

View File

@ -1,8 +1,11 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
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 { SearchEvent } from '../search-event';
import { Subscription } from 'rxjs/Subscription';
import { SearchTriggerService } from './search-trigger.service';
import { harborRootRoute } from '../../shared/shared.const';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@ -13,32 +16,52 @@ const deBounceTime = 500; //ms
selector: 'global-search',
templateUrl: "global-search.component.html"
})
export class GlobalSearchComponent implements OnInit {
//Publish search event to parent
@Output() searchEvt = new EventEmitter<SearchEvent>();
export class GlobalSearchComponent implements OnInit, OnDestroy {
//Keep search term as Subject
private searchTerms = new Subject<string>();
//Keep subscription for future use
private searchSub: Subscription;
private stateSub: Subscription;
//To indicate if the result panel is opened
private isResPanelOpened: boolean = false;
constructor(
private searchTrigger: SearchTriggerService,
private router: Router) { }
public get shouldHide(): boolean {
return this.router.routerState.snapshot.url === harborRootRoute && !this.isResPanelOpened;
}
//Implement ngOnIni
ngOnInit(): void {
this.searchTerms
this.searchSub = this.searchTerms
.debounceTime(deBounceTime)
.distinctUntilChanged()
.subscribe(term => {
this.searchEvt.emit({
term: 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
search(term: string): void {
//Send event only when term is not empty
//Send event even term is empty
let nextTerm = term.trim();
if (nextTerm != "") {
this.searchTerms.next(nextTerm);
}
this.searchTerms.next(term.trim());
}
}

View File

@ -2,12 +2,13 @@
display: block;
position: absolute;
height: 100%;
width: 97%;
width: 98%;
/*shoud be lesser than 1000 to aoivd override the popup menu*/
z-index: 999;
box-sizing: border-box;
background: #fafafa;
top: 0px;
padding-left: 24px;
}
.search-header {
@ -17,7 +18,6 @@
}
.search-title {
margin-top: 0px;
font-size: 28px;
letter-spacing: normal;
color: #000;
@ -38,3 +38,13 @@
left: 50%;
position: absolute;
}
.grid-header-wrapper {
text-align: right;
}
.grid-filter {
position: relative;
top: 8px;
margin: 0px auto 0px auto;
}

View File

@ -1,11 +1,18 @@
<div class="search-overlay" *ngIf="state">
<div id="placeholder1" style="height: 24px;"></div>
<div class="search-header">
<span class="search-title">Search results</span>
<span class="search-title">Search results for '{{currentTerm}}'</span>
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
</span>
</div>
<!-- spinner -->
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
<div>Results is showing here!</div>
<div id="results">
<h2>Projects</h2>
<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>
</div>
</div>

View File

@ -2,6 +2,11 @@ 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 { MessageService } from '../../global-message/message.service';
import { SearchTriggerService } from './search-trigger.service';
@Component({
selector: "search-result",
@ -12,19 +17,40 @@ import { SearchResults } from './search-results';
})
export class SearchResultComponent {
@Output() closeEvt = new EventEmitter<boolean>();
private searchResults: SearchResults = new SearchResults();
private originalCopy: SearchResults;
searchResults: SearchResults;
private currentTerm: string = "";
//Open or close
private stateIndicator: boolean = false;
//Search in progress
private onGoing: boolean = true;
private onGoing: boolean = false;
//Whether or not mouse point is onto the close indicator
private mouseOn: boolean = false;
constructor(private search: GlobalSearchService) { }
constructor(
private search: GlobalSearchService,
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
private doFilterProjects(event: string) {
this.searchResults.project = this.originalCopy.project.filter(pro => pro.name.indexOf(event) != -1);
}
private clone(src: SearchResults): SearchResults {
let res: SearchResults = new SearchResults();
if (src) {
src.project.forEach(pro => res.project.push(Object.assign({}, pro)));
src.repository.forEach(repo => res.repository.push(Object.assign({}, repo)))
return res;
}
return res//Empty object
}
public get state(): boolean {
return this.stateIndicator;
@ -46,35 +72,50 @@ export class SearchResultComponent {
//Show the results
show(): void {
this.stateIndicator = true;
this.searchTrigger.searchInputStat(true);
}
//Close the result page
close(): void {
//Tell shell close
this.closeEvt.emit(true);
this.searchTrigger.closeSearch(true);
this.searchTrigger.searchInputStat(false);
this.stateIndicator = false;
}
//Call search service to complete the search request
doSearch(term: string): void {
//Do nothing if search is ongoing
if (this.onGoing) {
return;
}
//Confirm page is displayed
if (!this.stateIndicator) {
this.show();
}
this.currentTerm = term;
//If term is empty, then clear the results
if (term === "") {
this.searchResults.project = [];
this.searchResults.repository = [];
return;
}
//Show spinner
this.onGoing = true;
this.search.doSearch(term)
.then(searchResults => {
this.onGoing = false;
this.searchResults = searchResults;
console.info(searchResults);
this.originalCopy = searchResults; //Keeo the original data
this.searchResults = this.clone(searchResults);
})
.catch(error => {
this.onGoing = false;
console.error(error);//TODO: Use general erro handler
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
});
}
}

View File

@ -2,6 +2,11 @@ import { Project } from '../../project/project';
import { Repository } from '../../repository/repository';
export class SearchResults {
projects: Project[];
repositories: Repository[];
constructor(){
this.project = [];
this.repository = [];
}
project: Project[];
repository: Repository[];
}

View File

@ -0,0 +1,17 @@
.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

@ -0,0 +1,9 @@
<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

@ -0,0 +1,62 @@
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

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { AlertType } from '../../shared/shared.const';
@Injectable()
export class SearchTriggerService {
private searchTriggerSource = new Subject<string>();
private searchCloseSource = new Subject<boolean>();
private searchInputSource = new Subject<boolean>();
searchTriggerChan$ = this.searchTriggerSource.asObservable();
searchCloseChan$ = this.searchCloseSource.asObservable();
searchInputChan$ = this.searchInputSource.asObservable();
triggerSearch(event: string) {
this.searchTriggerSource.next(event);
}
//Set event to true for shell
//set to false for search panel
closeSearch(event: boolean) {
this.searchCloseSource.next(event);
}
//Notify the state change of search box in home start page
searchInputStat(event: boolean) {
this.searchInputSource.next(event);
}
}

View File

@ -1,11 +1,11 @@
<clr-main-container>
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch">
<global-message [isAppLevel]="false"></global-message>
<!-- Only appear when searching -->
<search-result (closeEvt)="searchClose($event)"></search-result>
<search-result></search-result>
<router-outlet></router-outlet>
</div>
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>

View File

@ -1,8 +1,7 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
import { modalEvents } from '../modal-events.const';
import { AccountSettingsModalComponent } from '../../account/account-settings/account-settings-modal.component';
@ -11,7 +10,12 @@ import { PasswordSettingComponent } from '../../account/password/password-settin
import { NavigatorComponent } from '../navigator/navigator.component';
import { SessionService } from '../../shared/session.service';
import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component'
import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component';
import { SearchStartComponent } from '../global-search/search-start.component';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'harbor-shell',
@ -19,7 +23,7 @@ import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.com
styleUrls: ["harbor-shell.component.css"]
})
export class HarborShellComponent implements OnInit {
export class HarborShellComponent implements OnInit, OnDestroy {
@ViewChild(AccountSettingsModalComponent)
private accountSettingsModal: AccountSettingsModalComponent;
@ -36,18 +40,43 @@ export class HarborShellComponent implements OnInit {
@ViewChild(AboutDialogComponent)
private aboutDialog: AboutDialogComponent;
@ViewChild(SearchStartComponent)
private searchSatrt: SearchStartComponent;
//To indicator whwther or not the search results page is displayed
//We need to use this property to do some overriding work
private isSearchResultsOpened: boolean = false;
private searchSub: Subscription;
private searchCloseSub: Subscription;
constructor(
private route: ActivatedRoute,
private session: SessionService) { }
private session: SessionService,
private searchTrigger: SearchTriggerService) { }
ngOnInit() {
this.route.data.subscribe(data => {
//dummy
this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => {
this.doSearch(searchEvt);
});
this.searchCloseSub = this.searchTrigger.searchCloseChan$.subscribe(close => {
if (close) {
this.searchClose();
}else{
this.watchClickEvt();//reuse
}
});
}
ngOnDestroy(): void {
if (this.searchSub) {
this.searchSub.unsubscribe();
}
if (this.searchCloseSub) {
this.searchCloseSub.unsubscribe();
}
}
public get showSearch(): boolean {
@ -82,22 +111,31 @@ export class HarborShellComponent implements OnInit {
}
//Handle the global search event and then let the result page to trigger api
doSearch(event: SearchEvent): void {
doSearch(event: string): void {
if (event === "") {
if (!this.isSearchResultsOpened) {
//Will not open search result panel if term is empty
return;
} else {
//If opened, then close the search result panel
this.isSearchResultsOpened = false;
this.searchResultComponet.close();
return;
}
}
//Once this method is called
//the search results page must be opened
this.isSearchResultsOpened = true;
//Call the child component to do the real work
this.searchResultComponet.doSearch(event.term);
this.searchResultComponet.doSearch(event);
}
//Search results page closed
//remove the related ovevriding things
searchClose(event: boolean): void {
if (event) {
searchClose(): void {
this.isSearchResultsOpened = false;
}
}
//Close serch result panel if existing
watchClickEvt(): void {

View File

@ -1,3 +1,5 @@
export const enum modalEvents {
USER_PROFILE, CHANGE_PWD, ABOUT
export const modalEvents = {
USER_PROFILE: "USER_PROFILE",
CHANGE_PWD: "CHANGE_PWD",
ABOUT: "ABOUT"
}

View File

@ -5,7 +5,7 @@
<span class="title">Harbor</span>
</a>
</div>
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
<global-search></global-search>
<div class="header-actions">
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">

View File

@ -3,7 +3,6 @@ import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
import { modalEvents } from '../modal-events.const';
import { SessionUser } from '../../shared/session-user';
@ -21,7 +20,6 @@ import { supportedLangs, enLang, languageNames } from '../../shared/shared.const
export class NavigatorComponent implements OnInit {
// constructor(private router: Router){}
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
@Output() searchEvt = new EventEmitter<SearchEvent>();
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
private sessionUser: SessionUser = null;
@ -83,11 +81,6 @@ export class NavigatorComponent implements OnInit {
});
}
//Only transfer the search event to the parent shell
transferSearchEvent(evt: SearchEvent): void {
this.searchEvt.emit(evt);
}
//Log out system
logOut(): void {
this.session.signOff()
@ -116,7 +109,7 @@ export class NavigatorComponent implements OnInit {
homeAction(): void {
if(this.sessionUser != null){
//Navigate to default page
this.router.navigate(['harbor','projects']);
this.router.navigate(['harbor']);
}else{
//Naviagte to signin page
this.router.navigate(['sign-in']);

View File

@ -1,4 +0,0 @@
//Define a object to store the search event
export class SearchEvent {
term: string;
}

View File

@ -1,54 +0,0 @@
<h3>Dashboard</h3>
<div class="row">
<div class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-block">
<h1 class="card-title">Why user Harbor?</h1>
<p class="card-text">
Project Harbor is an enterprise-class registry server, which extends the open source Docker Registry server by adding the functionality usually required by an enterprise, such as security, control, and management. Harbor is primarily designed to be a private registry - providing the needed security and control that enterprises require. It also helps minimize ...
</p>
</div>
<div class="card-footer">
<a href="..." class="btn btn-sm btn-link">View all</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-block">
<h1 class="card-title">Getting started</h1>
<ul class="list" style="list-style-type: none;">
<li><img src="../../images/Step1.png" style="width: 19%; height: auto;"/><a style="margin: 30px;" href="">Anonymous repository access</a></li>
<li><img src="../../images/Step2.png" style="width: 19%; height: auto;"/><a style="margin: 30px;" href="">Repositories managed by project</a></li>
<li><img src="../../images/Step3.png" style="width: 19%; height: auto;"/><a style="margin: 30px;" href="">Role based access control</a></li>
</ul>
</div>
</div>
</div>
<div class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-block">
<h1 class="card-title">Activities</h1>
<p class="card-text">
...
</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Version</clr-dg-column>
<clr-dg-column>Count</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories">
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell>{{r.version}}</clr-dg-cell>
<clr-dg-cell>{{r.count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{repositories.length}} item(s)</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,20 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Repository } from '../repository/repository';
@Component({
selector: 'dashboard',
templateUrl: 'dashboard.component.html'
})
export class DashboardComponent implements OnInit {
repositories: Repository[];
ngOnInit(): void {
this.repositories = [
{ name: 'Ubuntu', version: '14.04', count: 1 },
{ name: 'MySQL', version: 'Latest', count: 2 },
{ name: 'Photon', version: '1.0', count: 3 }
];
}
}

View File

@ -1,10 +0,0 @@
import { NgModule } from '@angular/core';
import { DashboardComponent } from './dashboard.component';
import { SharedModule } from '../shared/shared.module';
@NgModule({
imports: [ SharedModule ],
declarations: [ DashboardComponent ],
exports: [ DashboardComponent ]
})
export class DashboardModule {}

View File

@ -14,6 +14,7 @@ import { DestinationComponent } from './replication/destination/destination.comp
import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { RepositoryComponent } from './repository/repository.component';
import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component';
import { ReplicationComponent } from './replication/replication.component';
import { MemberComponent } from './project/member/member.component';
import { AuditLogComponent } from './log/audit-log.component';
@ -26,6 +27,7 @@ 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';
const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
@ -39,6 +41,10 @@ const harborRoutes: Routes = [
authResolver: BaseRoutingResolver
},
children: [
{
path: '',
component: SearchStartComponent
},
{
path: 'projects',
component: ProjectComponent
@ -67,6 +73,10 @@ const harborRoutes: Routes = [
}
]
},
{
path: 'tags/:id/:repo',
component: TagRepositoryComponent
},
{
path: 'projects/:id',
component: ProjectDetailComponent,

View File

@ -15,10 +15,10 @@
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
Project name is required.
{{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
Minimum length of project name is 2 characters.
{{'PROJECT.NAME_MINIMUM_LENGTH' | translate}}
</span>
</label>
</div>

View File

@ -9,13 +9,13 @@
<button class="action-item" (click)="onEdit(p)">Edit</button>
<button class="action-item" (click)="onDelete(p)">Delete</button>
</clr-dg-action-overflow>-->
<clr-dg-cell><a [routerLink]="['/harbor', 'projects', p.project_id, 'repository']" >{{p.name}}</a></clr-dg-cell>
<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>
<harbor-action-overflow *ngIf="isSessionValid">
<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>

View File

@ -1,19 +1,44 @@
import { Component, EventEmitter, Output, Input } from '@angular/core';
import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
import { Router } 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';
@Component({
selector: 'list-project',
templateUrl: 'list-project.component.html'
})
export class ListProjectComponent {
export class ListProjectComponent implements OnInit {
@Input() projects: Project[];
@Output() toggle = new EventEmitter<Project>();
@Output() delete = new EventEmitter<Project>();
private currentUser: SessionUser = null;
constructor(
private session: SessionService,
private router: Router,
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void {
this.currentUser = this.session.getCurrentUser();
}
public get isSessionValid(): boolean {
return this.currentUser != null;
}
goToLink(proId: number): void {
this.router.navigate(['/harbor', 'projects', proId, 'repository']);
this.searchTrigger.closeSearch(false);
}
toggleProject(p: Project) {
this.toggle.emit(p);
}

View File

@ -1,3 +1,4 @@
<a style="display: block;" [routerLink]="['/harbor', 'projects']">&lt; {{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
<h1 class="display-in-line">{{currentProject.name}}</h1>
<nav class="subnav">
<ul class="nav">

View File

@ -34,7 +34,7 @@ import { ProjectRoutingResolver } from './project-routing-resolver.service';
MemberComponent,
AddMemberComponent
],
exports: [ProjectComponent],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService]
})
export class ProjectModule {

View File

@ -1,5 +1,5 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened">
<h3 class="modal-title">New Endpoint</h3>
<h3 class="modal-title">{{modalTitle}}</h3>
<div class="modal-body">
<form #targetForm="ngForm">
<section class="form-block">
@ -11,29 +11,29 @@
</div>
</clr-alert>
<div class="form-group">
<label for="destination_name" class="col-md-4">Destination name<span style="color: red">*</span></label>
<label for="destination_name" class="col-md-4">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required>
<input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
Destination name is required.
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-4">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required>
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
Destination URL is required.
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<label for="destination_username" class="col-md-4">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<label for="destination_password" class="col-md-4">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
@ -45,8 +45,8 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || targetEndpoint.errors">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="createEditDestinationOpened = false">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="!targetForm.form.valid" (click)="onSubmit()">Ok</button>
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || targetEndpoint.errors">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="createEditDestinationOpened = false" [disabled]="testOngoing">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" [disabled]="!targetForm.form.valid" (click)="onSubmit()" [disabled]="testOngoing">{{ 'BUTTON.OK' | translate }}</button>
</div>
</clr-modal>

View File

@ -6,6 +6,7 @@ import { AlertType, ActionType } from '../../shared/shared.const';
import { Target } from '../target';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-edit-destination',
@ -13,6 +14,7 @@ import { Target } from '../target';
})
export class CreateEditDestinationComponent {
modalTitle: string;
createEditDestinationOpened: boolean;
errorMessageOpened: boolean;
@ -30,7 +32,8 @@ export class CreateEditDestinationComponent {
constructor(
private replicationService: ReplicationService,
private messageService: MessageService) {}
private messageService: MessageService,
private translateService: TranslateService) {}
openCreateEditTarget(targetId?: number) {
this.target = new Target();
@ -46,20 +49,22 @@ export class CreateEditDestinationComponent {
if(targetId) {
this.actionType = ActionType.EDIT;
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res=>this.modalTitle=res);
this.replicationService
.getTarget(targetId)
.subscribe(
target=>this.target=target,
error=>this.messageService
.announceMessage(error.status, 'Failed to get target with ID:' + targetId, AlertType.DANGER)
.announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER)
);
} else {
this.actionType = ActionType.ADD_NEW;
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res=>this.modalTitle=res);
}
}
testConnection() {
this.pingTestMessage = 'Testing connection...';
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = true;
this.testOngoing = !this.testOngoing;
this.replicationService
@ -67,12 +72,12 @@ export class CreateEditDestinationComponent {
.subscribe(
response=>{
this.pingStatus = true;
this.pingTestMessage = 'Connection tested successfully.';
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
},
error=>{
this.pingStatus = false;
this.pingTestMessage = 'Failed to ping target.';
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
}
)
@ -94,9 +99,23 @@ export class CreateEditDestinationComponent {
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = 'Failed to add target:' + error;
this.messageService
.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
let errorMessageKey = '';
switch(error.status) {
case 409:
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
break;
case 400:
errorMessageKey = 'DESTINATION.INVALID_NAME';
break;
default:
errorMessageKey = 'UNKNOWN_ERROR';
}
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
this.errorMessage = res;
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
});
}
);
break;
@ -112,8 +131,23 @@ export class CreateEditDestinationComponent {
error=>{
this.errorMessageOpened = true;
this.errorMessage = 'Failed to update target:' + error;
this.messageService
.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
let errorMessageKey = '';
switch(error.status) {
case 409:
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
break;
case 400:
errorMessageKey = 'DESTINATION.INVALID_NAME';
break;
default:
errorMessageKey = 'UNKNOWN_ERROR';
}
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
this.errorMessage = res;
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
});
}
);
break;

View File

@ -2,7 +2,7 @@
<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> Endpoint</button>
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
</div>
<div class="col-xs-4 push-xs-1">
@ -11,20 +11,20 @@
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Destination</clr-dg-column>
<clr-dg-column>Creation Time</clr-dg-column>
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of targets">
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="editTarget(t)">Edit Target</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTarget(t)">Delete</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,17 +1,17 @@
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Operation</clr-dg-column>
<clr-dg-column>Creation time</clr-dg-column>
<clr-dg-column>End time</clr-dg-column>
<clr-dg-column>Logs</clr-dg-column>
<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>
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let j of jobs">
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
<clr-dg-cell></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) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (jobs ? jobs.length : 0) }} {{'REPLICATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

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

View File

@ -2,17 +2,17 @@
<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 Rule</button>
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="col-xs-5 push-xs-1">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentRuleStatus.description}}
{{currentRuleStatus.description | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let r of ruleStatus" (click)="doFilterPolicyStatus(r.key)"> {{r.description}}</a>
<a href="javascript:void(0)" clrDropdownItem *ngFor="let r of ruleStatus" (click)="doFilterPolicyStatus(r.key)"> {{r.description | translate}}</a>
</div>
</clr-dropdown>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)"></grid-filter>
@ -22,10 +22,10 @@
<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 Jobs</span>
<span>{{'REPLICATION.REPLICATION_JOBS' | translate}}</span>
</div>
<div class="col-xs-4">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption]}}</button>
<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>
</div>
@ -33,11 +33,11 @@
<div class="row flex-items-xs-right" [hidden]="currentJobSearchOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentJobStatus.description}}
{{currentJobStatus.description | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let j of jobStatus" (click)="doFilterJobStatus(j.key)"> {{j.description}}</a>
<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">

View File

@ -16,23 +16,23 @@ import { Job } from './job';
import { Target } from './target';
const ruleStatus = [
{ 'key': '', 'description': 'All Status'},
{ 'key': '1', 'description': 'Enabled'},
{ 'key': '0', 'description': 'Disabled'}
{ 'key': '', 'description': 'REPLICATION.ALL_STATUS'},
{ 'key': '1', 'description': 'REPLICATION.ENABLED'},
{ 'key': '0', 'description': 'REPLICATION.DISABLED'}
];
const jobStatus = [
{ 'key': '', 'description': 'All' },
{ 'key': 'pending', 'description': 'Pending' },
{ 'key': 'running', 'description': 'Running' },
{ 'key': 'error', 'description': 'Error' },
{ 'key': 'retrying', 'description': 'Retrying' },
{ 'key': 'stopped' , 'description': 'Stopped' },
{ 'key': 'finished', 'description': 'Finished' },
{ 'key': 'canceled', 'description': 'Canceled' }
{ 'key': '', 'description': 'REPLICATION.ALL' },
{ 'key': 'pending', 'description': 'REPLICATION.PENDING' },
{ 'key': 'running', 'description': 'REPLICATION.RUNNING' },
{ 'key': 'error', 'description': 'REPLICATION.ERROR' },
{ 'key': 'retrying', 'description': 'REPLICATION.RETRYING' },
{ 'key': 'stopped' , 'description': 'REPLICATION.STOPPED' },
{ 'key': 'finished', 'description': 'REPLICATION.FINISHED' },
{ 'key': 'canceled', 'description': 'REPLICATION.CANCELED' }
];
const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
const optionalSearch: {} = {0: 'REPLICATION.ADVANCED', 1: 'REPLICATION.SIMPLE'};
class SearchOption {
policyId: number;

View File

@ -0,0 +1,18 @@
<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-dg-row>
<clr-dg-footer>{{repositories ? repositories.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,17 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Repository } from '../repository';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html'
})
export class ListRepositoryComponent {
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() delete = new EventEmitter<string>();
deleteRepo(repoName: string) {
this.delete.emit(repoName);
}
}

View File

@ -1,9 +0,0 @@
export class Repo {
name: string;
status: string;
tag: string;
author: string;
dockerVersion: string;
created: string;
pullCommand: string;
}

View File

@ -1,41 +1,18 @@
<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">
<div class="row flex-items-xs-right">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-outline-primary" clrDropdownToggle>
My Projects
<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="#/project" clrDropdownItem>My Projects</a>
<a href="#/project" clrDropdownItem>Public Projects</a>
<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>
<div class="col-xs-4 flex-xs-middle">
<input type="text" placeholder="Search for projects">
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Tag</clr-dg-column>
<clr-dg-column>Author</clr-dg-column>
<clr-dg-column>Docker version</clr-dg-column>
<clr-dg-column>Created</clr-dg-column>
<clr-dg-column>Pull Command</clr-dg-column>
<clr-dg-row *ngFor="let r of repos">
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell>{{r.status}}</clr-dg-cell>
<clr-dg-cell>{{r.tag}}</clr-dg-cell>
<clr-dg-cell>{{r.author}}</clr-dg-cell>
<clr-dg-cell>{{r.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{r.created}}</clr-dg-cell>
<clr-dg-cell>{{r.pullCommand}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{repos.length}} item(s)</clr-dg-footer>
</clr-datagrid>
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)"></list-repository>
</div>
</div>

View File

@ -1,18 +1,101 @@
import { Component, OnInit } from '@angular/core';
import { Repo } from './repo';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { RepositoryService } from './repository.service';
import { Repository } from './repository';
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';
const repositoryTypes = [
{ key: '0', description: 'REPOSITORY.MY_REPOSITORY' },
{ key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' }
];
@Component({
selector: 'repository',
templateUrl: 'repository.component.html'
})
export class RepositoryComponent implements OnInit {
repos: Repo[];
changedRepositories: Repository[];
projectId: number;
repositoryTypes = repositoryTypes;
currentRepositoryType: {};
lastFilteredRepoName: string;
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private repositoryService: RepositoryService,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService
) {
this.subscription = this.deletionDialogService
.deletionConfirm$
.subscribe(
message=>{
let repoName = message.data;
this.repositoryService
.deleteRepository(repoName)
.subscribe(
response=>{
this.refresh();
console.log('Successful deleted repo:' + repoName);
},
error=>this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)
);
}
);
}
ngOnInit(): void {
this.repos = [
{ name: 'ubuntu', status: 'ready', tag: '14.04', author: 'Admin', dockerVersion: '1.10.1', created: '2016-10-10', pullCommand: 'docker pull 10.117.5.61/project01/ubuntu:14.04' },
{ name: 'mysql', status: 'ready', tag: '5.6', author: 'docker', dockerVersion: '1.11.2', created: '2016-09-23', pullCommand: 'docker pull 10.117.5.61/project01/mysql:5.6' },
{ name: 'photon', status: 'ready', tag: 'latest', author: 'Admin', dockerVersion: '1.10.1', created: '2016-11-10', pullCommand: 'docker pull 10.117.5.61/project01/photon:latest' },
];
this.projectId = this.route.snapshot.parent.params['id'];
this.currentRepositoryType = this.repositoryTypes[0];
this.lastFilteredRepoName = '';
this.retrieve(this.lastFilteredRepoName);
}
ngOnDestroy(): void {
if(this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(repoName: string) {
this.repositoryService
.listRepositories(this.projectId, repoName)
.subscribe(
response=>this.changedRepositories=response,
error=>this.messageService.announceMessage(error.status, 'Failed to list repositories.', AlertType.DANGER)
);
}
doFilterRepositoryByType(type: string) {
this.currentRepositoryType = this.repositoryTypes.find(r=>r.key == type);
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.retrieve(this.lastFilteredRepoName);
}
deleteRepo(repoName: string) {
let message = new DeletionMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName, repoName, DeletionTargets.REPOSITORY);
this.deletionDialogService.openComfirmDialog(message);
}
refresh() {
this.retrieve('');
}
}

View File

@ -1,10 +1,25 @@
import { NgModule } from '@angular/core';
import { RepositoryComponent } from './repository.component';
import { RouterModule } from '@angular/router';
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 { RepositoryService } from './repository.service';
@NgModule({
imports: [ SharedModule ],
declarations: [ RepositoryComponent ],
exports: [ RepositoryComponent ]
imports: [
SharedModule,
RouterModule
],
declarations: [
RepositoryComponent,
ListRepositoryComponent,
TagRepositoryComponent
],
exports: [ RepositoryComponent ],
providers: [ RepositoryService ]
})
export class RepositoryModule {}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Repository } from './repository';
import { Observable } from 'rxjs/Observable'
@Injectable()
export class RepositoryService {
constructor(private http: Http){}
listRepositories(projectId: number, repoName: string): Observable<Repository[]> {
console.log('List repositories with project ID:' + projectId);
return this.http
.get(`/api/repositories?project_id=${projectId}&q=${repoName}&detail=1`)
.map(response=>response.json() as Repository[])
.catch(error=>Observable.throw(error));
}
deleteRepository(repoName: string): Observable<any> {
console.log('Delete repository with repo name:' + repoName);
return this.http
.delete(`/api/repositories?repo_name=${repoName}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}

View File

@ -1,5 +1,32 @@
export class Repository {
name:string;
version:string;
count:number;
/*
{
"id": "2",
"name": "library/mysql",
"owner_id": 1,
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1,
"creation_time": "2017-02-14T09:22:58Z",
"update_time": "0001-01-01T00:00:00Z"
}
*/
export class Repository {
id: number;
name: string;
owner_id: number;
project_id: number;
description: string;
pull_count: number;
start_count: number;
tags_count: number;
creation_time: Date;
update_time: Date;
constructor(name: string, tags_count: number) {
this.name = name;
this.tags_count = tags_count;
}
}

View File

@ -0,0 +1,24 @@
<a [routerLink]="['/harbor', 'projects', projectId, 'repository']">&lt; {{'REPOSITORY.REPOSITORIES' | translate}}</a>
<h2>{{repoName}} <span class="badge">{{tags ? tags.length : 0}}</span></h2>
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VERIFIED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<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>
<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>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'tag-repository',
templateUrl: 'tag-repository.component.html'
})
export class TagRepositoryComponent implements OnInit {
projectId: number;
repoName: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
}
deleteTag(tagName: string) {
}
}

View File

@ -1,5 +1,5 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened">
<h3 class="modal-title">New Replication Rule</h3>
<h3 class="modal-title">{{modalTitle}}</h3>
<div class="modal-body">
<form #policyForm="ngForm">
<section class="form-block">
@ -11,27 +11,27 @@
</div>
</clr-alert>
<div class="form-group">
<label for="policy_name" class="col-md-4">Name<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-4">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required>
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
Name is required
{{'REPLICATION.NAME_IS_REQUIRED'}}
</span>
</label>
</div>
<div class="form-group">
<label for="policy_description" class="col-md-4">Description</label>
<label for="policy_description" class="col-md-4">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel">
</div>
<div class="form-group">
<label class="col-md-4">Enable</label>
<label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel">
<label for="policy_enable"></label>
</div>
</div>
<div class="form-group">
<label for="destination_name" class="col-md-4">Destination name<span style="color: red">*</span></label>
<label for="destination_name" class="col-md-4">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
<div class="select" *ngIf="!isCreateDestination">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing">
<option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
@ -40,29 +40,29 @@
<label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
Destination name is required.
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span>
</label>
<div class="checkbox-inline">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing">
<label for="check_new">New destination</label>
<label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-4">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
Destination URL is required.
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<label for="destination_username" class="col-md-4">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<label for="destination_password" class="col-md-4">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
@ -74,8 +74,8 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="createEditPolicyOpened = false">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="!policyForm.form.valid" (click)="onSubmit()">Ok</button>
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing">{{'REPLICATION.TEST_CONNECTION' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="createEditPolicyOpened = false">{{'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" [disabled]="!policyForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -9,12 +9,15 @@ import { AlertType, ActionType } from '../../shared/shared.const';
import { Policy } from '../../replication/policy';
import { Target } from '../../replication/target';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-edit-policy',
templateUrl: 'create-edit-policy.component.html'
})
export class CreateEditPolicyComponent implements OnInit {
modalTitle: string;
createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
@ -34,8 +37,10 @@ export class CreateEditPolicyComponent implements OnInit {
testOngoing: boolean;
pingStatus: boolean;
constructor(private replicationService: ReplicationService,
private messageService: MessageService) {}
constructor(
private replicationService: ReplicationService,
private messageService: MessageService,
private translateService: TranslateService) {}
prepareTargets(targetId?: number) {
this.replicationService
@ -72,6 +77,7 @@ export class CreateEditPolicyComponent implements OnInit {
if(policyId) {
this.actionType = ActionType.EDIT;
this.translateService.get('REPLICATION.EDIT_POLICY').subscribe(res=>this.modalTitle=res);
this.replicationService
.getPolicy(policyId)
.subscribe(
@ -85,6 +91,7 @@ export class CreateEditPolicyComponent implements OnInit {
)
} else {
this.actionType = ActionType.ADD_NEW;
this.translateService.get('REPLICATION.ADD_POLICY').subscribe(res=>this.modalTitle=res);
this.prepareTargets();
}
}
@ -204,7 +211,7 @@ export class CreateEditPolicyComponent implements OnInit {
testConnection() {
this.pingStatus = true;
this.pingTestMessage = 'Testing connection...';
this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
let pingTarget = new Target();
pingTarget.endpoint = this.createEditPolicy.endpointUrl;
@ -215,12 +222,12 @@ export class CreateEditPolicyComponent implements OnInit {
.subscribe(
response=>{
this.testOngoing = !this.testOngoing;
this.pingTestMessage = 'Connection tested successfully.';
this.translateService.get('REPLICATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = true;
},
error=>{
this.testOngoing = !this.testOngoing;
this.pingTestMessage = 'Failed to ping target.';
this.translateService.get('REPLICATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = false;
}
);

View File

@ -8,6 +8,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{'BUTTON.DELETE' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{'BUTTON.CONFIRM' | translate}}</button>
</div>
</clr-modal>

View File

@ -1,10 +1,10 @@
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column *ngIf="projectless">Project</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Destination</clr-dg-column>
<clr-dg-column>Last Start Time</clr-dg-column>
<clr-dg-column>Activation</clr-dg-column>
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of policies;let i = index;" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
@ -12,13 +12,13 @@
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>{{p.start_time}}</clr-dg-cell>
<clr-dg-cell>
{{p.enabled === 1 ? 'Enabled' : 'Disabled'}}
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="editPolicy(p)">Edit Policy</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="enablePolicy(p)">{{ p.enabled === 0 ? 'Enable' : 'Disabled' }}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deletePolicy(p)">Delete</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="enablePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (policies ? policies.length : 0) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (policies ? policies.length : 0) }} {{'REPLICATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -14,7 +14,7 @@ export const httpStatusCode = {
"Forbidden": 403
};
export const enum DeletionTargets {
EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET
EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET, REPOSITORY
};
export const harborRootRoute = "/harbor";

View File

@ -7,13 +7,13 @@
"TITLE": "Sign Up"
},
"BUTTON": {
"CANCEL": "Cancel",
"OK": "Ok",
"CANCEL": "CANCEL",
"OK": "OK",
"DELETE": "DELETE",
"LOG_IN": "LOG IN",
"SIGN_UP_LINK": "Sign up for an account",
"SIGN_UP": "SIGN UP",
"CONFIRM": "Confirm",
"CONFIRM": "CONFIRM",
"SEND": "SEND",
"SAVE": "SAVE",
"TEST_MAIL": "TEST MAIL SERVER",
@ -113,6 +113,8 @@
"MY_PROJECTS": "My Projects",
"PUBLIC_PROJECTS": "Public Projects",
"NEW_PROJECT": "New Project",
"NAME_IS_REQUIRED": "Project name is required.",
"NAME_MINIMUM_LENGTH": "Project name is too short, it should be greater than 2 characters.",
"NAME_ALREADY_EXISTS": "Project name already exists.",
"NAME_IS_ILLEGAL": "Project name is illegal.",
"UNKNOWN_ERROR": "Unknown error occurred while creating project.",
@ -125,7 +127,8 @@
"REPOSITORIES": "Repositories",
"REPLICATION": "Replication",
"USERS": "Users",
"LOGS": "Logs"
"LOGS": "Logs",
"PROJECTS": "Projects"
},
"MEMBER": {
"NEW_MEMBER": "New Member",
@ -162,13 +165,105 @@
"FILTER_PLACEHOLDER": "Filter Logs"
},
"REPLICATION": {
"REPLICATION_RULES": "Replication Rules",
"ENDPOINTS": "Endpoints",
"FILTER_POLICIES_PLACEHOLDER": "Filter Policies",
"FILTER_JOBS_PLACEHOLDER": "Filter Jobs",
"DELETION_TITLE": "Confirm Policy Deletion",
"DELETION_SUMMARY": "Do you want to delete policy {{param}}?",
"FILTER_TARGETS_PLACEHOLDER": "Filter Targets",
"DELETION_TITLE_TARGET": "Confirm Target Deletion",
"DELETION_SUMMARY_TARGET": "Do you want to delete target {{param}}?"
"DELETION_SUMMARY_TARGET": "Do you want to delete target {{param}}?",
"ADD_POLICY": "Add Policy",
"EDIT_POLICY": "Edit Policy",
"DELETE_POLICY": "Delete Policy",
"TEST_CONNECTION": "Test Connection",
"TESTING_CONNECTION": "Testing Connection...",
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping target.",
"NAME": "Name",
"PROJECT": "Project",
"NAME_IS_REQUIRED": "Name is required.",
"DESCRIPTION": "Description",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DESTINATION_NAME": "Destination Name",
"DESTINATION_NAME_IS_REQUIRED": "Destination name is required.",
"NEW_DESTINATION": "New Destination",
"DESTINATION_URL": "Endpoint URL",
"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",
"LAST_START_TIME": "Last Start Time",
"ACTIVATION": "Activation",
"REPLICATION_JOBS": "Replication Jobs",
"ALL": "All",
"PENDING": "Pending",
"RUNNING": "Running",
"ERROR": "Error",
"RETRYING": "Retrying",
"STOPPED": "Stopped",
"FINISHED": "Finished",
"CANCELED": "Canceled",
"SIMPLE": "Simple",
"ADVANCED": "Advanced",
"STATUS": "Status",
"OPERATION": "Operation",
"CREATION_TIME": "Start Time",
"END_TIME": "End Time",
"LOGS": "Logs",
"ITEMS": "item(s)"
},
"DESTINATION": {
"ENDPOINT": "Endpoint",
"NAME": "Destination Name",
"NAME_IS_REQUIRED": "Destination name is required.",
"URL": "Endpoint URL",
"URL_IS_REQUIRED": "Endpoint URL is required.",
"USERNAME": "Username",
"PASSWORD": "Password",
"TEST_CONNECTION": "Test Connection",
"TITLE_EDIT": "Edit Endpoint",
"TITLE_ADD": "Create Endpoint",
"DELETE": "Delete Endpoint",
"TESTING_CONNECTION": "Testing Connection...",
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping target.",
"CONFLICT_NAME": "Name or endpoint URL already exists.",
"INVALID_NAME": "Invalid destination name.",
"FAILED_TO_GET_TARGET": "Failed to get endpoint.",
"CREATION_TIME": "Creation Time",
"ITEMS": "item(s)"
},
"REPOSITORY": {
"COPY_ID": "Copy ID",
"COPY_PARENT_ID": "Copy Parent ID",
"DELETE": "Delete",
"NAME": "Name",
"TAGS_COUNT": "Tags",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Pull Command",
"MY_REPOSITORY": "My Repository",
"PUBLIC_REPOSITORY": "Public Repository",
"DELETION_TITLE_REPO": "Confirm Repository Deletion",
"DELETION_SUMMARY_REPO": "Do you want to delete repository {{param}}?",
"DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"FILTER_FOR_REPOSITORIES": "Filter for repositories",
"TAG": "Tag",
"VERIFIED": "Verified",
"AUTHOR": "Author",
"CREATED": "Creation Time",
"DOCKER_VERSION": "Docker Version",
"ARCHITECTURE": "Architecture",
"OS": "OS",
"SHOW_DETAILS": "Show Details",
"REPOSITORIES": "Repositories",
"ITEMS": "item(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Form value changed, confirm to cancel?"

View File

@ -113,6 +113,8 @@
"MY_PROJECTS": "我的项目",
"PUBLIC_PROJECTS": "公开项目",
"NEW_PROJECT": "新建项目",
"NAME_IS_REQUIRED": "项目名称为必填项",
"NAME_MINIMUM_LENGTH": "项目名称长度过短至少多于2个字符。",
"NAME_ALREADY_EXISTS": "项目名称已存在。",
"NAME_IS_ILLEGAL": "项目名称非法。",
"UNKNOWN_ERROR": "创建项目时发生未知错误。",
@ -125,7 +127,8 @@
"REPOSITORIES": "镜像仓库",
"REPLICATION": "复制",
"USERS": "用户",
"LOGS": "日志"
"LOGS": "日志",
"PROJECTS": "项目"
},
"MEMBER": {
"NEW_MEMBER": "新增成员",
@ -163,14 +166,105 @@
"FILTER_PLACEHOLDER": "过滤日志"
},
"REPLICATION": {
"REPLICATION_RULES": "复制",
"ENDPOINTS": "目标",
"FILTER_POLICIES_PLACEHOLDER": "过滤策略",
"FILTER_JOBS_PLACEHOLDER": "过滤任务",
"DELETION_TITLE": "删除策略确认",
"DELETION_SUMMARY": "确认删除策略 {{param}}?",
"FILTER_TARGETS_PLACEHOLDER": "过滤目标",
"DELETION_TITLE_TARGET": "删除目标确认",
"DELETION_SUMMARY_TARGET": "确认删除目标 {{param}}?"
"DELETION_SUMMARY_TARGET": "确认删除目标 {{param}}?",
"ADD_POLICY": "新增策略",
"EDIT_POLICY": "修改策略",
"DELETE_POLICY": "删除策略",
"TEST_CONNECTION": "测试连接",
"TESTING_CONNECTION": "正在测试连接...",
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
"TEST_CONNECTION_FAILURE": "测试连接失败。",
"NAME": "名称",
"PROJECT": "项目",
"NAME_IS_REQUIRED": "名称为必填项",
"DESCRIPTION": "描述",
"ENABLE": "启用",
"DISABLE": "停用",
"DESTINATION_NAME": "目标名",
"DESTINATION_NAME_IS_REQUIRED": "目标名称为必填项。",
"NEW_DESTINATION": "创建目标",
"DESTINATION_URL": "目标URL",
"DESTINATION_URL_IS_REQUIRED": "目标URL为必填项。",
"DESTINATION_USERNAME": "用户名",
"DESTINATION_PASSWORD": "密码",
"REPLICATION_RULE": "创建策略",
"ALL_STATUS": "所有状态",
"ENABLED": "启用",
"DISABLED": "停用",
"LAST_START_TIME": "上次起始时间",
"ACTIVATION": "活动状态",
"REPLICATION_JOBS": "复制任务",
"ALL": "全部",
"PENDING": "挂起",
"RUNNING": "运行中",
"ERROR": "错误",
"RETRYING": "重试中",
"STOPPED": "已停止",
"FINISHED": "已完成",
"CANCELED": "已取消",
"SIMPLE": "简单检索",
"ADVANCED": "高级检索",
"STATUS": "状态",
"OPERATION": "操作",
"CREATION_TIME": "创建时间",
"END_TIME": "结束时间",
"LOGS": "日志",
"ITEMS": "条记录"
},
"DESTINATION": {
"ENDPOINT": "目标",
"NAME": "目标名",
"NAME_IS_REQUIRED": "目标名为必填项。",
"URL": "目标URL",
"URL_IS_REQUIRED": "目标URL为必填项。",
"USERNAME": "用户名",
"PASSWORD": "密码",
"TEST_CONNECTION": "测试连接",
"TITLE_EDIT": "编辑目标",
"TITLE_ADD": "新建目标",
"DELETE": "删除目标",
"TESTING_CONNECTION": "正在测试连接...",
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
"TEST_CONNECTION_FAILURE": "测试连接失败。",
"CONFLICT_NAME": "目标名或目标URL已存在。",
"INVALID_NAME": "无效的目标名称。",
"FAILED_TO_GET_TARGET": "获取目标失败。",
"CREATION_TIME": "创建时间",
"ITEMS": "条记录"
},
"REPOSITORY": {
"COPY_ID": "复制ID",
"COPY_PARENT_ID": "复制父级ID",
"DELETE": "删除",
"NAME": "名称",
"TAGS_COUNT": "标签数",
"PULL_COUNT": "下载数",
"PULL_COMMAND": "Pull命令",
"MY_REPOSITORY": "我的镜像",
"PUBLIC_REPOSITORY": "公共镜像",
"DELETION_TITLE_REPO": "删除镜像仓库确认",
"DELETION_SUMMARY_REPO": "确认删除镜像仓库 {{param}}?",
"DELETION_TITLE_TAG": "删除镜像标签确认",
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签",
"VERIFIED": "已验证",
"AUTHOR": "作者",
"CREATED": "创建时间",
"DOCKER_VERSION": "Docker版本",
"ARCHITECTURE": "架构",
"OS": "操作系统",
"SHOW_DETAILS": "显示详细",
"REPOSITORIES": "镜像仓库",
"ITEMS": "条记录"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认取消?"