Merge pull request #2562 from steven-zou/master

Refactor harnor portal with sharable components in harbor-ui lib
This commit is contained in:
Steven Zou 2017-06-21 16:51:46 +08:00 committed by GitHub
commit ed1263148f
127 changed files with 1731 additions and 3552 deletions

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ src/ui_ng/aot/**/*.json
**/*ngfactory.ts **/*ngfactory.ts
**/aot **/aot
**/dist **/dist
**/.bin

View File

@ -79,7 +79,7 @@ script:
- sudo mkdir -p /harbor - sudo mkdir -p /harbor
- sudo mv ./VERSION /harbor/VERSION - sudo mv ./VERSION /harbor/VERSION
- sudo service mysql stop - sudo service mysql stop
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4
- cat ./src/ui_ng/lib/npm-ut-test-results - cat ./src/ui_ng/lib/npm-ut-test-results
- sudo ./tests/testprepare.sh - sudo ./tests/testprepare.sh
- sudo docker-compose -f ./make/docker-compose.test.yml up -d - sudo docker-compose -f ./make/docker-compose.test.yml up -d
@ -100,7 +100,7 @@ script:
- docker-compose -f make/docker-compose.test.yml down - docker-compose -f make/docker-compose.test.yml down
- sudo rm -rf /data/config/* - sudo rm -rf /data/config/*
- ls /data/cert - ls /data/cert
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 NOTARYFLAG=true - sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4 NOTARYFLAG=true
- docker ps - docker ps
- ./tests/notarytest.sh - ./tests/notarytest.sh

View File

@ -50,13 +50,13 @@ You can compile the code by one of the three approaches:
* Build, install and bring up Harbor without Notary: * Build, install and bring up Harbor without Notary:
```sh ```sh
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 $ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4
``` ```
* Build, install and bring up Harbor with Notary: * Build, install and bring up Harbor with Notary:
```sh ```sh
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 NOTARYFLAG=true $ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4 NOTARYFLAG=true
``` ```
#### II. Compile code with your own Golang environment, then build Harbor #### II. Compile code with your own Golang environment, then build Harbor

View File

@ -1,10 +1,9 @@
{ {
"project": { "project": {
"version": "1.1.0", "version": "1.2.0",
"name": "Harbor" "name": "Harbor"
}, },
"apps": [ "apps": [{
{
"root": "src", "root": "src",
"outDir": "dist", "outDir": "dist",
"assets": [ "assets": [
@ -34,8 +33,7 @@
"dev": "environments/environment.ts", "dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts" "prod": "environments/environment.prod.ts"
} }
} }],
],
"addons": [], "addons": [],
"packages": [], "packages": [],
"e2e": { "e2e": {

View File

@ -76,10 +76,12 @@ If **projectId** is set to the id of specified project, then only show the repli
<hbr-endpoint></hbr-endpoint> <hbr-endpoint></hbr-endpoint>
``` ```
* **Repository and Tag Management View[updating]** * **Repository and Tag Management View**
**projectId** is used to specify which projects the repositories are from. **projectId** is used to specify which projects the repositories are from.
**projectName** is used to generate the related commands for pushing images.
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user. **hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property. **hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
@ -87,7 +89,7 @@ If **projectId** is set to the id of specified project, then only show the repli
**tagClickEvent** is an @output event emitter for you to catch the tag click events. **tagClickEvent** is an @output event emitter for you to catch the tag click events.
``` ```
<hbr-repository-stackview [projectId]="..." [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview> <hbr-repository-stackview [projectId]="..." [projectName]="" [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
... ...
@ -98,6 +100,19 @@ watchTagClickEvent(tag: Tag): void {
``` ```
* **Tag detail view**
This view is linked by the repository stack view only when the Clair is enabled in Harbor.
**tagId** is an @Input property and used to specify the tag of which details are displayed.
**repositoryId** is an @Input property and used to specified the repository to which the tag is belonged.
**backEvt** is an @Output event emitter and used to distribute the click event of the back arrow in the detail page.
```
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="..." [repositoryId]="..."></hbr-tag-detail>
```
## Configurations ## Configurations
All the related configurations are defined in the **HarborModuleConfig** interface. All the related configurations are defined in the **HarborModuleConfig** interface.
@ -111,6 +126,7 @@ export const DefaultServiceConfig: IServiceConfig = {
targetBaseEndpoint: "/api/targets", targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication", replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication", replicationJobEndpoint: "/api/jobs/replication",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
enablei18Support: false, enablei18Support: false,
defaultLang: DEFAULT_LANG, //'en-us' defaultLang: DEFAULT_LANG, //'en-us'
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang' langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
@ -147,6 +163,8 @@ It supports partially overriding. For the items not overridden, default values w
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication". * **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang". * **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es']. * **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
@ -215,11 +233,14 @@ HarborLibraryModule.forRoot({
... ...
``` ```
**3. user session(Ongoing/Discussing)** **3. user session**
Some components may need the user authorization and authentication information to display different views. There might be two alternatives to select: Some components may need the user authorization and authentication information to display different views. The following way of handing user session is supported by the library.
* Use @Input properties or interface to let top component or page to pass the required user session information in. * Use @Input properties or interface to let top component or page to pass the required user session information in.
* Component retrieves the required information from some API provided by top component or page when necessary.
```
//In the above repository stack view, the user session informations are passed via @input properties.
[hasSignedIn]="..." [hasProjectAdminRole]="..."
```
**4. services** **4. services**
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces. The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
@ -606,7 +627,7 @@ export class MyScanningResultService extends ScanningResultService {
* *
* @memberOf ScanningResultService * @memberOf ScanningResultService
*/ */
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{ getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
... ...
} }
@ -619,7 +640,22 @@ export class MyScanningResultService extends ScanningResultService {
* *
* @memberOf ScanningResultService * @memberOf ScanningResultService
*/ */
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{ getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
...
}
/**
* Start a new vulnerability scanning
*
* @abstract
* @param {string} repoName
* @param {string} tagId
* @returns {(Observable<any> | Promise<any> | any)}
*
* @memberOf ScanningResultService
*/
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
... ...
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor-ui", "name": "harbor-ui",
"version": "0.1.0", "version": "0.2.0",
"description": "Harbor shared UI components based on Clarity and Angular4", "description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": { "scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
@ -18,25 +18,24 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^4.1.0", "@angular/animations": "~4.1.3",
"@angular/common": "^4.1.0", "@angular/common": "~4.1.3",
"@angular/compiler": "^4.1.0", "@angular/compiler": "~4.1.3",
"@angular/core": "^4.1.0", "@angular/core": "~4.1.3",
"@angular/forms": "^4.1.0", "@angular/forms": "~4.1.3",
"@angular/http": "^4.1.0", "@angular/http": "~4.1.3",
"@angular/platform-browser": "^4.1.0", "@angular/platform-browser": "~4.1.3",
"@angular/platform-browser-dynamic": "^4.1.0", "@angular/platform-browser-dynamic": "~4.1.3",
"@angular/router": "^4.1.0", "@angular/router": "~4.1.3",
"@ngx-translate/core": "^6.0.0", "@ngx-translate/core": "^6.0.0",
"@ngx-translate/http-loader": "0.0.3", "@ngx-translate/http-loader": "0.0.3",
"@webcomponents/custom-elements": "1.0.0-alpha.3", "@webcomponents/custom-elements": "1.0.0-alpha.3",
"clarity-angular": "^0.9.7", "clarity-angular": "~0.9.8",
"clarity-icons": "^0.9.7", "clarity-icons": "~0.9.8",
"clarity-ui": "^0.9.7", "clarity-ui": "~0.9.8",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-clipboard": "^8.0.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",
"rxjs": "^5.0.1", "rxjs": "^5.0.1",
"ts-helpers": "^1.1.1", "ts-helpers": "^1.1.1",
@ -45,12 +44,13 @@
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "^1.0.0", "@angular/cli": "^1.0.0",
"@angular/compiler-cli": "^4.0.1", "@angular/compiler-cli": "~4.1.3",
"@types/core-js": "^0.9.41", "@types/core-js": "^0.9.41",
"@types/jasmine": "~2.2.30", "@types/jasmine": "~2.2.30",
"@types/node": "^6.0.42", "@types/node": "^6.0.42",
"bootstrap": "4.0.0-alpha.5", "bootstrap": "4.0.0-alpha.5",
"codelyzer": "~2.0.0-beta.4", "codelyzer": "~2.0.0-beta.4",
"copyfiles": "^1.2.0",
"enhanced-resolve": "^3.0.0", "enhanced-resolve": "^3.0.0",
"jasmine-core": "2.4.1", "jasmine-core": "2.4.1",
"jasmine-spec-reporter": "2.5.0", "jasmine-spec-reporter": "2.5.0",
@ -61,14 +61,14 @@
"karma-phantomjs-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.0",
"karma-remap-istanbul": "^0.2.1", "karma-remap-istanbul": "^0.2.1",
"protractor": "^4.0.9", "protractor": "^4.0.9",
"rimraf": "^2.6.1",
"rollup": "^0.41.6", "rollup": "^0.41.6",
"rollup-plugin-node-resolve": "^3.0.0",
"ts-node": "1.2.1", "ts-node": "1.2.1",
"tslint": "^4.1.1", "tslint": "^4.1.1",
"typescript": "~2.2.0", "typescript": "~2.2.0",
"typings": "^1.4.0", "typings": "^1.4.0",
"uglify-js": "^2.8.22", "uglify-js": "^2.8.22",
"webdriver-manager": "10.2.5", "webdriver-manager": "10.2.5"
"rimraf": "^2.6.1",
"copyfiles": "^1.2.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor-ui", "name": "harbor-ui",
"version": "0.1.42", "version": "0.2.0",
"description": "Harbor shared UI components based on Clarity and Angular4", "description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware", "author": "VMware",
"module": "index.js", "module": "index.js",
@ -28,18 +28,19 @@
"@angular/platform-browser": "^4.0.1", "@angular/platform-browser": "^4.0.1",
"@angular/platform-browser-dynamic": "^4.0.1", "@angular/platform-browser-dynamic": "^4.0.1",
"@angular/router": "^4.0.1", "@angular/router": "^4.0.1",
"@webcomponents/custom-elements": "1.0.0-alpha.3",
"web-animations-js": "^2.2.1",
"clarity-angular": "^0.9.0",
"clarity-icons": "^0.9.0",
"clarity-ui": "^0.9.0",
"core-js": "^2.4.1",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",
"zone.js": "^0.8.4",
"mutationobserver-shim": "^0.3.2",
"@ngx-translate/core": "^6.0.0", "@ngx-translate/core": "^6.0.0",
"@ngx-translate/http-loader": "0.0.3", "@ngx-translate/http-loader": "0.0.3",
"ngx-cookie": "^1.0.0" "@webcomponents/custom-elements": "1.0.0-alpha.3",
"clarity-angular": "^0.9.8",
"clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8",
"core-js": "^2.4.1",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",
"web-animations-js": "^2.2.1",
"zone.js": "^0.8.4"
} }
} }

View File

@ -1,3 +1,5 @@
import resolve from 'rollup-plugin-node-resolve';
export default { export default {
entry: 'dist/index.js', entry: 'dist/index.js',
dest: 'dist/bundles/harborui.umd.js', dest: 'dist/bundles/harborui.umd.js',
@ -11,10 +13,13 @@ export default {
'@angular/forms', '@angular/forms',
'@angular/platform-browser', '@angular/platform-browser',
'@angular/http', '@angular/http',
'@angular/router',
'clarity-angular', 'clarity-angular',
'@ngx-translate/core', '@ngx-translate/core',
'@ngx-translate/http-loader', '@ngx-translate/http-loader',
'ngx-cookie',
'rxjs', 'rxjs',
'rxjs/Rx',
'rxjs/Subject', 'rxjs/Subject',
'rxjs/Observable', 'rxjs/Observable',
'rxjs/add/observable/of', 'rxjs/add/observable/of',
@ -29,8 +34,14 @@ export default {
'@angular/forms': 'ng.forms', '@angular/forms': 'ng.forms',
'@angular/http': 'ng.http', '@angular/http': 'ng.http',
'@angular/platform-browser': 'ng.platformBrowser', '@angular/platform-browser': 'ng.platformBrowser',
'@angular/router': 'ng.router',
'clarity-angular': 'ng.clarity',
'ngx-cookie': 'ngx.cookie',
'@ngx-translate/core': 'ngx.translate',
'@ngx-translate/http-loader': 'ngx.translate',
'rxjs': 'rxjs', 'rxjs': 'rxjs',
'rxjs/Subject': 'rxjs.Subject', 'rxjs/Subject': 'rxjs.Subject',
'rxjs/Rx': 'Rx',
'rxjs/Observable': 'Rx', 'rxjs/Observable': 'Rx',
'rxjs/ReplaySubject': 'Rx', 'rxjs/ReplaySubject': 'Rx',
'rxjs/add/operator/map': 'Rx.Observable.prototype', 'rxjs/add/operator/map': 'Rx.Observable.prototype',
@ -50,5 +61,10 @@ export default {
// console.warn everything else // console.warn everything else
console.warn(warning.message); console.warn(warning.message);
},
plugins: [resolve({
customResolveOptions: {
moduleDirectory: 'node_modules'
} }
})]
} }

View File

@ -1,6 +1,6 @@
export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable"> export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-warning" *ngIf="!editable"> <div class="alert alert-warning" *ngIf="!editable">
<div class="alert-item"> <div class="alert-item">
@ -40,7 +40,6 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div> </div>
</section> </section>
</form> </form>

View File

@ -11,7 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core'; import {
Component,
Output,
EventEmitter,
ViewChild,
AfterViewChecked
} from '@angular/core';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { EndpointService } from '../service/endpoint.service'; import { EndpointService } from '../service/endpoint.service';
@ -41,12 +47,8 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditDestinationOpened: boolean; createEditDestinationOpened: boolean;
editable: boolean; editable: boolean;
testOngoing: boolean; testOngoing: boolean;
pingTestMessage: string;
pingStatus: boolean;
actionType: ActionType; actionType: ActionType;
@ -95,8 +97,6 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
this.endpointHasChanged = false; this.endpointHasChanged = false;
this.targetNameHasChanged = false; this.targetNameHasChanged = false;
this.pingTestMessage = '';
this.pingStatus = true;
this.testOngoing = false; this.testOngoing = false;
if (targetId) { if (targetId) {
@ -121,10 +121,6 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
} }
testConnection() { testConnection() {
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = true;
this.testOngoing = !this.testOngoing;
let payload: Endpoint = this.initEndpoint; let payload: Endpoint = this.initEndpoint;
if (this.endpointHasChanged) { if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint; payload.endpoint = this.target.endpoint;
@ -134,18 +130,17 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
payload.id = this.target.id; payload.id = this.target.id;
} }
this.testOngoing = true;
toPromise<Endpoint>(this.endpointService toPromise<Endpoint>(this.endpointService
.pingEndpoint(payload)) .pingEndpoint(payload))
.then( .then(
response => { response => {
this.pingStatus = true; this.testOngoing = false;
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res); this.inlineAlert.showInlineSuccess({ message: "DESTINATION.TEST_CONNECTION_SUCCESS" });
this.testOngoing = !this.testOngoing;
}).catch( }).catch(
error => { error => {
this.pingStatus = false; this.testOngoing = false;
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res); this.inlineAlert.showInlineError('DESTINATION.TEST_CONNECTION_FAILURE');
this.testOngoing = !this.testOngoing;
}); });
} }
@ -189,7 +184,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
this.translateService this.translateService
.get(errorMessageKey) .get(errorMessageKey)
.subscribe(res => { .subscribe(res => {
this.errorHandler.error(res); this.inlineAlert.showInlineError(res);
}); });
} }
); );
@ -228,7 +223,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
this.translateService this.translateService
.get(errorMessageKey) .get(errorMessageKey)
.subscribe(res => { .subscribe(res => {
this.errorHandler.error(res); this.inlineAlert.showInlineError(res);
}); });
} }
); );

