mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
merge latest ui_ng code in
This commit is contained in:
parent
2006466814
commit
e115f8b577
@ -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 {
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -37,4 +37,14 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.grid-header-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.grid-filter {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
margin: 0px auto 0px auto;
|
||||
}
|
@ -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>
|
@ -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;
|
||||
@ -42,39 +68,54 @@ export class SearchResultComponent {
|
||||
mouseAction(over: boolean): void {
|
||||
this.mouseOn = over;
|
||||
}
|
||||
|
||||
|
||||
//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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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[];
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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()'>
|
||||
|
@ -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,21 +111,30 @@ 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) {
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
searchClose(): void {
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
|
||||
//Close serch result panel if existing
|
||||
|
@ -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"
|
||||
}
|
@ -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;">
|
||||
|
@ -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']);
|
||||
|
@ -1,4 +0,0 @@
|
||||
//Define a object to store the search event
|
||||
export class SearchEvent {
|
||||
term: string;
|
||||
}
|
@ -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>
|
@ -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 }
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -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 {}
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -1,27 +1,27 @@
|
||||
<clr-datagrid>
|
||||
<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>
|
||||
<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-cell><a [routerLink]="['/harbor', 'projects', p.project_id, 'repository']" >{{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>
|
||||
<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>
|
||||
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</a>
|
||||
</harbor-action-overflow>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}}</clr-dg-footer>
|
||||
<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">
|
||||
<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>
|
||||
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</a>
|
||||
</harbor-action-overflow>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
@ -1,25 +1,50 @@
|
||||
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[];
|
||||
@Input() projects: Project[];
|
||||
|
||||
@Output() toggle = new EventEmitter<Project>();
|
||||
@Output() delete = new EventEmitter<Project>();
|
||||
@Output() toggle = new EventEmitter<Project>();
|
||||
@Output() delete = new EventEmitter<Project>();
|
||||
|
||||
toggleProject(p: Project) {
|
||||
this.toggle.emit(p);
|
||||
}
|
||||
private currentUser: SessionUser = null;
|
||||
|
||||
deleteProject(p: Project) {
|
||||
this.delete.emit(p);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
deleteProject(p: Project) {
|
||||
this.delete.emit(p);
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
<a style="display: block;" [routerLink]="['/harbor', 'projects']">< {{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
|
||||
<h1 class="display-in-line">{{currentProject.name}}</h1>
|
||||
<nav class="subnav">
|
||||
<ul class="nav">
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export class Repo {
|
||||
name: string;
|
||||
status: string;
|
||||
tag: string;
|
||||
author: string;
|
||||
dockerVersion: string;
|
||||
created: string;
|
||||
pullCommand: string;
|
||||
}
|
@ -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">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'">
|
||||
<button class="btn btn-sm btn-outline-primary" clrDropdownToggle>
|
||||
My Projects
|
||||
<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>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
|
||||
</div>
|
||||
<div class="col-xs-4 flex-xs-middle">
|
||||
<input type="text" placeholder="Search for projects">
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
@ -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('');
|
||||
}
|
||||
}
|
@ -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 {}
|
27
src/ui_ng/src/app/repository/repository.service.ts
Normal file
27
src/ui_ng/src/app/repository/repository.service.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -1,5 +1,32 @@
|
||||
/*
|
||||
{
|
||||
"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 {
|
||||
name:string;
|
||||
version:string;
|
||||
count:number;
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<a [routerLink]="['/harbor', 'projects', projectId, 'repository']">< {{'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>
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
@ -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>
|
@ -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";
|
||||
|
||||
|
@ -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?"
|
||||
|
@ -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": "表单内容改变,确认取消?"
|
||||
|
Loading…
Reference in New Issue
Block a user