Merge pull request #3338 from pengpengshui/master

fix issue about pull command in repository  #3275
This commit is contained in:
Steven Zou 2017-10-18 21:28:53 -05:00 committed by GitHub
commit a5ca531819
21 changed files with 174 additions and 47 deletions

View File

@ -8,7 +8,8 @@
"outDir": "dist",
"assets": [
"images",
"favicon.ico"
"favicon.ico",
"setting.json"
],
"index": "index.html",
"main": "main.ts",

View File

@ -1,4 +1,4 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { COPY_INPUT_HTML } from './copy-input.html';
import { PUSH_IMAGE_STYLE } from './push-image.css';
@ -14,16 +14,19 @@ export const enum CopyStatus {
providers: []
})
export class CopyInputComponent {
export class CopyInputComponent implements OnInit {
@Input() inputSize: number = 40;
@Input() headerTitle: string = "Copy Input";
@Input() defaultValue: string = "N/A";
@Input() iconMode: boolean = false;
state: CopyStatus = CopyStatus.NORMAL;
@Output() onCopySuccess: EventEmitter<any> = new EventEmitter<any>();
@Output() onCopyError: EventEmitter<any> = new EventEmitter<any>();
ngOnInit(): void { }
onSuccess($event: any): void {
this.state = CopyStatus.SUCCESS;
this.onCopySuccess.emit($event);
@ -38,6 +41,10 @@ export class CopyInputComponent {
this.state = CopyStatus.NORMAL;
}
setPullCommendShow(): void {
this.iconMode = false;
}
public get isCopied(): boolean {
return this.state === CopyStatus.SUCCESS;
}

View File

@ -1,15 +1,19 @@
export const COPY_INPUT_HTML: string = `
<div>
<div class="command-title">
<div class="command-title" *ngIf="!iconMode">
{{headerTitle}}
</div>
<div>
<span>
<span [class.hide]="iconMode">
<input type="text" class="command-input" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget readonly/>
</span>
<span>
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>
<input type="text" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget1 style="width: 1px; min-width: 0px; padding: 0;">
</span>
<span>
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget1" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>
</span>
</div>
</div>
`;

View File

@ -43,4 +43,7 @@ export const PUSH_IMAGE_STYLE: string = `
.btn-font {
font-size: 14px !important;
}
.hide{
display:none;
}
`;

View File

@ -15,12 +15,12 @@ export const TAG_TEMPLATE = `
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<clr-dg-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 160px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 130px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
@ -30,13 +30,15 @@ export const TAG_TEMPLATE = `
<button class="action-item" *ngIf="hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell style="width: 80px;" [ngSwitch]="existObservablePackage(t)">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
<clr-dg-cell class="truncated" style="min-width: 160px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span>
</clr-dg-cell>
<clr-dg-cell style="width: 80px;">{{t.size}}</clr-dg-cell>
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;" *ngIf="withClair">
<clr-dg-cell style="width: 90px;">{{t.size}}</clr-dg-cell>
<clr-dg-cell style="min-width: 120px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell style="width: 140px;" *ngIf="withClair">
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
@ -47,7 +49,7 @@ export const TAG_TEMPLATE = `
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</clr-dg-cell>
<clr-dg-cell style="width: 100px;">{{t.author}}</clr-dg-cell>
<clr-dg-cell class="truncated" style="width: 130px;" title="{{t.author}}">{{t.author}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
</clr-dg-row>

View File

@ -18,6 +18,7 @@ import { Observable, Subscription } from 'rxjs/Rx';
import { ChannelService } from '../channel/index';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import {CopyInputComponent} from "../push-image/copy-input.component";
describe('TagComponent (inline template)', () => {
@ -53,7 +54,8 @@ describe('TagComponent (inline template)', () => {
ConfirmationDialogComponent,
VULNERABILITY_DIRECTIVES,
FILTER_DIRECTIVES,
JobLogViewerComponent
JobLogViewerComponent,
CopyInputComponent
],
providers: [
ErrorHandler,

View File

@ -50,6 +50,7 @@ import {
import { TranslateService } from '@ngx-translate/core';
import { State, Comparator } from 'clarity-angular';
import {CopyInputComponent} from "../push-image/copy-input.component";
@Component({
selector: 'hbr-tag',
@ -91,6 +92,8 @@ export class TagComponent implements OnInit {
confirmationDialog: ConfirmationDialogComponent;
@ViewChild('digestTarget') textInput: ElementRef;
@ViewChild('copyInput') copyInput: CopyInputComponent;
constructor(
private errorHandler: ErrorHandler,
@ -253,7 +256,7 @@ export class TagComponent implements OnInit {
}
}
//Get vulnerability scanning status
//Get vulnerability scanning status
scanStatus(t: Tag): string {
if (t && t.scan_overview && t.scan_overview.scan_status) {
return t.scan_overview.scan_status;
@ -285,4 +288,9 @@ export class TagComponent implements OnInit {
this.channel.publishScanEvent(this.repoName + "/" + tagId);
}
}
//pull command
onCpError($event: any): void {
this.copyInput.setPullCommendShow();
}
}

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8",
"core-js": "^2.4.1",
"harbor-ui": "0.4.72",
"harbor-ui": "0.4.83",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -24,7 +24,7 @@
flex-direction: column;
height: auto;
position: relative;
margin-left: 1px;
border-left: 1px solid #eee;
}
.login-wrapper-override {

View File

@ -1,4 +1,4 @@
<div class="login-wrapper login-wrapper-override">
<div class="login-wrapper login-wrapper-override" [ngStyle]="{'background-image': 'url(' + customLoginBgImg + ')'}">
<form #signInForm="ngForm" class="login">
<label class="title">{{appTitle | translate}}<span class="trademark tm-font">&#8482;</span>
</label>
@ -12,7 +12,7 @@
{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}
</span>
</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<label for="username" aria-haspopup="true" role="tpopular-repo-wrapperooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<input class="password" type="password" required
[(ngModel)]="signInCredential.password"
name="login_password" id="login_password" placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}'

View File

@ -28,6 +28,7 @@ import { AppConfig } from '../../app-config';
import { User } from '../../user/user';
import { CookieService, CookieOptions } from 'ngx-cookie';
import {SkinableConfig} from "../../skinable-config.service";
//Define status flags for signing in states
export const signInStatusNormal = 0;
@ -48,6 +49,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
//Remeber me indicator
rememberMe: boolean = false;
rememberedName: string = "";
customLoginBgImg: string;
//Form reference
signInForm: NgForm;
@ViewChild('signInForm') currentForm: NgForm;
@ -68,10 +71,16 @@ export class SignInComponent implements AfterViewChecked, OnInit {
private session: SessionService,
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private cookie: CookieService
) { }
private cookie: CookieService,
private skinableConfig: SkinableConfig) { }
ngOnInit(): void {
// custom skin
let customSkinObj = this.skinableConfig.getSkinConfig();
if (customSkinObj && customSkinObj.loginBgImg) {
this.customLoginBgImg = customSkinObj.loginBgImg;
}
//Make sure the updated configuration can be loaded
this.appConfigService.load()
.then(updatedConfig => this.appConfig = updatedConfig);

View File

@ -23,9 +23,13 @@ import { ConfigurationModule } from './config/config.module';
import { TranslateService } from "@ngx-translate/core";
import { AppConfigService } from './app-config.service';
import {SkinableConfig} from "./skinable-config.service";
export function initConfig(configService: AppConfigService) {
return () => configService.load();
export function initConfig(configService: AppConfigService, skinableService: SkinableConfig) {
return () => {
skinableService.getCustomFile();
configService.load();
}
}
export function getCurrentLanguage(translateService: TranslateService) {
@ -41,14 +45,15 @@ export function getCurrentLanguage(translateService: TranslateService) {
BaseModule,
AccountModule,
HarborRoutingModule,
ConfigurationModule
ConfigurationModule,
],
providers: [
AppConfigService,
SkinableConfig,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [ AppConfigService ],
deps: [ AppConfigService, SkinableConfig],
multi: true
},
{

View File

@ -23,6 +23,8 @@ import { AppConfigService } from '../../app-config.service';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import {TranslateService} from "@ngx-translate/core";
import {SkinableConfig} from "../../skinable-config.service";
const deBounceTime = 500; //ms
@ -43,16 +45,31 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
isResPanelOpened: boolean = false;
searchTerm: string = "";
//Placeholder text
placeholderText: string = "GLOBAL_SEARCH.PLACEHOLDER";
placeholderText: string;
constructor(
private searchTrigger: SearchTriggerService,
private router: Router,
private appConfigService: AppConfigService) { }
private appConfigService: AppConfigService,
private translate: TranslateService,
private skinableConfig: SkinableConfig) {
}
//Implement ngOnIni
ngOnInit(): void {
//custom skin
let customSkinObj = this.skinableConfig.getProjects();
if (customSkinObj && customSkinObj.projectName) {
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.projectName}).subscribe(res => {
//Placeholder text
this.placeholderText = res;
});
}else {
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': 'Harbor'}).subscribe(res => {
//Placeholder text
this.placeholderText = res;
});
}
this.searchSub = this.searchTerms
.debounceTime(deBounceTime)
//.distinctUntilChanged()

View File

@ -1,8 +1,9 @@
<clr-header class="header-5 header">
<clr-header class="header-5 header" [ngStyle]='{"background-color": customStyle?.headerBgColor?customStyle?.headerBgColor:"#004a70" }'>
<div class="branding">
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
<clr-icon shape="vm-bug"></clr-icon>
<span class="title">{{ appTitle | translate}}</span>
<clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon>
<img [attr.src]="customStyle?.headerLogo" *ngIf="customStyle?.headerLogo" style="width: 36px;height: 36px; object-fit: fill;">
<span class="title">{{customProjectName?.projectName? customProjectName?.projectName:(appTitle | translate)}}</span>
</a>
</div>
<div class="header-nav">

View File

@ -26,6 +26,7 @@ import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../share
import { AppConfigService } from '../../app-config.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import {SkinableConfig} from "../../skinable-config.service";
@Component({
selector: 'navigator',
@ -40,6 +41,8 @@ export class NavigatorComponent implements OnInit {
selectedLang: string = enLang;
appTitle: string = 'APP_TITLE.HARBOR';
customStyle: {[key: string]: any};
customProjectName: {[key: string]: any};
constructor(
private session: SessionService,
@ -48,11 +51,20 @@ export class NavigatorComponent implements OnInit {
private cookie: CookieService,
private appConfigService: AppConfigService,
private msgHandler: MessageHandlerService,
private searchTrigger: SearchTriggerService) {
private searchTrigger: SearchTriggerService,
private skinableConfig: SkinableConfig) {
}
ngOnInit(): void {
// custom skin
let customSkinObj = this.skinableConfig.getSkinConfig();
if (customSkinObj) {
if (customSkinObj.projects) {
this.customProjectName = customSkinObj.projects;
}
this.customStyle = customSkinObj;
}
this.selectedLang = this.translate.currentLang;
this.translate.onLangChange.subscribe((langChange: {lang: string}) => {
this.selectedLang = langChange.lang;

View File

@ -1,15 +1,15 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="false">
<h3 class="modal-title margin-left-override">vmware</h3>
<h3 class="modal-title margin-left-override">{{customName?.companyName? customName?.companyName : 'vmware'}}</h3>
<div class="modal-body margin-left-override">
<div>
<h2>Harbor</h2>
<h2>{{customName?.projectName? customName?.projectName : ('APP_TITLE.HARBOR' | translate)}}</h2>
</div>
<div style="height: 12px;"></div>
<div>
<span class="p5 about-version">{{'ABOUT.VERSION' | translate}} {{version}}</span>
</div>
<div style="height: 12px;"></div>
<div>
<div *ngIf="!customIntroduction">
<p class="p5">{{'ABOUT.COPYRIGHT' | translate}} <a href="http://www.vmware.com/go/patents" target="_blank" class="about-text-link">http://www.vmware.com/go/patents</a> {{'ABOUT.COPYRIGHT_SUFIX' | translate}}</p>
<p class="p5">{{'ABOUT.TRADEMARK' | translate}}</p>
<p class="p5">
@ -17,6 +17,9 @@
</p>
<div style="height: 24px;"></div>
</div>
<div *ngIf="customIntroduction">
<p class="p5">{{customIntroduction}}</p>
</div>
</div>
<div class="modal-footer margin-left-override">
<button type="button" class="btn btn-primary" (click)="close()">{{'BUTTON.CLOSE' | translate}}</button>

View File

@ -11,20 +11,39 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import { AppConfigService } from '../../app-config.service';
import {TranslateService} from "@ngx-translate/core";
import {SkinableConfig} from "../../skinable-config.service";
@Component({
selector: 'about-dialog',
templateUrl: "about-dialog.component.html",
styleUrls: ["about-dialog.component.css"]
})
export class AboutDialogComponent {
export class AboutDialogComponent implements OnInit{
opened: boolean = false;
build: string = "4276418";
customIntroduction: string;
customName: {[key: string]: any };
constructor(private appConfigService: AppConfigService) { }
constructor(private appConfigService: AppConfigService,
private translate: TranslateService,
private skinableConfig: SkinableConfig) {
}
ngOnInit(): void {
// custom skin
let customSkinObj = this.skinableConfig.getProjects();
if (customSkinObj) {
let selectedLang = this.translate.currentLang;
this.customName = customSkinObj;
if (customSkinObj.introduction && customSkinObj.introduction[selectedLang]) {
this.customIntroduction = customSkinObj.introduction[selectedLang];
}
}
}
public get version(): string {
let appConfig = this.appConfigService.getConfig();

View File

@ -0,0 +1,34 @@
import {Injectable} from "@angular/core";
import {Http} from "@angular/http";
import {Observable} from "rxjs/Observable";
/**
* Created by pengf on 9/15/2017.
*/
@Injectable()
export class SkinableConfig {
customSkinData: {[key: string]: any};
constructor(private http: Http) {}
public getCustomFile(): Promise<any> {
return this.http.get('../setting.json')
.toPromise()
.then(response => { this.customSkinData = response.json(); return this.customSkinData; })
.catch(error => {
console.error('custom skin json file load failed');
});
}
public getSkinConfig() {
return this.customSkinData;
}
public getProjects() {
if (this.customSkinData) {
return this.customSkinData.projects;
}else {
return null;
}
}
}

View File

@ -86,7 +86,7 @@
"LOGOUT": "Log Out"
},
"GLOBAL_SEARCH": {
"PLACEHOLDER": "Search Harbor...",
"PLACEHOLDER": "Search {{param}}...",
"PLACEHOLDER_VIC": "Search Registry..."
},
"SIDE_NAV": {

View File

@ -86,7 +86,7 @@
"LOGOUT": "Cerrar sesión"
},
"GLOBAL_SEARCH": {
"PLACEHOLDER": "Buscar en Harbor...",
"PLACEHOLDER": "Buscar en {{param}}...",
"PLACEHOLDER_VIC": "Buscar en el registro..."
},
"SIDE_NAV": {

View File

@ -86,7 +86,7 @@
"LOGOUT": "退出"
},
"GLOBAL_SEARCH": {
"PLACEHOLDER": "搜索 Harbor...",
"PLACEHOLDER": "搜索 {{param}}...",
"PLACEHOLDER_VIC": "搜索 Registry..."
},
"SIDE_NAV": {