View File

@ -1,7 +1,7 @@
export const CREATE_EDIT_RULE_TEMPLATE: string = ` export const CREATE_EDIT_RULE_TEMPLATE: string = `
<clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable"> <clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body" style="max-height: 85vh;"> <div class="modal-body" style="max-height: 85vh;">
<form #ruleForm="ngForm"> <form #ruleForm="ngForm">
<section class="form-block"> <section class="form-block">

View File

@ -60,6 +60,7 @@ export const DefaultServiceConfig: IServiceConfig = {
targetBaseEndpoint: "/api/targets", targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication", replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication", replicationJobEndpoint: "/api/jobs/replication",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
enablei18Support: false, enablei18Support: false,
defaultLang: DEFAULT_LANG, defaultLang: DEFAULT_LANG,
langCookieKey: DEFAULT_LANG_COOKIE_KEY, langCookieKey: DEFAULT_LANG_COOKIE_KEY,

View File

@ -13,8 +13,9 @@ export class TranslateServiceInitializer {
public init(config: i18nConfig = {}): void { public init(config: i18nConfig = {}): void {
let selectedLang: string = config.defaultLang ? config.defaultLang : DEFAULT_LANG; let selectedLang: string = config.defaultLang ? config.defaultLang : DEFAULT_LANG;
let supportedLangs: string[] = config.supportedLangs ? config.supportedLangs : DEFAULT_SUPPORTING_LANGS;
this.translateService.addLangs(config.supportedLangs ? config.supportedLangs : DEFAULT_SUPPORTING_LANGS); this.translateService.addLangs(supportedLangs);
this.translateService.setDefaultLang(selectedLang); this.translateService.setDefaultLang(selectedLang);
if (config.enablei18Support) { if (config.enablei18Support) {
@ -25,12 +26,14 @@ export class TranslateServiceInitializer {
langSetting = this.translateService.getBrowserCultureLang().toLowerCase(); langSetting = this.translateService.getBrowserCultureLang().toLowerCase();
} }
if (config.supportedLangs && config.supportedLangs.length > 0) { if (langSetting && langSetting.trim() !== "") {
if (config.supportedLangs.find(lang => lang === langSetting)) { if (supportedLangs && supportedLangs.length > 0) {
if (supportedLangs.find(lang => lang === langSetting)) {
selectedLang = langSetting; selectedLang = langSetting;
} }
} }
} }
}
this.translateService.use(selectedLang); this.translateService.use(selectedLang);
} }

View File

@ -14,3 +14,4 @@ export * from './replication/index';
export * from './vulnerability-scanning/index'; export * from './vulnerability-scanning/index';
export * from './i18n/index'; export * from './i18n/index';
export * from './push-image/index'; export * from './push-image/index';
export * from './third-party/index';

View File

@ -22,7 +22,7 @@ import { INLINE_ALERT_STYLE } from './inline-alert.component.css';
import { INLINE_ALERT_TEMPLATE } from './inline-alert.component.html'; import { INLINE_ALERT_TEMPLATE } from './inline-alert.component.html';
@Component({ @Component({
selector: 'inline-alert', selector: 'hbr-inline-alert',
template: INLINE_ALERT_TEMPLATE, template: INLINE_ALERT_TEMPLATE,
styles: [ INLINE_ALERT_STYLE ] styles: [ INLINE_ALERT_STYLE ]
}) })

View File

@ -12,36 +12,26 @@ import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component'; import { FilterComponent } from '../filter/filter.component';
import { click } from '../utils';
describe('RecentLogComponent (inline template)', () => { describe('RecentLogComponent (inline template)', () => {
let component: RecentLogComponent; let component: RecentLogComponent;
let fixture: ComponentFixture<RecentLogComponent>; let fixture: ComponentFixture<RecentLogComponent>;
let serviceConfig: IServiceConfig; let serviceConfig: IServiceConfig;
let logService: AccessLogService; let logService: AccessLogService;
let spy: jasmine.Spy; let spy: jasmine.Spy;
let mockItems: AccessLogItem[] = [{ let mockItems: AccessLogItem[] = [];
log_id: 23,
user_id: 45,
project_id: 11,
repo_name: "myproject/",
repo_tag: "N/A",
operation: "create",
op_time: "2017-04-11T10:26:22Z",
username: "user91"
}, {
log_id: 18,
user_id: 1,
project_id: 5,
repo_name: "demo2/vmware/harbor-ui",
repo_tag: "0.6",
operation: "push",
op_time: "2017-03-09T02:29:59Z",
username: "admin"
}];
let mockData: AccessLog = { let mockData: AccessLog = {
metadata: { metadata: {
xTotalCount: 2 xTotalCount: 18
}, },
data: mockItems data: []
};
let mockData2: AccessLog = {
metadata: {
xTotalCount: 1
},
data: []
}; };
let testConfig: IServiceConfig = { let testConfig: IServiceConfig = {
logBaseEndpoint: "/api/logs/testing" logBaseEndpoint: "/api/logs/testing"
@ -68,8 +58,36 @@ describe('RecentLogComponent (inline template)', () => {
serviceConfig = TestBed.get(SERVICE_CONFIG); serviceConfig = TestBed.get(SERVICE_CONFIG);
logService = fixture.debugElement.injector.get(AccessLogService); logService = fixture.debugElement.injector.get(AccessLogService);
//Mock data
for (let i = 0; i < 18; i++) {
let item: AccessLogItem = {
log_id: 23 + i,
user_id: 45 + i,
project_id: 11 + i,
repo_name: "myproject/demo" + i,
repo_tag: "N/A",
operation: "create",
op_time: "2017-04-11T10:26:22Z",
username: "user91" + i
};
mockItems.push(item);
}
mockData2.data = mockItems.slice(0, 1);
mockData.data = mockItems;
spy = spyOn(logService, 'getRecentLogs') spy = spyOn(logService, 'getRecentLogs')
.and.returnValue(Promise.resolve(mockData)); .and.callFake(function (params: RequestQueryParams) {
if (params && params.get('repository')) {
return Promise.resolve(mockData2);
} else {
if (params.get('page') == '1') {
mockData.data = mockItems.slice(0, 15);
} else {
mockData.data = mockItems.slice(15, 18)
}
return Promise.resolve(mockData);
}
});
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -83,54 +101,17 @@ describe('RecentLogComponent (inline template)', () => {
expect(serviceConfig.logBaseEndpoint).toEqual("/api/logs/testing"); expect(serviceConfig.logBaseEndpoint).toEqual("/api/logs/testing");
}); });
it('should inject and call the AccessLogService', () => { it('should get data from AccessLogService', async(() => {
expect(logService).toBeTruthy(); expect(logService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getRecentLogs called'); expect(spy.calls.any()).toBe(true, 'getRecentLogs called');
});
it('should get data from AccessLogService', async(() => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges(); fixture.detectChanges();
expect(component.recentLogs).toBeTruthy(); expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy(); expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(2); expect(component.recentLogs.length).toEqual(15);
});
}));
it('should support filtering list by keywords', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter('push');
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
let log: AccessLogItem = component.recentLogs[0];
expect(log).toBeTruthy();
expect(log.username).toEqual('admin');
});
}));
it('should support refreshing', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter('push');
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
});
component.refresh();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
}); });
})); }));
@ -143,8 +124,95 @@ describe('RecentLogComponent (inline template)', () => {
expect(de).toBeTruthy(); expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement; let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('user91'); expect(el.textContent.trim()).toEqual('user910');
}); });
})); }));
it('should support pagination', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(4);
});
});
}));
it('should support filtering list by keywords', async(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.search-btn');
expect(el).toBeTruthy("Not found search icon");
click(el);
fixture.detectChanges();
let el2: HTMLInputElement = fixture.nativeElement.querySelector('input');
expect(el2).toBeTruthy("Not found input");
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter("demo0");
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(2);
});
});
}));
it('should support refreshing', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(4)
let refreshEl: HTMLElement = fixture.nativeElement.querySelector(".refresh-btn");
expect(refreshEl).toBeTruthy("Not found refresh button");
refreshEl.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(16);
});
});
});
}));
}); });

View File

@ -41,7 +41,9 @@ export class RecentLogComponent implements OnInit {
@Input() withTitle: boolean = false; @Input() withTitle: boolean = false;
pageSize: number = DEFAULT_PAGE_SIZE; pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 0; currentPage: number = 1;//Double bound to pagination component
currentPagePvt: number = 0; //Used to confirm whether page is changed
currentState: State;
opTimeComparator: Comparator<AccessLogItem> = new CustomComparator<AccessLogItem>('op_time', 'date'); opTimeComparator: Comparator<AccessLogItem> = new CustomComparator<AccessLogItem>('op_time', 'date');
@ -61,28 +63,45 @@ export class RecentLogComponent implements OnInit {
} }
public doFilter(terms: string): void { public doFilter(terms: string): void {
if (terms.trim() === "") { this.currentTerm = terms.trim();
//Clear search results //Trigger data loading and start from first page
this.recentLogs = this.logsCache.data.filter(log => log.username != ""); this.loading = true;
return; this.currentPage = 1;
if (this.currentPagePvt === 1) {
//Force reloading
let st: State = this.currentState;
if (!st) {
st = {
page: {}
};
}
st.page.from = 0;
st.page.to = this.pageSize - 1;
st.page.size = this.pageSize;
this.currentPagePvt = 0;//Reset pvt
this.load(st);
} }
this.currentTerm = terms;
this.recentLogs = this.logsCache.data.filter(log => this.isMatched(terms, log));
} }
public refresh(): void { public refresh(): void {
this.currentTerm = ""; this.doFilter("");
this.currentPage = 0;
this.load({});
} }
load(state: State) { load(state: State) {
//Keep it for future filter
this.currentState = state;
let pageNumber: number = this._calculatePage(state); let pageNumber: number = this._calculatePage(state);
if (pageNumber !== this.currentPage) { if (pageNumber !== this.currentPagePvt) {
//load data //load data
let params: RequestQueryParams = new RequestQueryParams(); let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + pageNumber); params.set("page", '' + pageNumber);
params.set("page_size", '' + this.pageSize); params.set("page_size", '' + this.pageSize);
if (this.currentTerm && this.currentTerm !== "") {
params.set('repository', this.currentTerm);
}
this.loading = true; this.loading = true;
toPromise<AccessLog>(this.logService.getRecentLogs(params)) toPromise<AccessLog>(this.logService.getRecentLogs(params))
@ -96,7 +115,7 @@ export class RecentLogComponent implements OnInit {
//Do customized sorting //Do customized sorting
this._doSorting(state); this._doSorting(state);
this.currentPage = pageNumber; this.currentPagePvt = pageNumber;
this.loading = false; this.loading = false;
}) })
@ -105,6 +124,8 @@ export class RecentLogComponent implements OnInit {
this.errorHandler.error(error); this.errorHandler.error(error);
}); });
} else { } else {
//Column sorting and filtering
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data
//Do customized filter //Do customized filter

View File

@ -33,7 +33,7 @@ export const LOG_TEMPLATE: string = `
<clr-dg-footer> <clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
of {{pagination.totalItems}} {{'AUDIT_LOG.ITEMS' | translate}} of {{pagination.totalItems}} {{'AUDIT_LOG.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination> <clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>
</div> </div>

View File

@ -39,4 +39,8 @@ export const PUSH_IMAGE_STYLE: string = `
min-width: 360px; min-width: 360px;
max-width: 720px; max-width: 720px;
} }
.btn-font {
font-size: 14px !important;
}
`; `;

View File

@ -1,7 +1,7 @@
export const PUSH_IMAGE_HTML: string = ` export const PUSH_IMAGE_HTML: string = `
<div> <div>
<clr-dropdown [clrMenuPosition]="'bottom-right'"> <clr-dropdown [clrMenuPosition]="'bottom-right'">
<button class="btn btn-link" clrDropdownToggle (click)="onclick()"> <button class="btn btn-link btn-font" clrDropdownToggle (click)="onclick()">
{{ 'PUSH_IMAGE.TITLE' | translate | uppercase}} {{ 'PUSH_IMAGE.TITLE' | translate | uppercase}}
<clr-icon shape="caret down"></clr-icon> <clr-icon shape="caret down"></clr-icon>
</button> </button>
@ -10,14 +10,16 @@ export const PUSH_IMAGE_HTML: string = `
<section> <section>
<span><h5 class="h5-override">{{ 'PUSH_IMAGE.TITLE' | translate }}</h5></span> <span><h5 class="h5-override">{{ 'PUSH_IMAGE.TITLE' | translate }}</h5></span>
<span> <span>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right"> <clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'md'">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon> <clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{ 'PUSH_IMAGE.TOOLTIP' | translate }}</span> <clr-tooltip-content>
</a> {{ 'PUSH_IMAGE.TOOLTIP' | translate }}
</clr-tooltip-content>
</clr-tooltip>
</span> </span>
</section> </section>
<section> <section>
<inline-alert #copyAlert></inline-alert> <hbr-inline-alert #copyAlert></hbr-inline-alert>
</section> </section>
<section> <section>
<article class="commands-section"> <article class="commands-section">

View File

@ -9,7 +9,7 @@ export const REPLICATION_STYLE: string = `
.option-left { .option-left {
padding-left: 16px; padding-left: 16px;
margin-top: 12px; margin-top: 18px;
} }
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;

View File

@ -1,13 +1,13 @@
export const REPLICATION_TEMPLATE: string = ` export const REPLICATION_TEMPLATE: string = `
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between" style="height:24px;"> <div class="row flex-items-xs-between" style="height:32px;">
<div class="flex-xs-middle option-left"> <div class="flex-xs-middle option-left">
<button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button> <button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule> <create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
</div> </div>
<div class="flex-xs-middle option-right"> <div class="flex-xs-middle option-right">
<div class="select" style="float: left; top: 9px;"> <div class="select" style="float: left; top: 8px;">
<select (change)="doFilterRuleStatus($event)"> <select (change)="doFilterRuleStatus($event)">
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option> <option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
</select> </select>

View File

@ -1,9 +1,10 @@
export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
<div> <div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 24px;"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 32px;">
<div class="row flex-items-xs-right option-right"> <div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle"> <div class="flex-xs-middle">
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter> <hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span> <span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div> </div>
@ -21,7 +22,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
<clr-dg-cell>{{r.name}}</clr-dg-cell> <clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell> <clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell> <clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag> <hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}

View File

@ -14,6 +14,9 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service'; import { TagService, TagDefaultService } from '../service/tag.service';
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service'; import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { click } from '../utils'; import { click } from '../utils';
@ -90,7 +93,10 @@ describe('RepositoryComponentStackview (inline template)', () => {
RepositoryStackviewComponent, RepositoryStackviewComponent,
TagComponent, TagComponent,
ConfirmationDialogComponent, ConfirmationDialogComponent,
FilterComponent FilterComponent,
VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES,
INLINE_ALERT_DIRECTIVES
], ],
providers: [ providers: [
ErrorHandler, ErrorHandler,

View File

@ -30,7 +30,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../service/interface'; import { Tag, TagClickEvent } from '../service/interface';
@Component({ @Component({
selector: 'hbr-repository-stackview', selector: 'hbr-repository-stackview',
@ -41,10 +41,11 @@ import { Tag } from '../service/interface';
export class RepositoryStackviewComponent implements OnInit { export class RepositoryStackviewComponent implements OnInit {
@Input() projectId: number; @Input() projectId: number;
@Input() projectName: string = "unknown";
@Input() hasSignedIn: boolean; @Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;
@Output() tagClickEvent = new EventEmitter<Tag>(); @Output() tagClickEvent = new EventEmitter<TagClickEvent>();
lastFilteredRepoName: string; lastFilteredRepoName: string;
repositories: Repository[]; repositories: Repository[];
@ -72,6 +73,10 @@ export class RepositoryStackviewComponent implements OnInit {
return this.systemInfo ? this.systemInfo.with_notary : false; return this.systemInfo ? this.systemInfo.with_notary : false;
} }
public get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
confirmDeletion(message: ConfirmationAcknowledgement) { confirmDeletion(message: ConfirmationAcknowledgement) {
if (message && if (message &&
message.source === ConfirmationTargets.REPOSITORY && message.source === ConfirmationTargets.REPOSITORY &&
@ -132,7 +137,7 @@ export class RepositoryStackviewComponent implements OnInit {
this.retrieve(); this.retrieve();
} }
watchTagClickEvt(tag: Tag): void { watchTagClickEvt(tagClickEvt: TagClickEvent): void {
this.tagClickEvent.emit(tag); this.tagClickEvent.emit(tagClickEvt);
} }
} }

View File

@ -45,7 +45,7 @@ export interface Tag extends Base {
author: string; author: string;
created: Date; created: Date;
signature?: string; signature?: string;
vulnerability?: VulnerabilitySummary; scan_overview?: VulnerabilitySummary;
} }
/** /**
@ -145,6 +145,7 @@ export interface AccessLogItem {
* *
*/ */
export interface SystemInfo { export interface SystemInfo {
with_clair?: boolean;
with_notary?: boolean; with_notary?: boolean;
with_admiral?: boolean; with_admiral?: boolean;
admiral_endpoint?: string; admiral_endpoint?: string;
@ -156,9 +157,8 @@ export interface SystemInfo {
harbor_version?: string; harbor_version?: string;
} }
//Not finalized yet
export enum VulnerabilitySeverity { export enum VulnerabilitySeverity {
NONE, UNKNOWN, LOW, MEDIUM, HIGH _SEVERITY, NONE, UNKNOWN, LOW, MEDIUM, HIGH
} }
export interface VulnerabilityBase { export interface VulnerabilityBase {
@ -170,16 +170,31 @@ export interface VulnerabilityBase {
export interface VulnerabilityItem extends VulnerabilityBase { export interface VulnerabilityItem extends VulnerabilityBase {
fixedVersion: string; fixedVersion: string;
layer: string; layer?: string;
description: string; description: string;
} }
export interface VulnerabilitySummary { export interface VulnerabilitySummary {
total_package: number; image_digest?: string;
package_with_none: number; scan_status: string;
package_with_high?: number; job_id?: number;
package_with_medium?: number; severity: VulnerabilitySeverity;
package_With_low?: number; components: VulnerabilityComponents;
package_with_unknown?: number; update_time: Date; //Use as complete timestamp
complete_timestamp: Date; }
export interface VulnerabilityComponents {
total: number;
summary: VulnerabilitySeverityMetrics[];
}
export interface VulnerabilitySeverityMetrics {
severity: VulnerabilitySeverity;
count: number;
}
export interface TagClickEvent {
project_id: string | number;
repository_name: string;
tag_name: string;
} }

View File

@ -3,7 +3,8 @@ import 'rxjs/add/observable/of';
import { Injectable, Inject } from "@angular/core"; import { Injectable, Inject } from "@angular/core";
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http, URLSearchParams } from '@angular/http'; import { Http, URLSearchParams } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils'; import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from '../utils';
import { RequestQueryParams } from './RequestQueryParams';
import { import {
VulnerabilityItem, VulnerabilityItem,
@ -27,7 +28,7 @@ export abstract class ScanningResultService {
* *
* @memberOf ScanningResultService * @memberOf ScanningResultService
*/ */
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary; abstract getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
/** /**
* Get the detailed vulnerabilities scanning results. * Get the detailed vulnerabilities scanning results.
@ -38,30 +39,60 @@ export abstract class ScanningResultService {
* *
* @memberOf ScanningResultService * @memberOf ScanningResultService
*/ */
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]; abstract getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
/**
* Start a new vulnerability scanning
*
* @abstract
* @param {string} repoName
* @param {string} tagId
* @returns {(Observable<any> | Promise<any> | any)}
*
* @memberOf ScanningResultService
*/
abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any;
} }
@Injectable() @Injectable()
export class ScanningResultDefaultService extends ScanningResultService { export class ScanningResultDefaultService extends ScanningResultService {
_baseUrl: string = '/api/repositories';
constructor( constructor(
private http: Http, private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig) { @Inject(SERVICE_CONFIG) private config: IServiceConfig) {
super(); super();
if (this.config && this.config.vulnerabilityScanningBaseEndpoint) {
this._baseUrl = this.config.vulnerabilityScanningBaseEndpoint;
}
} }
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary { getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
if (!tagId || tagId.trim() === '') { if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument'); return Promise.reject('Bad argument');
} }
return Observable.of({}); return Observable.of({});
} }
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] { getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
if (!tagId || tagId.trim() === '') { if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument'); return Promise.reject('Bad argument');
} }
return Observable.of([]); return this.http.get(`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json() as VulnerabilityItem[])
.catch(error => Promise.reject(error));
}
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return this.http.post(`${this._baseUrl}/${repoName}/tags/${tagId}/scan`, null).toPromise()
.then(() => { return true })
.catch(error => Promise.reject(error));
} }
} }

View File

@ -9,7 +9,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslatorJsonLoader } from '../i18n/local-json.loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config'; import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
import { CookieService, CookieModule } from 'ngx-cookie'; import { CookieService, CookieModule } from 'ngx-cookie';
import { ClipboardModule } from 'ngx-clipboard'; import { ClipboardModule } from '../third-party/ngx-clipboard/index';
/*export function HttpLoaderFactory(http: Http) { /*export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json'); return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');

View File

@ -7,10 +7,10 @@ export const TAG_DETAIL_HTML: string = `
</div> </div>
<div class="title-block"> <div class="title-block">
<div class="tag-name"> <div class="tag-name">
{{tagDetails.name}}:v{{tagDetails.docker_version}} {{tagDetails.name}}
</div> </div>
<div class="tag-timestamp"> <div class="tag-timestamp">
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}} {{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
</div> </div>
</div> </div>
</div> </div>
@ -23,11 +23,13 @@ export const TAG_DETAIL_HTML: string = `
<div class="image-detail-label"> <div class="image-detail-label">
<div>{{'TAG.ARCHITECTURE' | translate }}</div> <div>{{'TAG.ARCHITECTURE' | translate }}</div>
<div>{{'TAG.OS' | translate }}</div> <div>{{'TAG.OS' | translate }}</div>
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div> <div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
</div> </div>
<div class="image-detail-value"> <div class="image-detail-value">
<div>{{tagDetails.architecture}}</div> <div>{{tagDetails.architecture}}</div>
<div>{{tagDetails.os}}</div> <div>{{tagDetails.os}}</div>
<div>{{tagDetails.docker_version}}</div>
<div>{{scanCompletedDatetime | date}}</div> <div>{{scanCompletedDatetime | date}}</div>
</div> </div>
</div> </div>
@ -67,7 +69,7 @@ export const TAG_DETAIL_HTML: string = `
</section> </section>
<section class="detail-section"> <section class="detail-section">
<div class="vulnerability-block"> <div class="vulnerability-block">
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid> <hbr-vulnerabilities-grid [repositoryId]="repositoryId" [tagId]="tagId"></hbr-vulnerabilities-grid>
</div> </div>
<div> <div>
<ng-content></ng-content> <ng-content></ng-content>

View File

@ -5,25 +5,40 @@ import { ResultGridComponent } from '../vulnerability-scanning/result-grid.compo
import { TagDetailComponent } from './tag-detail.component'; import { TagDetailComponent } from './tag-detail.component';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Tag, VulnerabilitySummary } from '../service/interface'; import { Tag, VulnerabilitySummary, VulnerabilityItem, VulnerabilitySeverity } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index'; import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { FilterComponent } from '../filter/index'; import { FilterComponent } from '../filter/index';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
describe('TagDetailComponent (inline template)', () => { describe('TagDetailComponent (inline template)', () => {
let comp: TagDetailComponent; let comp: TagDetailComponent;
let fixture: ComponentFixture<TagDetailComponent>; let fixture: ComponentFixture<TagDetailComponent>;
let tagService: TagService; let tagService: TagService;
let scanningService: ScanningResultService;
let spy: jasmine.Spy; let spy: jasmine.Spy;
let vulSpy: jasmine.Spy;
let mockVulnerability: VulnerabilitySummary = { let mockVulnerability: VulnerabilitySummary = {
total_package: 124, scan_status: VULNERABILITY_SCAN_STATUS.finished,
package_with_none: 92, severity: 5,
package_with_high: 10, update_time: new Date(),
package_with_medium: 6, components: {
package_With_low: 13, total: 124,
package_with_unknown: 3, summary: [{
complete_timestamp: new Date() severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
}
}; };
let mockTag: Tag = { let mockTag: Tag = {
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
@ -34,7 +49,7 @@ describe('TagDetailComponent (inline template)', () => {
"author": "steven", "author": "steven",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null, "signature": null,
vulnerability: mockVulnerability scan_overview: mockVulnerability
}; };
let config: IServiceConfig = { let config: IServiceConfig = {
@ -70,6 +85,22 @@ describe('TagDetailComponent (inline template)', () => {
tagService = fixture.debugElement.injector.get(TagService); tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag)); spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
let mockData: VulnerabilityItem[] = [];
for (let i = 0; i < 30; i++) {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
package: "package_" + i,
layer: "layer_" + i,
version: '4.' + i + ".0",
fixedVersion: '4.' + i + '.11',
description: "Mock data"
};
mockData.push(res);
}
scanningService = fixture.debugElement.injector.get(ScanningResultService);
vulSpy = spyOn(scanningService, 'getVulnerabilityScanningResults').and.returnValue(Promise.resolve(mockData));
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -85,7 +116,7 @@ describe('TagDetailComponent (inline template)', () => {
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name'); let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('nginx:v1.12.3'); expect(el.textContent.trim()).toEqual('nginx');
}); });
})); }));
@ -113,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => {
expect(el).toBeTruthy(); expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div'); let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy(); expect(el2).toBeTruthy();
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL"); expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
}); });
})); }));

View File

@ -3,7 +3,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { TAG_DETAIL_STYLES } from './tag-detail.component.css'; import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
import { TAG_DETAIL_HTML } from './tag-detail.component.html'; import { TAG_DETAIL_HTML } from './tag-detail.component.html';
import { TagService, Tag } from '../service/index'; import { TagService, Tag, VulnerabilitySeverity } from '../service/index';
import { toPromise } from '../utils'; import { toPromise } from '../utils';
import { ErrorHandler } from '../error-handler/index'; import { ErrorHandler } from '../error-handler/index';
@ -15,6 +15,11 @@ import { ErrorHandler } from '../error-handler/index';
providers: [] providers: []
}) })
export class TagDetailComponent implements OnInit { export class TagDetailComponent implements OnInit {
_highCount: number = 0;
_mediumCount: number = 0;
_lowCount: number = 0;
_unknownCount: number = 0;
@Input() tagId: string; @Input() tagId: string;
@Input() repositoryId: string; @Input() repositoryId: string;
tagDetails: Tag = { tagDetails: Tag = {
@ -36,7 +41,32 @@ export class TagDetailComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (this.repositoryId && this.tagId) { if (this.repositoryId && this.tagId) {
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId)) toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
.then(response => this.tagDetails = response) .then(response => {
this.tagDetails = response;
if (this.tagDetails &&
this.tagDetails.scan_overview &&
this.tagDetails.scan_overview.components &&
this.tagDetails.scan_overview.components.summary) {
this.tagDetails.scan_overview.components.summary.forEach(item => {
switch (item.severity) {
case VulnerabilitySeverity.UNKNOWN:
this._unknownCount += item.count;
break;
case VulnerabilitySeverity.LOW:
this._lowCount += item.count;
break;
case VulnerabilitySeverity.MEDIUM:
this._mediumCount += item.count;
break;
case VulnerabilitySeverity.HIGH:
this._highCount += item.count;
break;
default:
break;
}
});
}
})
.catch(error => this.errorHandler.error(error)) .catch(error => this.errorHandler.error(error))
} }
} }
@ -45,29 +75,29 @@ export class TagDetailComponent implements OnInit {
this.backEvt.emit(this.tagId); this.backEvt.emit(this.tagId);
} }
public get author(): string {
return this.tagDetails && this.tagDetails.author? this.tagDetails.author: 'TAG.ANONYMITY';
}
public get highCount(): number { public get highCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ? return this._highCount;
this.tagDetails.vulnerability.package_with_high : 0;
} }
public get mediumCount(): number { public get mediumCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ? return this._mediumCount;
this.tagDetails.vulnerability.package_with_medium : 0;
} }
public get lowCount(): number { public get lowCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ? return this._lowCount;
this.tagDetails.vulnerability.package_With_low : 0;
} }
public get unknownCount(): number { public get unknownCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ? return this._unknownCount;
this.tagDetails.vulnerability.package_with_unknown : 0;
} }
public get scanCompletedDatetime(): Date { public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.vulnerability ? return this.tagDetails && this.tagDetails.scan_overview ?
this.tagDetails.vulnerability.complete_timestamp : new Date(); this.tagDetails.scan_overview.update_time : new Date();
} }
public get suffixForHigh(): string { public get suffixForHigh(): string {

View File

@ -30,4 +30,11 @@ export const TAG_STYLE = `
:host >>> .datagrid .datagrid-body .datagrid-row-master { :host >>> .datagrid .datagrid-body .datagrid-row-master {
background-color: #eee; background-color: #eee;
} }
.truncated {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow:ellipsis;
}
`; `;

View File

@ -14,23 +14,27 @@ export const TAG_TEMPLATE = `
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2> <h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded"> <clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column> <clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column> <clr-dg-column style="width: 150px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column> <clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column> <clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" [clrDgField]="'architecture'" *ngIf="!withClair">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'os'" *ngIf="!withClair">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder> <clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button> <button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button> <button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a></clr-dg-cell> <clr-dg-cell style="width: 80px;" [ngSwitch]="withClair">
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell> <a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null"> <span *ngSwitchDefault>{{t.name}}</span>
</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: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon> <clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right"> <a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
@ -38,11 +42,14 @@ export const TAG_TEMPLATE = `
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span> <span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a> </a>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell style="width: 150px;" *ngIf="withClair">
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell> <hbr-vulnerability-bar [tagId]="t.name" [summary]="t.scan_overview" (startScanning)="scanTag($event)"></hbr-vulnerability-bar>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell> <clr-dg-cell style="width: 100px;">{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</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-cell style="width: 80px;" *ngIf="!withClair">{{t.architecture}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.os}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}

View File

@ -10,7 +10,11 @@ import { TagComponent } from './tag.component';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Tag } from '../service/interface'; import { Tag } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService } from '../service/tag.service'; import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { FILTER_DIRECTIVES } from '../filter/index'
import { Observable, Subscription } from 'rxjs/Rx';
describe('TagComponent (inline template)', () => { describe('TagComponent (inline template)', () => {
@ -42,12 +46,15 @@ describe('TagComponent (inline template)', ()=> {
], ],
declarations: [ declarations: [
TagComponent, TagComponent,
ConfirmationDialogComponent ConfirmationDialogComponent,
VULNERABILITY_DIRECTIVES,
FILTER_DIRECTIVES
], ],
providers: [ providers: [
ErrorHandler, ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService } { provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
] ]
}); });
})); }));

View File

@ -11,7 +11,17 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import {
Component,
OnInit,
ViewChild,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy
} from '@angular/core';
import { TagService } from '../service/tag.service'; import { TagService } from '../service/tag.service';
@ -22,24 +32,30 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { Tag } from '../service/interface'; import { Tag, TagClickEvent } from '../service/interface';
import { TAG_TEMPLATE } from './tag.component.html'; import { TAG_TEMPLATE } from './tag.component.html';
import { TAG_STYLE } from './tag.component.css'; import { TAG_STYLE } from './tag.component.css';
import { toPromise, CustomComparator } from '../utils'; import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { State, Comparator } from 'clarity-angular'; import { State, Comparator } from 'clarity-angular';
import { ScanningResultService } from '../service/index';
import { Observable, Subscription } from 'rxjs/Rx';
const STATE_CHECK_INTERVAL: number = 2000;//2s
@Component({ @Component({
selector: 'hbr-tag', selector: 'hbr-tag',
template: TAG_TEMPLATE, template: TAG_TEMPLATE,
styles: [TAG_STYLE], styles: [TAG_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagComponent implements OnInit { export class TagComponent implements OnInit, OnDestroy {
@Input() projectId: number; @Input() projectId: number;
@Input() repoName: string; @Input() repoName: string;
@ -49,9 +65,10 @@ export class TagComponent implements OnInit {
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;
@Input() registryUrl: string; @Input() registryUrl: string;
@Input() withNotary: boolean; @Input() withNotary: boolean;
@Input() withClair: boolean;
@Output() refreshRepo = new EventEmitter<boolean>(); @Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<Tag>(); @Output() tagClickEvent = new EventEmitter<TagClickEvent>();
tags: Tag[]; tags: Tag[];
@ -66,6 +83,10 @@ export class TagComponent implements OnInit {
loading: boolean = false; loading: boolean = false;
stateCheckTimer: Subscription;
tagsInScanning: { [key: string]: any } = {};
scanningTagCount: number = 0;
@ViewChild('confirmationDialog') @ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent; confirmationDialog: ConfirmationDialogComponent;
@ -73,6 +94,7 @@ export class TagComponent implements OnInit {
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private tagService: TagService, private tagService: TagService,
private translateService: TranslateService, private translateService: TranslateService,
private scanningService: ScanningResultService,
private ref: ChangeDetectorRef) { } private ref: ChangeDetectorRef) { }
confirmDeletion(message: ConfirmationAcknowledgement) { confirmDeletion(message: ConfirmationAcknowledgement) {
@ -108,11 +130,24 @@ export class TagComponent implements OnInit {
} }
this.retrieve(); this.retrieve();
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
if (this.scanningTagCount > 0) {
this.updateScanningStates();
}
});
}
ngOnDestroy(): void {
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
}
} }
retrieve() { retrieve() {
this.tags = []; this.tags = [];
this.loading = true; this.loading = true;
toPromise<Tag[]>(this.tagService toPromise<Tag[]>(this.tagService
.getTags(this.repoName)) .getTags(this.repoName))
.then(items => { .then(items => {
@ -169,7 +204,55 @@ export class TagComponent implements OnInit {
onTagClick(tag: Tag): void { onTagClick(tag: Tag): void {
if (tag) { if (tag) {
this.tagClickEvent.emit(tag); let evt: TagClickEvent = {
project_id: this.projectId,
repository_name: this.repoName,
tag_name: tag.name
};
this.tagClickEvent.emit(evt);
} }
} }
scanTag(tagId: string): void {
//Double check
if (this.tagsInScanning[tagId]) {
return;
}
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, tagId))
.then(() => {
//Add to scanning map
this.tagsInScanning[tagId] = tagId;
//Counting
this.scanningTagCount += 1;
})
.catch(error => this.errorHandler.error(error));
}
updateScanningStates(): void {
toPromise<Tag[]>(this.tagService
.getTags(this.repoName))
.then(items => {
console.debug("updateScanningStates called!");
//Reset the scanning states
this.tagsInScanning = {};
this.scanningTagCount = 0;
items.forEach(item => {
if (item.scan_overview) {
if (item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.pending ||
item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.running) {
this.tagsInScanning[item.name] = item.name;
this.scanningTagCount += 1;
}
}
});
this.tags = items;
})
.catch(error => {
this.errorHandler.error(error);
});
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
}
} }

View File

@ -0,0 +1,2 @@
export * from './ngx-window-token/index';
export * from './ngx-clipboard/index';

View File

@ -0,0 +1,49 @@
import { ClipboardService } from './clipboard.service';
import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, Renderer, ElementRef } from '@angular/core';
@Directive({
selector: '[ngxClipboard]'
})
export class ClipboardDirective implements OnInit, OnDestroy {
@Input('ngxClipboard') public targetElm: HTMLInputElement;
@Input() public cbContent: string;
@Output() public cbOnSuccess: EventEmitter<any> = new EventEmitter<any>();
@Output() public cbOnError: EventEmitter<any> = new EventEmitter<any>();
constructor(
private clipboardSrv: ClipboardService,
private renderer: Renderer
) { }
public ngOnInit() { }
public ngOnDestroy() {
this.clipboardSrv.destroy();
}
@HostListener('click', ['$event.target']) private onClick(button: ElementRef) {
if (!this.clipboardSrv.isSupported) {
this.handleResult(false, undefined);
} else if (this.targetElm && this.clipboardSrv.isTargetValid(this.targetElm)) {
this.handleResult(this.clipboardSrv.copyFromInputElement(this.targetElm, this.renderer),
this.targetElm.value);
} else if (this.cbContent) {
this.handleResult(this.clipboardSrv.copyFromContent(this.cbContent, this.renderer), this.cbContent);
}
}
/**
* Fires an event based on the copy operation result.
* @param {Boolean} succeeded
*/
private handleResult(succeeded: Boolean, copiedContent: string) {
if (succeeded) {
this.cbOnSuccess.emit({ isSuccess: true, content: copiedContent });
} else {
this.cbOnError.emit({ isSuccess: false });
}
}
}

View File

@ -0,0 +1,110 @@
import { Inject, InjectionToken, Injectable, Optional, Renderer, SkipSelf } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';
import { WINDOW } from "../ngx-window-token/index";
@Injectable()
export class ClipboardService {
private tempTextArea: HTMLTextAreaElement;
constructor(
@Inject(DOCUMENT) private document: any,
@Inject(WINDOW) private window: any,
) { }
public get isSupported(): boolean {
return !!this.document.queryCommandSupported && !!this.document.queryCommandSupported('copy');
}
public isTargetValid(element: HTMLInputElement | HTMLTextAreaElement): boolean {
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
if (element.hasAttribute('disabled')) {
// tslint:disable-next-line:max-line-length
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
}
return true;
}
throw new Error('Target should be input or textarea');
}
/**
* copyFromInputElement
*/
public copyFromInputElement(targetElm: HTMLInputElement | HTMLTextAreaElement, renderer: Renderer): boolean {
try {
this.selectTarget(targetElm, renderer);
const re = this.copyText();
this.clearSelection(targetElm, this.window);
return re;
} catch (error) {
return false;
}
}
/**
* Creates a fake textarea element, sets its value from `text` property,
* and makes a selection on it.
*/
public copyFromContent(content: string, renderer: Renderer) {
if (!this.tempTextArea) {
this.tempTextArea = this.createTempTextArea(this.document, this.window);
this.document.body.appendChild(this.tempTextArea);
}
this.tempTextArea.value = content;
return this.copyFromInputElement(this.tempTextArea, renderer);
}
// remove temporary textarea if any
public destroy() {
if (this.tempTextArea) {
this.document.body.removeChild(this.tempTextArea);
this.tempTextArea = undefined;
}
}
// select the target html input element
private selectTarget(inputElement: HTMLInputElement | HTMLTextAreaElement, renderer: Renderer): number | undefined {
renderer.invokeElementMethod(inputElement, 'select');
renderer.invokeElementMethod(inputElement, 'setSelectionRange', [0, inputElement.value.length]);
return inputElement.value.length;
}
private copyText(): boolean {
return this.document.execCommand('copy');
}
// Removes current selection and focus from `target` element.
private clearSelection(inputElement: HTMLInputElement | HTMLTextAreaElement, window: Window) {
// tslint:disable-next-line:no-unused-expression
inputElement && inputElement.blur();
window.getSelection().removeAllRanges();
}
// create a fake textarea for copy command
private createTempTextArea(doc: Document, window: Window): HTMLTextAreaElement {
const isRTL = doc.documentElement.getAttribute('dir') === 'rtl';
let ta: HTMLTextAreaElement;
ta = doc.createElement('textarea');
// Prevent zooming on iOS
ta.style.fontSize = '12pt';
// Reset box model
ta.style.border = '0';
ta.style.padding = '0';
ta.style.margin = '0';
// Move element out of screen horizontally
ta.style.position = 'absolute';
ta.style[isRTL ? 'right' : 'left'] = '-9999px';
// Move element to the same position vertically
let yPosition = window.pageYOffset || doc.documentElement.scrollTop;
ta.style.top = yPosition + 'px';
ta.setAttribute('readonly', '');
return ta;
}
}
// this pattern is mentioned in https://github.com/angular/angular/issues/13854 in #43
export function CLIPBOARD_SERVICE_PROVIDER_FACTORY(doc: Document, win: Window, parentDispatcher: ClipboardService) {
return parentDispatcher || new ClipboardService(doc, win);
};
export const CLIPBOARD_SERVICE_PROVIDER = {
provide: ClipboardService,
deps: [DOCUMENT, WINDOW, [new Optional(), new SkipSelf(), ClipboardService]],
useFactory: CLIPBOARD_SERVICE_PROVIDER_FACTORY
};

View File

@ -0,0 +1,15 @@
import { ClipboardDirective } from './clipboard.directive';
import { CLIPBOARD_SERVICE_PROVIDER } from './clipboard.service';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { WindowTokenModule } from '../ngx-window-token/index';
export * from './clipboard.directive';
export * from './clipboard.service';
@NgModule({
imports: [CommonModule, WindowTokenModule],
declarations: [ClipboardDirective],
exports: [ClipboardDirective],
providers: [CLIPBOARD_SERVICE_PROVIDER]
})
export class ClipboardModule { }

View File

@ -0,0 +1 @@
export { WindowTokenModule, WINDOW } from './window-token';

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { InjectionToken } from '@angular/core';
export const WINDOW = new InjectionToken<Window>('WindowToken');
export function _window(): Window {
return window;
}
@NgModule({
providers: [{
provide: WINDOW,
useFactory: _window
}]
})
export class WindowTokenModule { }

View File

@ -124,3 +124,15 @@ export class CustomComparator<T> implements Comparator<T> {
* The default page size * The default page size
*/ */
export const DEFAULT_PAGE_SIZE: number = 15; export const DEFAULT_PAGE_SIZE: number = 15;
/**
* The state of vulnerability scanning
*/
export const VULNERABILITY_SCAN_STATUS = {
unknown: "n/a",
pending: "pending",
running: "running",
error: "error",
stopped: "stopped",
finished: "finished"
};

View File

@ -3,6 +3,7 @@ import { ResultGridComponent } from './result-grid.component';
import { ResultBarChartComponent } from './result-bar-chart.component'; import { ResultBarChartComponent } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component'; import { ResultTipComponent } from './result-tip.component';
export * from './result-tip.component';
export * from "./result-grid.component"; export * from "./result-grid.component";
export * from './result-bar-chart.component'; export * from './result-bar-chart.component';

View File

@ -11,6 +11,7 @@ import { ScanningResultService, ScanningResultDefaultService } from '../service/
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ErrorHandler } from '../error-handler/index'; import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
describe('ResultBarChartComponent (inline template)', () => { describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent; let component: ResultBarChartComponent;
@ -20,13 +21,25 @@ describe('ResultBarChartComponent (inline template)', () => {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing" vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
}; };
let mockData: VulnerabilitySummary = { let mockData: VulnerabilitySummary = {
total_package: 124, scan_status: VULNERABILITY_SCAN_STATUS.finished,
package_with_none: 92, severity: 5,
package_with_high: 10, update_time: new Date(),
package_with_medium: 6, components: {
package_With_low: 13, total: 124,
package_with_unknown: 3, summary: [{
complete_timestamp: new Date() severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
}
}; };
beforeEach(async(() => { beforeEach(async(() => {
@ -115,7 +128,7 @@ describe('ResultBarChartComponent (inline template)', () => {
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none'); let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
expect(el).not.toBeNull(); expect(el).not.toBeNull();
expect(el.style.width).toEqual("74px"); expect(el.style.width).toEqual("73px");
}); });
})); }));

View File

@ -2,11 +2,14 @@ import {
Component, Component,
Input, Input,
Output, Output,
EventEmitter EventEmitter,
OnInit
} from '@angular/core'; } from '@angular/core';
import { VulnerabilitySummary } from '../service/index'; import { VulnerabilitySummary } from '../service/index';
import { SCANNING_STYLES } from './scanning.css'; import { SCANNING_STYLES } from './scanning.css';
import { BAR_CHART_COMPONENT_HTML } from './scanning.html'; import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { VulnerabilitySeverity } from '../service/index';
export enum ScanState { export enum ScanState {
COMPLETED, //Scanning work successfully completed COMPLETED, //Scanning work successfully completed
@ -22,18 +25,47 @@ export enum ScanState {
styles: [SCANNING_STYLES], styles: [SCANNING_STYLES],
template: BAR_CHART_COMPONENT_HTML template: BAR_CHART_COMPONENT_HTML
}) })
export class ResultBarChartComponent { export class ResultBarChartComponent implements OnInit {
@Input() tagId: string = ""; @Input() tagId: string = "";
@Input() state: ScanState = ScanState.UNKNOWN; @Input() state: ScanState = ScanState.PENDING;
@Input() summary: VulnerabilitySummary = { @Input() summary: VulnerabilitySummary = {
total_package: 0, scan_status: VULNERABILITY_SCAN_STATUS.unknown,
package_with_none: 0, severity: VulnerabilitySeverity.UNKNOWN,
complete_timestamp: new Date() update_time: new Date(),
components: {
total: 0,
summary: []
}
}; };
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>(); @Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
scanningInProgress: boolean = false;
constructor() { } constructor() { }
ngOnInit(): void {
if (this.summary && this.summary.scan_status) {
switch (this.summary.scan_status) {
case VULNERABILITY_SCAN_STATUS.unknown:
this.state = ScanState.UNKNOWN;
break;
case VULNERABILITY_SCAN_STATUS.error:
this.state = ScanState.ERROR;
break;
case VULNERABILITY_SCAN_STATUS.pending:
this.state = ScanState.QUEUED;
break;
case VULNERABILITY_SCAN_STATUS.stopped:
this.state = ScanState.PENDING;
break;
case VULNERABILITY_SCAN_STATUS.finished:
this.state = ScanState.COMPLETED;
break;
default:
break;
}
}
}
public get completed(): boolean { public get completed(): boolean {
return this.state === ScanState.COMPLETED; return this.state === ScanState.COMPLETED;
} }
@ -60,6 +92,7 @@ export class ResultBarChartComponent {
scanNow(): void { scanNow(): void {
if (this.tagId && this.tagId !== '') { if (this.tagId && this.tagId !== '') {
this.scanningInProgress = true;
this.startScanning.emit(this.tagId); this.startScanning.emit(this.tagId);
} }
} }

View File

@ -1,7 +1,8 @@
import { Component, OnInit, Input } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { import {
ScanningResultService, ScanningResultService,
VulnerabilityItem VulnerabilityItem,
VulnerabilitySeverity
} from '../service/index'; } from '../service/index';
import { ErrorHandler } from '../error-handler/index'; import { ErrorHandler } from '../error-handler/index';
@ -16,7 +17,10 @@ import { SCANNING_STYLES } from './scanning.css';
}) })
export class ResultGridComponent implements OnInit { export class ResultGridComponent implements OnInit {
scanningResults: VulnerabilityItem[] = []; scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = [];
@Input() tagId: string; @Input() tagId: string;
@Input() repositoryId: string;
constructor( constructor(
private scanningService: ScanningResultService, private scanningService: ScanningResultService,
@ -24,26 +28,50 @@ export class ResultGridComponent implements OnInit {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadResults(this.tagId); this.loadResults(this.repositoryId, this.tagId);
} }
showDetail(result: VulnerabilityItem): void { loadResults(repositoryId: string, tagId: string): void {
console.log(result.id); toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId))
}
loadResults(tagId: string): void {
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
.then((results: VulnerabilityItem[]) => { .then((results: VulnerabilityItem[]) => {
this.scanningResults = results; this.dataCache = results;
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
}) })
.catch(error => { this.errorHandler.error(error) }) .catch(error => { this.errorHandler.error(error) })
} }
//TODO: Should query from back-end service
filterVulnerabilities(terms: string): void { filterVulnerabilities(terms: string): void {
console.log(terms); if (terms.trim() === '') {
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
} else {
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => this._regexpFilter(terms, item.package));
}
} }
refresh(): void { refresh(): void {
this.loadResults(this.tagId); this.loadResults(this.repositoryId, this.tagId);
}
severityText(severity: VulnerabilitySeverity): string {
switch (severity) {
case VulnerabilitySeverity.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VulnerabilitySeverity.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VulnerabilitySeverity.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VulnerabilitySeverity.NONE:
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
case VulnerabilitySeverity.UNKNOWN:
return 'VULNERABILITY.SEVERITY.UNKNOWN';
default:
return 'UNKNOWN';
}
}
_regexpFilter(terms: string, testedValue: any): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(testedValue);
} }
} }

View File

@ -9,6 +9,7 @@ import { ResultTipComponent } from './result-tip.component';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
describe('ResultTipComponent (inline template)', () => { describe('ResultTipComponent (inline template)', () => {
let component: ResultTipComponent; let component: ResultTipComponent;
@ -17,13 +18,25 @@ describe('ResultTipComponent (inline template)', () => {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing" vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
}; };
let mockData: VulnerabilitySummary = { let mockData: VulnerabilitySummary = {
total_package: 124, scan_status: VULNERABILITY_SCAN_STATUS.finished,
package_with_none: 90, severity: 5,
package_with_high: 13, update_time: new Date(),
package_with_medium: 10, components: {
package_With_low: 10, total: 124,
package_with_unknown: 1, summary: [{
complete_timestamp: new Date() severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
}
}; };
beforeEach(async(() => { beforeEach(async(() => {

View File

@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { SCANNING_STYLES } from './scanning.css'; import { SCANNING_STYLES } from './scanning.css';
import { TIP_COMPONENT_HTML } from './scanning.html'; import { TIP_COMPONENT_HTML } from './scanning.html';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
export const MIN_TIP_WIDTH = 5; export const MIN_TIP_WIDTH = 5;
export const MAX_TIP_WIDTH = 100; export const MAX_TIP_WIDTH = 100;
@ -15,24 +16,64 @@ export const MAX_TIP_WIDTH = 100;
}) })
export class ResultTipComponent implements OnInit { export class ResultTipComponent implements OnInit {
_tipTitle: string = ""; _tipTitle: string = "";
_highCount: number = 0;
_mediumCount: number = 0;
_lowCount: number = 0;
_unknownCount: number = 0;
_noneCount: number = 0;
totalPackages: number = 0;
packagesWithVul: number = 0;
@Input() summary: VulnerabilitySummary = { @Input() summary: VulnerabilitySummary = {
total_package: 0, scan_status: VULNERABILITY_SCAN_STATUS.unknown,
package_with_none: 0, severity: VulnerabilitySeverity.UNKNOWN,
complete_timestamp: new Date() update_time: new Date(),
components: {
total: 0,
summary: []
}
}; };
constructor(private translate: TranslateService) { } constructor(private translate: TranslateService) { }
ngOnInit(): void { ngOnInit(): void {
this.totalPackages = this.summary && this.summary.components ? this.summary.components.total : 0;
if (this.summary && this.summary.components && this.summary.components.summary) {
this.summary.components.summary.forEach(item => {
if (item.severity != VulnerabilitySeverity.NONE) {
this.packagesWithVul += item.count
}
switch (item.severity) {
case VulnerabilitySeverity.UNKNOWN:
this._unknownCount += item.count;
break;
case VulnerabilitySeverity.NONE:
this._noneCount += item.count;
break;
case VulnerabilitySeverity.LOW:
this._lowCount += item.count;
break;
case VulnerabilitySeverity.MEDIUM:
this._mediumCount += item.count;
break;
case VulnerabilitySeverity.HIGH:
this._highCount += item.count;
break;
default:
break;
}
});
}
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE', this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
{ totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package }) { totalVulnerability: this.packagesWithVul, totalPackages: this.totalPackages })
.subscribe((res: string) => this._tipTitle = res); .subscribe((res: string) => this._tipTitle = res);
} }
tipWidth(severity: VulnerabilitySeverity): string { tipWidth(severity: VulnerabilitySeverity): string {
let n: number = 0; let n: number = 0;
let m: number = this.summary ? this.summary.total_package : 0; let m: number = this.totalPackages;
if (m === 0) { if (m === 0) {
return 0 + 'px'; return 0 + 'px';
@ -76,8 +117,8 @@ export class ResultTipComponent implements OnInit {
return "VULNERABILITY.SINGULAR"; return "VULNERABILITY.SINGULAR";
} }
public get totalVulnerabilities(): number { public get completeTimestamp(): Date {
return this.summary.total_package - this.summary.package_with_none; return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
} }
public get hasHigh(): boolean { public get hasHigh(): boolean {
@ -105,22 +146,22 @@ export class ResultTipComponent implements OnInit {
} }
public get highCount(): number { public get highCount(): number {
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0; return this._highCount;
} }
public get mediumCount(): number { public get mediumCount(): number {
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0; return this._mediumCount;
} }
public get lowCount(): number { public get lowCount(): number {
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0; return this._lowCount;
} }
public get unknownCount(): number { public get unknownCount(): number {
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0; return this._unknownCount;
} }
public get noneCount(): number { public get noneCount(): number {
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0; return this._noneCount;
} }
public get highSuffix(): string { public get highSuffix(): string {
@ -144,6 +185,6 @@ export class ResultTipComponent implements OnInit {
} }
public get maxWidth(): string { public get maxWidth(): string {
return MAX_TIP_WIDTH+"px"; return (MAX_TIP_WIDTH + 20) + "px";
} }
} }

View File

@ -1,8 +1,6 @@
export const SCANNING_STYLES: string = ` export const SCANNING_STYLES: string = `
.bar-wrapper { .bar-wrapper {
width: 150px; width: 120px;
height: 24px;
display: inline-block;
} }
.bar-state { .bar-state {
text-align: center !important; text-align: center !important;
@ -17,9 +15,8 @@ export const SCANNING_STYLES: string = `
} }
.tip-wrapper { .tip-wrapper {
display: inline-block; display: inline-block;
height: 16px; height: 10px;
max-height: 16px; max-width: 120px;
max-width: 150px;
} }
.tip-position { .tip-position {
margin-left: -4px; margin-left: -4px;

View File

@ -1,11 +1,11 @@
export const TIP_COMPONENT_HTML: string = ` export const TIP_COMPONENT_HTML: string = `
<div class="tip-wrapper tip-position" [style.width]='maxWidth'> <div class="tip-wrapper tip-position" [style.width]='maxWidth'>
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'"> <clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(4)'></div> <div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(5)'></div>
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(3)'></div> <div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(4)'></div>
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(2)'></div> <div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(3)'></div>
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(1)'></div> <div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(2)'></div>
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(0)'></div> <div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(1)'></div>
<clr-tooltip-content> <clr-tooltip-content>
<div> <div>
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span> <span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
</div> </div>
<div> <div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span> <span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{summary.complete_timestamp | date}}</span> <span>{{completeTimestamp | date}}</span>
</div> </div>
</clr-tooltip-content> </clr-tooltip-content>
</clr-tooltip> </clr-tooltip>
@ -58,21 +58,23 @@ export const GRID_COMPONENT_HTML: string = `
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version</clr-dg-column> <clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version</clr-dg-column>
<clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'layer'">{{'VULNERABILITY.GRID.COLUMN_LAYER' | translate}}</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.GRID.PLACEHOLDER' | translate}}</clr-dg-placeholder> <clr-dg-placeholder>{{'VULNERABILITY.GRID.PLACEHOLDER' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults"> <clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-action-overflow>
<button class="action-item" (click)="showDetail(res)">Detail</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{res.id}}</clr-dg-cell> <clr-dg-cell>{{res.id}}</clr-dg-cell>
<clr-dg-cell>{{res.severity}}</clr-dg-cell> <clr-dg-cell [ngSwitch]="res.severity">
<span *ngSwitchCase="5" class="label label-danger">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="4" class="label label-warning">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="3" class="label label-success">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="1" class="label label-info">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell> <clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell> <clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell> <clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell>
<clr-dg-cell>{{res.layer}}</clr-dg-cell> <clr-dg-row-detail *clrIfExpanded>
<clr-dg-cell>{{res.description}}</clr-dg-cell> {{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
</clr-dg-row-detail>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
@ -87,7 +89,7 @@ export const GRID_COMPONENT_HTML: string = `
export const BAR_CHART_COMPONENT_HTML: string = ` export const BAR_CHART_COMPONENT_HTML: string = `
<div class="bar-wrapper"> <div class="bar-wrapper">
<div *ngIf="pending" class="bar-state"> <div *ngIf="pending" class="bar-state">
<button class="btn btn-link scanning-button" (click)="scanNow()">{{'VULNERABILITY.STATE.PENDING' | translate}}</button> <button class="btn btn-link scanning-button" (click)="scanNow()" [disabled]="scanningInProgress">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
</div> </div>
<div *ngIf="queued" class="bar-state"> <div *ngIf="queued" class="bar-state">
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span> <span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor", "name": "harbor",
"version": "1.1.0", "version": "1.2.0",
"description": "Harbor UI with Clarity", "description": "Harbor UI with Clarity",
"angular-cli": {}, "angular-cli": {},
"scripts": { "scripts": {
@ -13,25 +13,25 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^4.0.1", "@angular/animations": "~4.1.3",
"@angular/common": "^4.0.1", "@angular/common": "~4.1.3",
"@angular/compiler": "^4.0.1", "@angular/compiler": "~4.1.3",
"@angular/compiler-cli": "^4.0.2", "@angular/compiler-cli": "~4.1.3",
"@angular/core": "^4.0.1", "@angular/core": "~4.1.3",
"@angular/forms": "^4.0.1", "@angular/forms": "~4.1.3",
"@angular/http": "^4.0.1", "@angular/http": "~4.1.3",
"@angular/platform-browser": "^4.0.1", "@angular/platform-browser": "~4.1.3",
"@angular/platform-browser-dynamic": "^4.0.1", "@angular/platform-browser-dynamic": "~4.1.3",
"@angular/platform-server": "^4.0.2", "@angular/router": "~4.1.3",
"@angular/router": "^4.0.1",
"@ngx-translate/core": "^6.0.0", "@ngx-translate/core": "^6.0.0",
"@ngx-translate/http-loader": "0.0.3", "@ngx-translate/http-loader": "0.0.3",
"@types/jquery": "^2.0.41", "@types/jquery": "^2.0.41",
"@webcomponents/custom-elements": "1.0.0-alpha.3", "@webcomponents/custom-elements": "1.0.0-alpha.3",
"clarity-angular": "^0.9.0", "clarity-angular": "^0.9.8",
"clarity-icons": "^0.9.0", "clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.0", "clarity-ui": "^0.9.8",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "^0.2.13",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",
@ -42,7 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "^1.0.0", "@angular/cli": "^1.0.0",
"@angular/compiler-cli": "^4.0.1", "@angular/compiler-cli": "~4.1.3",
"@types/core-js": "^0.9.34", "@types/core-js": "^0.9.34",
"@types/jasmine": "~2.2.30", "@types/jasmine": "~2.2.30",
"@types/node": "^6.0.42", "@types/node": "^6.0.42",

View File

@ -31,20 +31,6 @@ export class AppComponent {
private session: SessionService, private session: SessionService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private titleService: Title) { private titleService: Title) {
translate.addLangs(supportedLangs);
translate.setDefaultLang(enLang);
//If user has selected lang, then directly use it
let langSetting = this.cookie.get("harbor-lang");
if (!langSetting || langSetting.trim() === "") {
//Use browser lang
langSetting = translate.getBrowserCultureLang().toLowerCase();
}
let selectedLang = this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang;
translate.use(selectedLang);
//Override page title //Override page title
let key: string = "APP_TITLE.HARBOR"; let key: string = "APP_TITLE.HARBOR";
if (this.appConfigService.isIntegrationMode()) { if (this.appConfigService.isIntegrationMode()) {
@ -55,10 +41,4 @@ export class AppComponent {
this.titleService.setTitle(res); this.titleService.setTitle(res);
}); });
} }
isLangMatch(browserLang: string, supportedLangs: string[]) {
if (supportedLangs && supportedLangs.length > 0) {
return supportedLangs.find(lang => lang === browserLang);
}
}
} }

View File

@ -13,9 +13,6 @@
// limitations under the License. // limitations under the License.
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER, LOCALE_ID } from '@angular/core'; import { NgModule, APP_INITIALIZER, LOCALE_ID } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { ClarityModule } from 'clarity-angular';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BaseModule } from './base/base.module'; import { BaseModule } from './base/base.module';
@ -24,17 +21,9 @@ import { SharedModule } from './shared/shared.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { ConfigurationModule } from './config/config.module'; import { ConfigurationModule } from './config/config.module';
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
import { AppConfigService } from './app-config.service'; import { AppConfigService } from './app-config.service';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
}
export function initConfig(configService: AppConfigService) { export function initConfig(configService: AppConfigService) {
return () => configService.load(); return () => configService.load();
} }
@ -52,18 +41,7 @@ export function getCurrentLanguage(translateService: TranslateService) {
BaseModule, BaseModule,
AccountModule, AccountModule,
HarborRoutingModule, HarborRoutingModule,
ConfigurationModule, ConfigurationModule
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (HttpLoaderFactory),
deps: [Http]
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler
}
})
], ],
providers: [ providers: [
AppConfigService, AppConfigService,

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Project } from '../../project/project'; import { Project } from '../../project/project';
import { Repository } from '../../repository/repository'; import { Repository } from 'harbor-ui';
export class SearchResults { export class SearchResults {
constructor(){ constructor(){

View File

@ -21,14 +21,14 @@ import { ProjectComponent } from './project/project.component';
import { UserComponent } from './user/user.component'; import { UserComponent } from './user/user.component';
import { ReplicationManagementComponent } from './replication/replication-management/replication-management.component'; import { ReplicationManagementComponent } from './replication/replication-management/replication-management.component';
import { TotalReplicationComponent } from './replication/total-replication/total-replication.component'; import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component';
import { DestinationComponent } from './replication/destination/destination.component'; import { DestinationPageComponent } from './replication/destination/destination-page.component';
import { ProjectDetailComponent } from './project/project-detail/project-detail.component'; import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { RepositoryComponent } from './repository/repository.component'; import { RepositoryPageComponent } from './repository/repository-page.component';
import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component'; import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component';
import { ReplicationComponent } from './replication/replication.component'; import { ReplicationPageComponent } from './replication/replication-page.component';
import { MemberComponent } from './project/member/member.component'; import { MemberComponent } from './project/member/member.component';
import { AuditLogComponent } from './log/audit-log.component'; import { AuditLogComponent } from './log/audit-log.component';
@ -36,7 +36,7 @@ import { ProjectRoutingResolver } from './project/project-routing-resolver.servi
import { SystemAdminGuard } from './shared/route/system-admin-activate.service'; import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { SignUpComponent } from './account/sign-up/sign-up.component'; import { SignUpComponent } from './account/sign-up/sign-up.component';
import { ResetPasswordComponent } from './account/password/reset-password.component'; import { ResetPasswordComponent } from './account/password/reset-password.component';
import { RecentLogComponent } from './log/recent-log.component'; import { LogPageComponent } from './log/log-page.component';
import { ConfigurationComponent } from './config/config.component'; import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component' import { PageNotFoundComponent } from './shared/not-found/not-found.component'
import { StartPageComponent } from './base/start-page/start.component'; import { StartPageComponent } from './base/start-page/start.component';
@ -48,6 +48,8 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
import { MemberGuard } from './shared/route/member-guard-activate.service'; import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
const harborRoutes: Routes = [ const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' }, { path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'reset_password', component: ResetPasswordComponent }, { path: 'reset_password', component: ResetPasswordComponent },
@ -68,7 +70,7 @@ const harborRoutes: Routes = [
}, },
{ {
path: 'logs', path: 'logs',
component: RecentLogComponent component: LogPageComponent
}, },
{ {
path: 'users', path: 'users',
@ -83,11 +85,11 @@ const harborRoutes: Routes = [
children: [ children: [
{ {
path: 'rules', path: 'rules',
component: TotalReplicationComponent component: TotalReplicationPageComponent
}, },
{ {
path: 'endpoints', path: 'endpoints',
component: DestinationComponent component: DestinationPageComponent
} }
] ]
}, },
@ -108,20 +110,24 @@ const harborRoutes: Routes = [
}, },
children: [ children: [
{ {
path: 'repository', path: 'repositories',
component: RepositoryComponent component: RepositoryPageComponent
}, },
{ {
path: 'replication', path: 'repositories/:repo/tags/:tag',
component: ReplicationComponent, component: TagDetailPageComponent
},
{
path: 'replications',
component: ReplicationPageComponent,
canActivate: [SystemAdminGuard] canActivate: [SystemAdminGuard]
}, },
{ {
path: 'member', path: 'members',
component: MemberComponent component: MemberComponent
}, },
{ {
path: 'log', path: 'logs',
component: AuditLogComponent component: AuditLogComponent
} }
] ]

View File

@ -1,5 +1,11 @@
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 22px; }
margin-bottom: 2px;
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
} }

View File

@ -1,10 +1,12 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top:12px;">
<div class="row flex-items-xs-right option-right"> <div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle"> <div class="flex-xs-middle">
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button> <button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter> <hbr-filter [withDivider]="true" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></hbr-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a> <span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div> </div>
</div> </div>
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0"> <div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">

View File

@ -0,0 +1,3 @@
<div>
<hbr-log withTitle="true"></hbr-log>
</div>

View File

@ -11,13 +11,12 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
export class Tag { import { Component } from '@angular/core';
digest: string;
name: string; @Component({
architecture: string; selector: 'log-page',
os: string; templateUrl: './log-page.component.html'
docker_version: string; })
author: string;
created: Date; export class LogPageComponent {
signature?: {[key: string]: any | any[]}
} }

View File

@ -15,17 +15,17 @@ import { NgModule } from '@angular/core';
import { AuditLogComponent } from './audit-log.component'; import { AuditLogComponent } from './audit-log.component';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { AuditLogService } from './audit-log.service'; import { AuditLogService } from './audit-log.service';
import { RecentLogComponent } from './recent-log.component'; import { LogPageComponent } from './log-page.component';
@NgModule({ @NgModule({
imports: [SharedModule], imports: [SharedModule],
declarations: [ declarations: [
AuditLogComponent, AuditLogComponent,
RecentLogComponent LogPageComponent
], ],
providers: [AuditLogService], providers: [AuditLogService],
exports: [ exports: [
AuditLogComponent, AuditLogComponent,
RecentLogComponent] LogPageComponent]
}) })
export class LogModule { } export class LogModule { }

View File

@ -1,42 +0,0 @@
.h2-log-override {
margin-top: 0px !important;
}
.action-head-pos {
padding-right: 18px;
}
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #00bfff;
}
.custom-lines-button {
padding: 0px !important;
min-width: 25px !important;
}
.lines-button-toggole {
font-size: 16px;
text-decoration: underline;
}
.log-select {
width: 130px;
display: inline-block;
top: 1px;
}
.item-divider {
height: 24px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 12px;
top: 8px;
position: relative;
}

View File

@ -1,38 +0,0 @@
<div>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">
<div class="select log-select">
<select id="log_display_num" (change)="handleOnchange($event)">
<option value="10">{{'RECENT_LOG.SUB_TITLE' | translate}} 10 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="25">{{'RECENT_LOG.SUB_TITLE' | translate}} 25 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="50">{{'RECENT_LOG.SUB_TITLE' | translate}} 50 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
</select>
</div>
<div class="item-divider"></div>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></grid-filter>
<span (click)="refresh()" class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</span>
</div>
</div>
<div>
<clr-datagrid>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let l of recentLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,112 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuditLog } from './audit-log';
import { SessionUser } from '../shared/session-user';
import { AuditLogService } from './audit-log.service';
import { SessionService } from '../shared/session.service';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
@Component({
selector: 'recent-log',
templateUrl: './recent-log.component.html',
styleUrls: ['recent-log.component.css']
})
export class RecentLogComponent implements OnInit {
sessionUser: SessionUser = null;
recentLogs: AuditLog[];
logsCache: AuditLog[];
onGoing: boolean = false;
lines: number = 10; //Support 10, 25 and 50
currentTerm: string;
constructor(
private session: SessionService,
private msgService: MessageService,
private logService: AuditLogService) {
this.sessionUser = this.session.getCurrentUser();//Initialize session
}
ngOnInit(): void {
this.retrieveLogs();
}
handleOnchange($event: any) {
this.currentTerm = '';
if ($event && $event.target && $event.target["value"]) {
this.lines = $event.target["value"];
if (this.lines < 10) {
this.lines = 10;
}
this.retrieveLogs();
}
}
public get logNumber(): number {
return this.recentLogs?this.recentLogs.length:0;
}
public get inProgress(): boolean {
return this.onGoing;
}
public doFilter(terms: string): void {
if (terms.trim() === "") {
this.recentLogs = this.logsCache.filter(log => log.username != "");
return;
}
this.currentTerm = terms;
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
}
public refresh(): void {
this.retrieveLogs();
}
retrieveLogs(): void {
if (this.lines < 10) {
this.lines = 10;
}
this.onGoing = true;
this.logService.getRecentLogs(this.lines)
.subscribe(
response => {
this.onGoing = false;
this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
},
error => {
this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
}
);
}
isMatched(terms: string, log: AuditLog): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(log.username) ||
reg.test(log.repo_name) ||
reg.test(log.operation) ||
reg.test(log.repo_tag);
}
}

View File

@ -68,7 +68,7 @@ export class ListProjectComponent {
goToLink(proId: number): void { goToLink(proId: number): void {
this.searchTrigger.closeSearch(true); this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'projects', proId, 'repository']; let linkUrl = ['harbor', 'projects', proId, 'repositories'];
this.router.navigate(linkUrl); this.router.navigate(linkUrl);
} }

View File

@ -1,8 +1,15 @@
.option-left { .option-left {
padding-left: 16px; padding-left: 16px;
margin-top: 24px;
} }
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 18px; }
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
} }

View File

@ -1,15 +1,15 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top: 8px;">
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left"> <div class="flex-xs-middle option-left" style="position: relative; top: 10px;">
<button *ngIf="hasProjectAdminRole" class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button> <button *ngIf="hasProjectAdminRole" class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member> <add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div> </div>
<div class="flex-xs-middle option-right"> <div class="flex-xs-middle option-right">
<grid-filter filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)" [currentValue]="searchMember"></grid-filter> <hbr-filter [withDivider]="true" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)" [currentValue]="searchMember"></hbr-filter>
<a href="javascript:void(0)" (click)="refresh()"> <span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon> <clr-icon shape="refresh"></clr-icon>
</a> </span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,16 +5,16 @@
<nav class="subnav sub-nav-bg-color"> <nav class="subnav sub-nav-bg-color">
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a> <a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a> <a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a> <a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin"> <li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a> <a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -28,7 +28,11 @@ export class ProjectRoutingResolver implements Resolve<Project>{
private router: Router) { } private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
//Support both parameters and query parameters
let projectId = route.params['id']; let projectId = route.params['id'];
if (!projectId) {
projectId = route.queryParams['project_id'];
}
return this.projectService return this.projectService
.getProject(projectId) .getProject(projectId)
.toPromise() .toPromise()

View File

@ -4,10 +4,16 @@
.option-left { .option-left {
padding-left: 16px; padding-left: 16px;
margin-top: 12px;
} }
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 18px; }
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
} }

View File

@ -6,23 +6,23 @@
<statistics-panel></statistics-panel> <statistics-panel></statistics-panel>
</div> </div>
</div> </div>
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between" style="height:32px;">
<div class="option-left"> <div class="option-left">
<button *ngIf="projectCreationRestriction" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button> <button *ngIf="projectCreationRestriction" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project> <create-project (create)="createProject($event)"></create-project>
</div> </div>
<div class="option-right"> <div class="option-right">
<div class="select" style="float: left;"> <div class="select" style="float: left; left:-6px; top:8px;">
<select (change)="doFilterProjects($event)"> <select (change)="doFilterProjects($event)">
<option value="0" [selected]="currentFilteredType === 0">{{projectTypes[0] | translate}}</option> <option value="0" [selected]="currentFilteredType === 0">{{projectTypes[0] | translate}}</option>
<option value="1">{{projectTypes[1] | translate}}</option> <option value="1">{{projectTypes[1] | translate}}</option>
<option value="2">{{projectTypes[2] | translate}}</option> <option value="2">{{projectTypes[2] | translate}}</option>
</select> </select>
</div> </div>
<grid-filter filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)" [currentValue]="projectName"></grid-filter> <hbr-filter [withDivider]="true" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)" [currentValue]="projectName"></hbr-filter>
<a href="javascript:void(0)" (click)="refresh()"> <span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon> <clr-icon shape="refresh"></clr-icon>
</a> </span>
</div> </div>
</div> </div>
<list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)"></list-project> <list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)"></list-project>

View File

@ -1,4 +0,0 @@
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}

View File

@ -1,53 +0,0 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<div class="alert alert-warning" *ngIf="!editable">
<div class="alert-item">
<span class="alert-text">
{{'DESTINATION.CANNOT_EDIT' | translate}}
</span>
</div>
</div>
<form #targetForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="destination_name" class="col-md-4 form-group-label-override">{{ '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-left">
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" required (keyup)="changedTargetName($event)">
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override">{{ '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-left">
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required (keyup)="clearPassword($event)">
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.username" size="20" name="username" #username="ngModel" (keyup)="clearPassword($event)">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.password" size="20" name="password" #password="ngModel" (focus)="clearPassword($event)">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<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)="onCancel()" [disabled]="testOngoing">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="testOngoing || targetForm.form.invalid || !editable">{{ 'BUTTON.OK' | translate }}</button>
</div>
</clr-modal>

View File

@ -1,279 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ReplicationService } from '../replication.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ActionType } from '../../shared/shared.const';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { Target } from '../target';
import { TranslateService } from '@ngx-translate/core';
const FAKE_PASSWORD = 'rjGcfuRu';
@Component({
selector: 'create-edit-destination',
templateUrl: './create-edit-destination.component.html',
styleUrls: [ 'create-edit-destination.component.css' ]
})
export class CreateEditDestinationComponent implements AfterViewChecked {
modalTitle: string;
createEditDestinationOpened: boolean;
editable: boolean;
testOngoing: boolean;
pingTestMessage: string;
pingStatus: boolean;
actionType: ActionType;
target: Target = new Target();
initVal: Target = new Target();
targetForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('targetForm')
currentForm: NgForm;
hasChanged: boolean;
endpointHasChanged: boolean;
targetNameHasChanged: boolean;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Output() reload = new EventEmitter<boolean>();
constructor(
private replicationService: ReplicationService,
private messageHandlerService: MessageHandlerService,
private translateService: TranslateService) {}
openCreateEditTarget(editable: boolean, targetId?: number) {
this.target = new Target();
this.createEditDestinationOpened = true;
this.editable = editable;
this.hasChanged = false;
this.endpointHasChanged = false;
this.targetNameHasChanged = false;
this.pingTestMessage = '';
this.pingStatus = true;
this.testOngoing = false;
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;
this.initVal.name = this.target.name;
this.initVal.endpoint = this.target.endpoint;
this.initVal.username = this.target.username;
this.initVal.password = FAKE_PASSWORD;
this.target.password = this.initVal.password;
},
error=>this.messageHandlerService.handleError(error)
);
} else {
this.actionType = ActionType.ADD_NEW;
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res=>this.modalTitle=res);
}
}
testConnection() {
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = true;
this.testOngoing = !this.testOngoing;
let payload: Target = new Target();
if(this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
} else {
payload.id = this.target.id;
}
this.replicationService
.pingTarget(payload)
.subscribe(
response=>{
this.pingStatus = true;
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
},
error=>{
this.pingStatus = false;
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
}
)
}
changedTargetName($event: any) {
if(this.editable) {
this.targetNameHasChanged = true;
}
}
clearPassword($event: any) {
if(this.editable) {
this.target.password = '';
this.endpointHasChanged = true;
}
}
onSubmit() {
switch(this.actionType) {
case ActionType.ADD_NEW:
this.replicationService
.createTarget(this.target)
.subscribe(
response=>{
this.messageHandlerService.showSuccess('DESTINATION.CREATED_SUCCESS');
this.createEditDestinationOpened = false;
this.reload.emit(true);
},
error=>{
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=>{
if(this.messageHandlerService.isAppLevel(error)) {
this.messageHandlerService.handleError(error);
this.createEditDestinationOpened = false;
} else {
this.inlineAlert.showInlineError(res);
}
});
}
);
break;
case ActionType.EDIT:
if(!(this.targetNameHasChanged || this.endpointHasChanged)) {
this.createEditDestinationOpened = false;
return;
}
let payload: Target = new Target();
if(this.targetNameHasChanged) {
payload.name = this.target.name;
}
if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
}
this.replicationService
.updateTarget(payload, this.target.id)
.subscribe(
response=>{
this.messageHandlerService.showSuccess('DESTINATION.UPDATED_SUCCESS');
this.createEditDestinationOpened = false;
this.reload.emit(true);
},
error=>{
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=>{
if(this.messageHandlerService.isAppLevel(error)) {
this.messageHandlerService.handleError(error);
this.createEditDestinationOpened = false;
} else {
this.inlineAlert.showInlineError(res);
}
});
}
);
break;
}
}
onCancel() {
if(this.hasChanged) {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else {
this.createEditDestinationOpened = false;
this.targetForm.reset();
}
}
confirmCancel(confirmed: boolean) {
this.createEditDestinationOpened = false;
this.inlineAlert.close();
}
mappedName: {} = {
'targetName': 'name',
'endpointUrl': 'endpoint',
'username': 'username',
'password': 'password'
};
ngAfterViewChecked(): void {
this.targetForm = this.currentForm;
if(this.targetForm) {
this.targetForm.valueChanges.subscribe(data=>{
for(let i in data) {
let current = data[i];
let origin = this.initVal[this.mappedName[i]];
if(((this.actionType === ActionType.EDIT && this.editable && !current) || current) && current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
}
}
});
}
}
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-endpoint></hbr-endpoint>
</div>

View File

@ -11,14 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
export class CreateEditPolicy { import { Component } from '@angular/core';
policyId: number;
name: string; @Component({
description: string; selector: 'destination-page',
enable: boolean; templateUrl: 'destination-page.component.html'
targetId: number; })
targetName: string; export class DestinationPageComponent {
endpointUrl: string;
username: string;
password: string;
} }

View File

@ -1,8 +0,0 @@
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 36px;
}

View File

@ -1,33 +0,0 @@
<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="flex-items-xs-middle option-left">
<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="flex-items-xs-middle option-right">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></grid-filter>
<a href="javascript:void(0)" (click)="refreshTargets()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<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 *clrDgItems="let t of targets" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
<button class="action-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,157 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Target } from '../target';
import { ReplicationService } from '../replication.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { CreateEditDestinationComponent } from '../create-edit-destination/create-edit-destination.component';
@Component({
selector: 'destination',
templateUrl: 'destination.component.html',
styleUrls: ['./destination.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DestinationComponent implements OnInit {
@ViewChild(CreateEditDestinationComponent)
createEditDestinationComponent: CreateEditDestinationComponent;
targets: Target[];
target: Target;
targetName: string;
subscription: Subscription;
constructor(
private replicationService: ReplicationService,
private messageHandlerService: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService,
private ref: ChangeDetectorRef) {
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetId = message.data;
this.replicationService
.deleteTarget(targetId)
.subscribe(
response => {
this.messageHandlerService.showSuccess('DESTINATION.DELETED_SUCCESS');
this.reload(true);
},
error => {
if(error && error.status === 412) {
this.messageHandlerService.showError('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED', '');
} else {
this.messageHandlerService.handleError(error);
}
});
}
});
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
ngOnInit(): void {
this.targetName = '';
this.retrieve('');
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(targetName: string): void {
this.replicationService
.listTargets(targetName)
.subscribe(
targets => {
this.targets = targets || [];
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
},
error => this.messageHandlerService.handleError(error)
);
}
doSearchTargets(targetName: string) {
this.targetName = targetName;
this.retrieve(targetName);
}
refreshTargets() {
this.retrieve('');
}
reload($event: any) {
this.targetName = '';
this.retrieve('');
}
openModal() {
this.createEditDestinationComponent.openCreateEditTarget(true);
this.target = new Target();
}
editTarget(target: Target) {
if (target) {
let editable = true;
this.replicationService
.listTargetPolicies(target.id)
.subscribe(
policies=>{
if(policies && policies.length > 0) {
for(let i = 0; i < policies.length; i++){
let p = policies[i];
if(p.enabled === 1) {
editable = false;
break;
}
}
}
this.createEditDestinationComponent.openCreateEditTarget(editable, target.id);
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
},
error=>this.messageHandlerService.handleError(error)
);
}
}
deleteTarget(target: Target) {
if (target) {
let targetId = target.id;
let deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_TARGET',
'REPLICATION.DELETION_SUMMARY_TARGET',
target.name,
target.id,
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}
}

View File

@ -1,24 +0,0 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<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" [clrDgItem]='j'>
<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 | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time | date: 'short'}}</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>
{{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,44 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Job } from '../job';
import { State } from 'clarity-angular';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
@Component({
selector: 'list-job',
templateUrl: 'list-job.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListJobComponent {
@Input() jobs: Job[];
@Input() totalRecordCount: number;
@Input() totalPage: number;
@Output() paginate = new EventEmitter<State>();
constructor(
private messageHandlerService: MessageHandlerService,
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
pageOffset: number = 1;
refresh(state: State) {
if(this.jobs) {
this.paginate.emit(state);
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
/*
{
"id": 1,
"project_id": 1,
"project_name": "library",
"target_id": 1,
"target_name": "target_01",
"name": "sync_01",
"enabled": 0,
"description": "sync_01 desc.",
"cron_str": "",
"start_time": "0001-01-01T00:00:00Z",
"creation_time": "2017-02-24T06:41:52Z",
"update_time": "2017-02-24T06:41:52Z",
"error_job_count": 0,
"deleted": 0
}
*/
export class Policy {
id: number;
project_id: number;
project_name: string;
target_id: number;
target_name: string;
name: string;
enabled: number;
description: string;
cron_str: string;
start_time: Date;
creation_time: Date;
update_time: Date;
error_job_count: number;
deleted: number;
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-replication [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
</div>

View File

@ -11,26 +11,19 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
/* import { Component, OnInit } from '@angular/core';
{ import { ActivatedRoute } from '@angular/router';
"id": 1,
"status": "running",
"repository": "library/mysql",
"policy_id": 1,
"operation": "transfer",
"tags": null,
"creation_time": "2017-02-24T06:44:04Z",
"update_time": "2017-02-24T06:44:04Z"
}
*/ @Component({
export class Job { selector: 'replicaton',
id: number; templateUrl: 'replication-page.component.html'
status: string; })
repository: string; export class ReplicationPageComponent implements OnInit {
policy_id: number; projectIdentify: string | number;
operation: string;
tags: string; constructor(private route: ActivatedRoute) { }
creation_time: Date;
update_time: Date; ngOnInit(): void {
this.projectIdentify = +this.route.snapshot.parent.params['id'];
}
} }

View File

@ -1,17 +0,0 @@
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 18px;
}
.option-left-down {
margin-top: 36px;
}
.option-right-down {
padding-right: 16px;
margin-top: 24px;
}

View File

@ -1,62 +0,0 @@
<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="flex-xs-middle option-left">
<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="flex-xs-middle option-right">
<div class="select" style="float: left;">
<select (change)="doFilterPolicyStatus($event)">
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
</select>
</div>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)" [currentValue]="search.policyName"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOnePolicy($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></grid-filter>
<a href="javascript:void(0)" (click)="refreshJobs()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
<div class="select" style="float: left;">
<select (change)="doFilterJobStatus($event)">
<option *ngFor="let j of jobStatus" value="{{j.key}}" [selected]="currentJobStatus.key === j.key">{{j.description | translate}}</option>
</select>
</div>
<div class="flex-items-xs-middle">
<clr-icon shape="date"></clr-icon>
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="fromDateInput" type="date" #fromTime="ngModel" name="from" [(ngModel)]="search.startTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByStartTime(fromTime.value)">
<span *ngIf="fromTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
<clr-icon shape="date"></clr-icon>
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
<input id="toDateInput" type="date" #toTime="ngModel" name="to" [(ngModel)]="search.endTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByEndTime(toTime.value)">
<span *ngIf="toTimeInvalid" class="tooltip-content">
{{'AUDIT_LOG.INVALID_DATE' | translate }}
</span>
</label>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
</div>
</div>

View File

@ -1,274 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NgModel } from '@angular/forms';
import { CreateEditPolicyComponent } from '../shared/create-edit-policy/create-edit-policy.component';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { ReplicationService } from './replication.service';
import { SessionUser } from '../shared/session-user';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
import { State } from 'clarity-angular';
const ruleStatus = [
{ 'key': 'all', 'description': 'REPLICATION.ALL_STATUS'},
{ 'key': '1', 'description': 'REPLICATION.ENABLED'},
{ 'key': '0', 'description': 'REPLICATION.DISABLED'}
];
const jobStatus = [
{ 'key': 'all', '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: 'REPLICATION.ADVANCED', 1: 'REPLICATION.SIMPLE'};
class SearchOption {
policyId: number;
policyName: string = '';
repoName: string = '';
status: string = '';
startTime: string = '';
startTimestamp: string = '';
endTime: string = '';
endTimestamp: string = '';
page: number = 1;
pageSize: number = 5;
}
@Component({
selector: 'replicaton',
templateUrl: 'replication.component.html',
styleUrls: ['./replication.component.css']
})
export class ReplicationComponent implements OnInit {
projectId: number;
search: SearchOption = new SearchOption();
ruleStatus = ruleStatus;
currentRuleStatus: {key: string, description: string};
jobStatus = jobStatus;
currentJobStatus: {key: string, description: string};
changedPolicies: Policy[];
changedJobs: Job[];
initSelectedId: number;
policies: Policy[];
jobs: Job[];
jobsTotalRecordCount: number;
jobsTotalPage: number;
toggleJobSearchOption = optionalSearch;
currentJobSearchOption: number;
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent;
@ViewChild('fromTime') fromTimeInput: NgModel;
@ViewChild('toTime') toTimeInput: NgModel;
get fromTimeInvalid(): boolean {
return this.fromTimeInput.errors && this.fromTimeInput.errors.dateValidator && (this.fromTimeInput.dirty || this.fromTimeInput.touched);
}
get toTimeInvalid(): boolean {
return this.toTimeInput.errors && this.toTimeInput.errors.dateValidator && (this.toTimeInput.dirty || this.toTimeInput.touched);
}
constructor(
private messageHandlerService: MessageHandlerService,
private replicationService: ReplicationService,
private route: ActivatedRoute) {
}
ngOnInit(): void {
this.projectId = +this.route.snapshot.parent.params['id'];
this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
this.retrievePolicies();
let isCreate = this.route.snapshot.parent.queryParams['is_create'];
if (isCreate && <boolean>isCreate) {
this.openModal();
}
}
retrievePolicies(): void {
this.replicationService
.listPolicies(this.search.policyName, this.projectId)
.subscribe(
response=>{
this.changedPolicies = response || [];
if(this.changedPolicies && this.changedPolicies.length > 0) {
this.initSelectedId = this.changedPolicies[0].id;
}
this.policies = this.changedPolicies;
if(this.changedPolicies && this.changedPolicies.length > 0) {
this.search.policyId = this.changedPolicies[0].id;
this.fetchPolicyJobs();
}
},
error=>this.messageHandlerService.handleError(error)
);
}
openModal(): void {
this.createEditPolicyComponent.openCreateEditPolicy(true);
}
openEditPolicy(policy: Policy) {
if(policy) {
let editable = true;
if(policy.enabled === 1) {
editable = false;
}
this.createEditPolicyComponent.openCreateEditPolicy(editable, policy.id);
}
}
fetchPolicyJobs(state?: State) {
if(state) {
this.search.page = state.page.to + 1;
}
this.replicationService
.listJobs(this.search.policyId, this.search.status, this.search.repoName,
this.search.startTimestamp, this.search.endTimestamp, this.search.page, this.search.pageSize)
.subscribe(
response=>{
this.jobsTotalRecordCount = response.headers.get('x-total-count');
this.jobsTotalPage = Math.ceil(this.jobsTotalRecordCount / this.search.pageSize);
this.changedJobs = response.json();
this.jobs = this.changedJobs;
for(let i = 0; i < this.jobs.length; i++) {
let j = this.jobs[i];
if(j.status == 'retrying' || j.status == 'error') {
this.messageHandlerService.showError('REPLICATION.FOUND_ERROR_IN_JOBS', '');
break;
}
}
},
error=>this.messageHandlerService.handleError(error)
);
}
selectOnePolicy(policy: Policy) {
if(policy) {
this.search.policyId = policy.id;
this.search.repoName = '';
this.search.status = '';
this.currentJobSearchOption = 0;
this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' };
this.fetchPolicyJobs();
}
}
doSearchPolicies(policyName: string) {
this.search.policyName = policyName;
this.retrievePolicies();
}
doFilterPolicyStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentRuleStatus = this.ruleStatus.find(r=>r.key === status);
if(this.currentRuleStatus.key === 'all') {
this.changedPolicies = this.policies;
} else {
this.changedPolicies = this.policies.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
}
}
}
doFilterJobStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentJobStatus = this.jobStatus.find(r=>r.key === status);
if(this.currentJobStatus.key === 'all') {
status = '';
}
this.search.status = status;
this.doSearchJobs(this.search.repoName);
}
}
doSearchJobs(repoName: string) {
this.search.repoName = repoName;
this.fetchPolicyJobs();
}
reloadPolicies(isReady: boolean) {
if(isReady) {
this.search.policyName = '';
this.retrievePolicies();
}
}
refreshPolicies() {
this.retrievePolicies();
}
refreshJobs() {
this.fetchPolicyJobs();
}
toggleSearchJobOptionalName(option: number) {
(option === 1) ? this.currentJobSearchOption = 0 : this.currentJobSearchOption = 1;
}
convertDate(strDate: string): string {
if(/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/.test(strDate)) {
let parts = strDate.split(/[-\/]/);
strDate = parts[2] /*Year*/ + '-' +parts[1] /*Month*/ + '-' + parts[0] /*Date*/;
}
return strDate;
}
doJobSearchByStartTime(strDate: string) {
this.search.startTimestamp = '';
if(this.fromTimeInput.valid && strDate) {
strDate = this.convertDate(strDate);
this.search.startTimestamp = new Date(strDate).getTime() / 1000 + '';
}
this.fetchPolicyJobs();
}
doJobSearchByEndTime(strDate: string) {
this.search.endTimestamp = '';
if(this.toTimeInput.valid && strDate) {
strDate = this.convertDate(strDate);
let oneDayOffset = 3600 * 24;
this.search.endTimestamp = (new Date(strDate).getTime() / 1000 + oneDayOffset) + '';
}
this.fetchPolicyJobs();
}
}

View File

@ -13,16 +13,13 @@
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ReplicationManagementComponent } from './replication-management/replication-management.component';
import { ReplicationComponent } from './replication.component'; import { ReplicationManagementComponent } from './replication-management/replication-management.component';
import { ListJobComponent } from './list-job/list-job.component'; import { ReplicationPageComponent } from './replication-page.component';
import { TotalReplicationComponent } from './total-replication/total-replication.component'; import { TotalReplicationPageComponent } from './total-replication/total-replication-page.component';
import { DestinationComponent } from './destination/destination.component'; import { DestinationPageComponent } from './destination/destination-page.component';
import { CreateEditDestinationComponent } from './create-edit-destination/create-edit-destination.component';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { ReplicationService } from './replication.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -30,14 +27,15 @@ import { ReplicationService } from './replication.service';
RouterModule RouterModule
], ],
declarations: [ declarations: [
ReplicationComponent, ReplicationPageComponent,
ReplicationManagementComponent, ReplicationManagementComponent,
ListJobComponent, TotalReplicationPageComponent,
TotalReplicationComponent, DestinationPageComponent
DestinationComponent,
CreateEditDestinationComponent
], ],
exports: [ ReplicationComponent ], exports: [
providers: [ ReplicationService ] ReplicationPageComponent,
DestinationPageComponent,
TotalReplicationPageComponent
]
}) })
export class ReplicationModule { } export class ReplicationModule { }

View File

@ -1,179 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 { Injectable } from '@angular/core';
import { Http, URLSearchParams, Response } from '@angular/http';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/mergeMap';
@Injectable()
export class ReplicationService {
constructor(private http: Http) {}
listPolicies(policyName: string, projectId?: any): Observable<Policy[]> {
if(!projectId) {
projectId = '';
}
return this.http
.get(`/api/policies/replication?project_id=${projectId}&name=${policyName}`)
.map(response=>response.json() as Policy[])
.catch(error=>Observable.throw(error));
}
getPolicy(policyId: number): Observable<Policy> {
return this.http
.get(`/api/policies/replication/${policyId}`)
.map(response=>response.json() as Policy)
.catch(error=>Observable.throw(error));
}
createPolicy(policy: Policy): Observable<any> {
return this.http
.post(`/api/policies/replication`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
updatePolicy(policy: Policy): Observable<any> {
if (policy && policy.id) {
return this.http
.put(`/api/policies/replication/${policy.id}`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
return Observable.throw(new Error("Policy is nil or has no ID set."));
}
createOrUpdatePolicyWithNewTarget(policy: Policy, target: Target): Observable<any> {
return this.http
.post(`/api/targets`, JSON.stringify(target))
.map(response=>{
return response.status;
})
.catch(error=>Observable.throw(error))
.flatMap((status)=>{
if(status === 201) {
return this.http
.get(`/api/targets?name=${target.name}`)
.map(res=>res)
.catch(error=>Observable.throw(error));
}
})
.flatMap((res: Response) => {
if(res.status === 200) {
let lastAddedTarget= <Target>res.json()[0];
if(lastAddedTarget && lastAddedTarget.id) {
policy.target_id = lastAddedTarget.id;
if(policy.id) {
return this.http
.put(`/api/policies/replication/${policy.id}`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
} else {
return this.http
.post(`/api/policies/replication`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}
}
})
.catch(error=>Observable.throw(error));
}
enablePolicy(policyId: number, enabled: number): Observable<any> {
return this.http
.put(`/api/policies/replication/${policyId}/enablement`, {enabled: enabled})
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deletePolicy(policyId: number): Observable<any> {
return this.http
.delete(`/api/policies/replication/${policyId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
// /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=&repository=
listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = '', page: number, pageSize: number): Observable<any> {
return this.http
.get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}&page=${page}&page_size=${pageSize}`)
.map(response=>response)
.catch(error=>Observable.throw(error));
}
listTargets(targetName: string): Observable<Target[]> {
return this.http
.get(`/api/targets?name=${targetName}`)
.map(response=>response.json() as Target[])
.catch(error=>Observable.throw(error));
}
listTargetPolicies(targetId: number): Observable<Policy[]> {
return this.http
.get(`/api/targets/${targetId}/policies`)
.map(response=>response.json() as Policy[])
.catch(error=>Observable.throw(error));
}
getTarget(targetId: number): Observable<Target> {
return this.http
.get(`/api/targets/${targetId}`)
.map(response=>response.json() as Target)
.catch(error=>Observable.throw(error));
}
createTarget(target: Target): Observable<any> {
return this.http
.post(`/api/targets`, JSON.stringify(target))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
pingTarget(target: Target): Observable<any> {
if(target.id) {
return this.http
.post(`/api/targets/${target.id}/ping`, {})
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
return this.http
.post(`/api/targets/ping`, target)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
updateTarget(target: Target, targetId: number): Observable<any> {
return this.http
.put(`/api/targets/${targetId}`, JSON.stringify(target))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deleteTarget(targetId: number): Observable<any> {
return this.http
.delete(`/api/targets/${targetId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}

View File

@ -1,36 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
/*
{
"id": 1,
"endpoint": "http://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "Harbor12345",
"type": 0,
"creation_time": "2017-02-24T06:41:52Z",
"update_time": "2017-02-24T06:41:52Z"
}
*/
export class Target {
id: number;
endpoint: string;
name: string;
username: string;
password: string;
type: number;
creation_time: Date;
update_time: Date;
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-replication [withReplicationJob]='false'></hbr-replication>
</div>

View File

@ -11,11 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core'; import { Component } from '@angular/core';
export class MyMissingTranslationHandler implements MissingTranslationHandler { @Component({
handle(params: MissingTranslationHandlerParams) { selector: 'total-replication',
const missingText = "{Miss Harbor Text}"; templateUrl: 'total-replication-page.component.html'
return params.key || missingText; })
} export class TotalReplicationPageComponent {
} }

View File

@ -1,15 +0,0 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right option-right">
<div class="flex-items-xs-middle">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)" [currentValue]="policyName"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-policy [policies]="changedPolicies" [projectless]="true" (editOne)="openEditPolicy($event)" (selectOne)="selectPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
</div>

View File

@ -1,95 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ReplicationService } from '../../replication/replication.service';
import { CreateEditPolicyComponent } from '../../shared/create-edit-policy/create-edit-policy.component';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { Policy } from '../../replication/policy';
@Component({
selector: 'total-replication',
templateUrl: 'total-replication.component.html',
providers: [ ReplicationService ],
styleUrls: ['./total-replication.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TotalReplicationComponent implements OnInit {
changedPolicies: Policy[];
policies: Policy[];
policyName: string = '';
projectId: number;
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent;
constructor(
private replicationService: ReplicationService,
private messageHandlerService: MessageHandlerService,
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
ngOnInit() {
this.retrievePolicies();
}
retrievePolicies(): void {
this.replicationService
.listPolicies(this.policyName)
.subscribe(
response=>{
this.changedPolicies = response;
this.policies = this.changedPolicies;
},
error=>this.messageHandlerService.handleError(error)
);
}
doSearchPolicies(policyName: string) {
this.policyName = policyName;
this.retrievePolicies();
}
openEditPolicy(policy: Policy) {
if(policy) {
let editable = true;
if(policy.enabled === 1) {
editable = false;
}
this.createEditPolicyComponent.openCreateEditPolicy(editable, policy.id);
}
}
selectPolicy(policy: Policy) {
if(policy) {
this.projectId = policy.project_id;
}
}
refreshPolicies() {
this.retrievePolicies();
}
reloadPolicies(isReady: boolean) {
if(isReady) {
this.policyName = '';
this.retrievePolicies();
}
}
}

View File

@ -1,17 +0,0 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,65 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, Input, Output, EventEmitter, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Router } from '@angular/router';
import { Repository } from '../repository';
import { State } from 'clarity-angular';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListRepositoryComponent implements OnInit {
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() delete = new EventEmitter<string>();
@Output() paginate = new EventEmitter<State>();
@Input() hasProjectAdminRole: boolean;
pageOffset: number = 1;
constructor(
private router: Router,
private searchTrigger: SearchTriggerService,
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
ngOnInit() { }
deleteRepo(repoName: string) {
this.delete.emit(repoName);
}
refresh(state: State) {
if (this.repositories) {
this.paginate.emit(state);
}
}
public gotoLink(projectId: number, repoName: string): void {
this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'tags', projectId, repoName];
this.router.navigate(linkUrl);
}
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-repository-stackview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
</div>

View File

@ -0,0 +1,54 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project/project';
import { SessionService } from '../shared/session.service';
import { TagClickEvent } from 'harbor-ui';
@Component({
selector: 'repository',
templateUrl: 'repository-page.component.html'
})
export class RepositoryPageComponent implements OnInit {
projectId: number;
hasProjectAdminRole: boolean;
hasSignedIn: boolean;
projectName: string;
constructor(
private route: ActivatedRoute,
private session: SessionService,
private router: Router
) {
}
ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id'];
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
let pro: Project = <Project>resolverData['projectResolver'];
this.hasProjectAdminRole = pro.has_project_admin_role;
this.projectName = pro.name;
}
this.hasSignedIn = this.session.getCurrentUser() !== null;
}
watchTagClickEvent(tagEvt: TagClickEvent): void {
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
this.router.navigate(linkUrl);
}
}

View File

@ -1,5 +0,0 @@
.option-right {
padding-right: 16px;
margin-top: 32px;
margin-bottom: 12px;
}

View File

@ -1,13 +0,0 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle">
<grid-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></list-repository>
</div>
</div>

View File

@ -1,126 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { RepositoryService } from './repository.service';
import { Repository } from './repository';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
import { Project } from '../project/project';
@Component({
selector: 'repository',
templateUrl: 'repository.component.html',
styleUrls: ['./repository.component.css']
})
export class RepositoryComponent implements OnInit {
changedRepositories: Repository[];
projectId: number;
lastFilteredRepoName: string;
totalPage: number;
totalRecordCount: number;
hasProjectAdminRole: boolean;
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private repositoryService: RepositoryService,
private messageHandlerService: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService
) {
this.subscription = this.deletionDialogService
.confirmationConfirm$
.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let repoName = message.data;
this.repositoryService
.deleteRepository(repoName)
.subscribe(
response => {
this.refresh();
this.messageHandlerService.showSuccess('REPOSITORY.DELETED_REPO_SUCCESS');
},
error => this.messageHandlerService.handleError(error)
);
}
});
}
ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id'];
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.lastFilteredRepoName = '';
this.retrieve();
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(state?: State) {
this.repositoryService
.listRepositories(this.projectId, this.lastFilteredRepoName)
.subscribe(
response => {
this.changedRepositories = response.json();
},
error => this.messageHandlerService.handleError(error)
);
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.retrieve();
}
deleteRepo(repoName: string) {
let message = new ConfirmationMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName,
repoName,
ConfirmationTargets.REPOSITORY,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(message);
}
refresh() {
this.retrieve();
}
}

Some files were not shown because too many files have changed in this diff Show More