Merge pull request #5423 from ninjadq/integrate_chart_view

Feature of helm chart UI
This commit is contained in:
Steven Zou 2018-08-01 14:26:33 +08:00 committed by GitHub
commit 8b7706d191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 15562 additions and 84 deletions

2
.gitignore vendored
View File

@ -52,5 +52,3 @@ src/ui_ng/aot/**/*.json
**/aot
**/dist
**/.bin
package-lock.json
src/ui_ng/package-lock.json

View File

@ -79,7 +79,7 @@ script:
- sudo mkdir -p /harbor
- sudo mv ./VERSION /harbor/UIVERSION
- sudo service postgresql stop
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.6.0
- cat ./src/ui_ng/npm-ut-test-results
- sudo ./tests/testprepare.sh
- sudo make -f make/photon/Makefile _build_db _build_registry -e VERSIONTAG=dev -e CLAIRDBVERSION=dev -e REGISTRYVERSION=v2.6.2
@ -105,7 +105,7 @@ script:
- sudo rm -rf /data/config/*
- sudo rm -rf /data/database/*
- ls /data/cert
- sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 NOTARYFLAG=true CLAIRFLAG=true
- sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.6.0 NOTARYFLAG=true CLAIRFLAG=true
- sleep 10
- docker ps
- ./tests/validatecontainers.sh

View File

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

View File

@ -1,4 +1,4 @@
FROM node:7.5.0
FROM node:10.7.0
RUN mkdir -p /harbor_resources
RUN mkdir -p /harbor_src
@ -7,14 +7,26 @@ COPY src/ui_ng/package.json /harbor_resources
COPY make/dev/nodeclarity/entrypoint.sh /
# Install Chrome
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list
RUN apt-get update && apt-get -y install google-chrome-stable
RUN apt-get update && apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
hicolor-icon-theme \
libcanberra-gtk* \
libgl1-mesa-dri \
libgl1-mesa-glx \
libpango1.0-0 \
libpulse0 \
libv4l-0 \
--no-install-recommends
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i google-chrome-stable_current_amd64.deb; apt-get -fy install
RUN rm google-chrome-stable_current_amd64.deb
# Install npm package
WORKDIR /harbor_resources
RUN npm __proxy__ install -g @angular/cli && \
npm __proxy__ install && \
RUN npm __proxy__ install && \
chmod u+x /entrypoint.sh
VOLUME ["/harbor_src"]

View File

@ -1,6 +1,6 @@
{
"project": {
"version": "1.2.0",
"version": "1.6.0",
"name": "Harbor"
},
"apps": [{
@ -19,6 +19,7 @@
"styles": [
"../node_modules/clarity-icons/clarity-icons.min.css",
"../node_modules/clarity-ui/clarity-ui.min.css",
"../node_modules/prismjs/themes/prism-solarizedlight.css",
"styles.css"
],
"scripts": [
@ -26,7 +27,10 @@
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
"../node_modules/clarity-icons/clarity-icons.min.js",
"../node_modules/web-animations-js/web-animations.min.js"
"../node_modules/web-animations-js/web-animations.min.js",
"../node_modules/marked/lib/marked.js",
"../node_modules/prismjs/prism.js",
"../node_modules/prismjs/components/prism-yaml.min.js"
],
"environmentSource": "environments/environment.ts",
"environments": {

View File

@ -26,6 +26,15 @@ fi
cat ./package.json
npm install
## Build harbor-ui and link it
rm -rf /harbor_src/ui_ng/lib/dist
npm run build:lib
chmod -R +xr /harbor_src/ui_ng/lib/dist
cd /harbor_src/ui_ng/lib/dist
npm link
cd /harbor_src/ui_ng
npm link harbor-ui
./node_modules/.bin/ngc -p tsconfig-aot.json
sed -i 's/* as//g' src/app/shared/gauge/gauge.component.js
./node_modules/.bin/rollup -c rollup-config.js
@ -44,3 +53,8 @@ cp ./node_modules/@webcomponents/custom-elements/custom-elements.min.js ../ui/st
cp ./node_modules/clarity-icons/clarity-icons.min.js ../ui/static/
cp ./node_modules/clarity-ui/clarity-ui.min.css ../ui/static/
cp -r ./node_modules/clarity-icons/shapes/ ../ui/static/
cp ./node_modules/prismjs/themes/prism-solarizedlight.css ../ui/static/
cp ./node_modules/marked/lib/marked.js ../ui/static/
cp ./node_modules/prismjs/prism.js ../ui/static/
cp ./node_modules/prismjs/components/prism-yaml.min.js ../ui/static/

View File

@ -15,13 +15,18 @@
Loading...
</div>
</harbor-app>
<link rel="stylesheet" href="/static/clarity-ui.min.css">
<link rel="stylesheet" href="/static/clarity-icons.min.css">
<link rel="stylesheet" href="/static/prism-solarizedlight.css">
<link rel="stylesheet" href="/static/styles.css">
<script src="/static/mutationobserver.min.js"></script>
<script src="/static/custom-elements.min.js"></script>
<script src="/static/clarity-icons.min.js"></script>
<script src="/static/marked.js"></script>
<script src="/static//prism.js"></script>
<script src="/static/prism-yaml.min.js"></script>
<script src="/static/build.min.js"></script>
</body>

View File

@ -20,6 +20,7 @@
"styles": [
"../node_modules/clarity-icons/clarity-icons.min.css",
"../node_modules/clarity-ui/clarity-ui.min.css",
"../node_modules/prismjs/themes/prism-solarizedlight.css",
"styles.css"
],
"scripts": [
@ -27,7 +28,10 @@
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
"../node_modules/clarity-icons/clarity-icons.min.js",
"../node_modules/web-animations-js/web-animations.min.js"
"../node_modules/web-animations-js/web-animations.min.js",
"../node_modules/marked/lib/marked.js",
"../node_modules/prismjs/prism.js",
"../node_modules/prismjs/components/prism-yaml.min.js"
],
"environmentSource": "environments/environment.ts",
"environments": {

View File

@ -4,7 +4,8 @@
"entryFile": "index.ts",
"externals": {
"@ngx-translate/core": "ngx-translate-core",
"@ngx-translate/core/index": "ngx-translate-core"
"@ngx-translate/core/index": "ngx-translate-core",
"ngx-markdown": "ngx-markdown"
}
}
}

View File

@ -38,6 +38,7 @@
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
"ngx-markdown": "^1.5.1",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",
"web-animations-js": "^2.2.1",

View File

@ -42,10 +42,11 @@ export class ConfirmationDialogComponent {
open(msg: ConfirmationMessage): void {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.message = msg;
this.translate.get(this.dialogTitle).subscribe((res: string) => this.dialogTitle = res);
this.translate.get(this.dialogContent, { 'param': msg.param }).subscribe((res: string) => this.dialogContent = res);
this.translate.get(msg.message, { 'param': msg.param }).subscribe((res: string) => {
this.dialogContent = res;
});
// Open dialog
this.buttons = msg.buttons;
this.opened = true;

View File

@ -25,10 +25,10 @@ import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/index'
import { HBR_GRIDVIEW_DIRECTIVES } from './gridview/index';
import { REPOSITORY_GRIDVIEW_DIRECTIVES } from './repository-gridview/index';
import { OPERATION_DIRECTIVES } from './operation/index';
import {LABEL_DIRECTIVES} from "./label/index";
import {CREATE_EDIT_LABEL_DIRECTIVES} from "./create-edit-label/index";
import {LABEL_PIECE_DIRECTIVES} from "./label-piece/index";
import { LABEL_DIRECTIVES } from "./label/index";
import { CREATE_EDIT_LABEL_DIRECTIVES } from "./create-edit-label/index";
import { LABEL_PIECE_DIRECTIVES } from "./label-piece/index";
import { HELMCHART_DIRECTIVE } from "./helm-chart/index";
import {
SystemInfoService,
SystemInfoDefaultService,
@ -52,6 +52,8 @@ import {
ProjectDefaultService,
LabelService,
LabelDefaultService,
HelmChartService,
HelmChartDefaultService
} from './service/index';
import {
ErrorHandler,
@ -90,7 +92,9 @@ export const DefaultServiceConfig: IServiceConfig = {
localI18nMessageVariableMap: {},
configurationEndpoint: "/api/configurations",
scanJobEndpoint: "/api/jobs/scan",
labelEndpoint: "/api/labels"
labelEndpoint: "/api/labels",
helmChartEndpoint: "/api/chartrepo",
downloadChartEndpoint: "/chartrepo"
};
/**
@ -138,6 +142,9 @@ export interface HarborModuleConfig {
// Service implementation for label
labelService?: Provider;
// Service implementation for helmchart
helmChartService?: Provider;
}
/**
@ -184,7 +191,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
LABEL_PIECE_DIRECTIVES,
HBR_GRIDVIEW_DIRECTIVES,
REPOSITORY_GRIDVIEW_DIRECTIVES,
OPERATION_DIRECTIVES
OPERATION_DIRECTIVES,
HELMCHART_DIRECTIVE
],
exports: [
LOG_DIRECTIVES,
@ -210,7 +218,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
LABEL_PIECE_DIRECTIVES,
HBR_GRIDVIEW_DIRECTIVES,
REPOSITORY_GRIDVIEW_DIRECTIVES,
OPERATION_DIRECTIVES
OPERATION_DIRECTIVES,
HELMCHART_DIRECTIVE
],
providers: []
})
@ -233,6 +242,7 @@ export class HarborLibraryModule {
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
config.helmChartService || {provide: HelmChartService, useClass: HelmChartDefaultService},
// Do initializing
TranslateServiceInitializer,
{
@ -264,6 +274,7 @@ export class HarborLibraryModule {
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
config.helmChartService || {provide: HelmChartService, useClass: HelmChartDefaultService},
ChannelService,
OperationService
]

View File

@ -0,0 +1,20 @@
<div class="row flex-items-xs-center dep-container">
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th class="left">{{'HELM_CHART.NAME' | translate}}</th>
<th class="left">{{'HELM_CHART.VERSION' | translate}}</th>
<th class="left">{{'HELM_CHART.REPO' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let dep of dependencies">
<td class="left">{{dep.name}}</td>
<td class="left">{{dep.version}}</td>
<td class="left"><a href="{{dep.repository}}">{{dep.repository}}</a></td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,3 @@
.dep-container {
margin-top: 30px;
}

View File

@ -0,0 +1,24 @@
import {
Component,
OnInit,
Input,
ChangeDetectionStrategy
} from "@angular/core";
import { HelmChartDependency } from "./../../service/interface";
@Component({
selector: "hbr-chart-detail-dependency",
templateUrl: "./chart-detail-dependency.component.html",
styleUrls: ["./chart-detail-dependency.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartDetailDependencyComponent implements OnInit {
@Input() dependencies: HelmChartDependency;
constructor() {}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,100 @@
<div class="row">
<div class="col-md-12">
<p>{{summary.description}}</p>
</div>
</div>
<div class="row">
<div class="col-md-8 md-container">
<div *ngIf="readme" class="md-div" [innerHTML]="readme | markdown"></div>
<div *ngIf="!readme">{{'HELM_CHART.NO_README' | translate}}</div>
</div>
<div class="col-md-4 summary-container">
<div class="col-md-12 content-group">
<div>
<label>{{'HELM_CHART.OVERVIEW' | translate }}</label>
</div>
<table class="table">
<tbody>
<tr>
<td class="left">{{'HELM_CHART.HOME' | translate }}</td>
<td class="left">
<a href="{{summary.home}}">{{summary.home}}</a>
</td>
</tr>
<tr>
<td class="left">{{'HELM_CHART.SRC_REPO' | translate }}</td>
<td class="left">
<a href="{{summary.sources}}">{{summary.sources}}</a>
</td>
</tr>
<tr>
<td class="left">{{'HELM_CHART.CREATED' | translate }}</td>
<td class="left">{{summary.created | date}}</td>
</tr>
<tr *ngFor="let maintainer of summary.maintainers; let i = index">
<td class="left" *ngIf="i === 0">{{'HELM_CHART.MAINTAINERS' | translate }}</td>
<td class="left" *ngIf="i !== 0"></td>
<td class="left">
<a href="mailto:{{maintainer.email}}">{{maintainer.name}}</a>
</td>
</tr>
<tr>
<td class="left">{{'HELM_CHART.VERSION' | translate }}</td>
<td class="left">{{ summary.appVersion }}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12 content-group">
<div>
<label>{{'HELM_CHART.ADD_REPO' | translate }}</label>
</div>
<table class="table">
<tbody>
<tr>
<td class="left">{{addCMD}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12 content-group">
<div>
<label>{{'HELM_CHART.INSTALL_CHART' | translate }}</label>
</div>
<table class="table">
<tbody>
<tr>
<td class="left">{{installCMD}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12 content-group">
<div>
<label>{{'HELM_CHART.SECURITY' | translate }}</label>
</div>
<table class="table">
<tbody>
<tr>
<td class="left">{{'HELM_CHART.SIGNED' | translate }}</td>
<div *ngIf="security?.signature?.signed;then signed_content else unsignd_content"></div>
<ng-template #signed_content>
<td class="left">
<span class="content-icon">
<clr-icon shape="shield-check" class="is-success"></clr-icon>
</span>&nbsp;{{'HELM_CHART.SIGNED' | translate }}</td>
</ng-template>
<ng-template #unsignd_content>
<td class="left">
<span class="content-icon">
<clr-icon shape="shield-x" class="is-error"></clr-icon>
</span>&nbsp;{{'HELM_CHART.UNSIGNED' | translate }}</td>
</ng-template>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
.md-container {
margin-top: 15px;
border: solid 1px #DDDDDD;
padding: 3px;
}
.summary-container {
margin-top: 15px;
table {
background-color: #F2F2F2;
margin-top: 0.5rem;
}
.content-group {
margin-bottom: 30px;
}
.content-icon {
margin-right: 6px;
}
}

View File

@ -0,0 +1,38 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input
} from "@angular/core";
import { HelmChartMetaData, HelmChartSecurity } from "./../../service/interface";
@Component({
selector: "hbr-chart-detail-summary",
templateUrl: "./chart-detail-summary.component.html",
styleUrls: ["./chart-detail-summary.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartDetailSummaryComponent implements OnInit {
@Input() summary: HelmChartMetaData;
@Input() security: HelmChartSecurity;
@Input() repoURL: string;
@Input() projectName: string;
@Input() chartName: string;
@Input() chartVersion: string;
@Input() readme: string;
constructor() {}
ngOnInit(): void {
}
public get addCMD() {
return `helm repo add REPO_NAME ${this.repoURL}/chartrepo/${this.projectName}`;
}
public get installCMD() {
return `helm install --version ${this.chartVersion} REPO_NAME/${this.chartName}`;
}
}

View File

@ -0,0 +1,34 @@
<div class="row flex-items-xs-right">
<div class="swichy-container">
<span class="card-btn" (click)="showYamlFile(false)" (mouseenter)="mouseEnter('value') " (mouseleave)="mouseLeave('value')">
<clr-icon size="36" shape="view-list" title='list values'
[ngClass]="{'is-highlight': isValueMode || isHovering('value') }" ></clr-icon>
</span>
<span class="list-btn" (click)="showYamlFile(true)" (mouseenter)="mouseEnter('yaml') " (mouseleave)="mouseLeave('yaml')">
<clr-icon size="36" shape="file" title="yaml file"
[ngClass]="{'is-highlight': !isValueMode || isHovering('yaml') }"></clr-icon>
</span>
</div>
</div>
<div class="row value-container">
<div class="col-xs-8" *ngIf="valueMode">
<div>
<label>{{'HELM_CHART.SHOW_KV' | translate }}</label>
</div>
<table class="table">
<tbody>
<tr *ngFor="let key of objKeys(values)">
<td class="left">{{key}}</td>
<td class="left">{{values[key]}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-xs-8" *ngIf="!valueMode">
<div>
<label>{{'HELM_CHART.SHOW_YAML' | translate }}</label>
</div>
<div class="yaml-container" [innerHTML]="yaml | language : 'yaml' | markdown"></div>
</div>
</div>

View File

@ -0,0 +1,15 @@
.value-container {
::ng-deep pre {
min-height: fit-content;
}
}
.swichy-container {
margin-top: 3px;
margin-right: 15px;
}
pre {
max-height: max-content;
padding-left: 21px;
}

View File

@ -0,0 +1,61 @@
import {
Component,
Input,
OnInit,
ChangeDetectionStrategy
} from "@angular/core";
@Component({
selector: "hbr-chart-detail-value",
templateUrl: "./chart-detail-value.component.html",
styleUrls: ["./chart-detail-value.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartDetailValueComponent implements OnInit {
@Input() values;
@Input() yaml;
// Default set to yaml file
valueMode = false;
valueHover = false;
yamlHover = true;
objKeys = Object.keys;
constructor() {}
ngOnInit(): void {
}
public get isValueMode() {
return this.valueMode;
}
isHovering(view: string) {
if (view === 'value') {
return this.valueHover ? true : false;
} else {
return this.yamlHover ? true : false;
}
}
showYamlFile(showYaml: boolean) {
this.valueMode = !showYaml;
}
mouseEnter(mode: string) {
if (mode === "value") {
this.valueHover = true;
} else {
this.yamlHover = true;
}
}
mouseLeave(mode: string) {
if (mode === "value") {
this.valueHover = false;
} else {
this.yamlHover = false;
}
}
}

View File

@ -0,0 +1,58 @@
<div>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<div class="title-container">
<div class="chart-name">
{{chartNameWithVersion | translate}}
</div>
<div>
{{roleName | translate}}
</div>
</div>
</div>
<div class="col-xs-1">
<button class="btn btn-sm btn-secondary"
(click)="downloadChart()">{{'HELM_CHART.DOWNLOAD' | translate}}</button>
</div>
</div>
<div class="detail-loading" *ngIf="loading">
<span class="spinner">
Loading...
</span>
</div>
<div *ngIf="!loading && isChartExist">
<clr-tabs>
<clr-tab>
<button clrTabLink id="summary-link">{{'HELM_CHART.SUMMARY' | translate}}</button>
<clr-tab-content id="summary-content" *clrIfActive>
<hbr-chart-detail-summary
[summary]="chartDetail.metadata"
[chartName]="chartName"
[repoURL]="repoURL"
[projectName]="project.name"
[chartVersion]="chartVersion"
[security]="chartDetail.security"
[readme]="chartDetail.files['README.md']"></hbr-chart-detail-summary>
</clr-tab-content>
</clr-tab>
<clr-tab>
<button clrTabLink id="depend-link">{{'HELM_CHART.DEPENDENCIES' | translate}}</button>
<clr-tab-content id="depend-content">
<hbr-chart-detail-dependency [dependencies]='chartDetail.dependencies'></hbr-chart-detail-dependency>
</clr-tab-content>
</clr-tab>
<clr-tab>
<button clrTabLink id="value-link">{{'HELM_CHART.VALUES' | translate}}</button>
<clr-tab-content id="value-content">
<hbr-chart-detail-value
[values]="chartDetail.values"
[yaml]="chartDetail.files['values.yaml']"></hbr-chart-detail-value>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
<div *ngIf="!loading && !isChartExist">
<h6>{{'HELM_CHART.NO_DETAIL' | translate }}</h6>
</div>
</div>

View File

@ -0,0 +1,20 @@
.title-container {
display: flex;
.chart-name {
border-right: 1px solid gray;
font-size: 27px;
font-weight: normal;
padding-right: 9px;
margin-right: 9px;
}
}
.detail-loading {
position: absolute;
top: 0;
left: 0;
right:0;
bottom:0;
width: 108px !important;
height: 108px !important;
}

View File

@ -0,0 +1,102 @@
import { Project } from "./../../project-policy-config/project";
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input,
ChangeDetectorRef
} from "@angular/core";
import { downloadFile, toPromise } from "../../utils";
import { SystemInfoService, HelmChartService } from "../../service/index";
import { HelmChartDetail, SystemInfo } from "./../../service/interface";
import { ErrorHandler } from "./../../error-handler/error-handler";
@Component({
selector: "hbr-chart-detail",
templateUrl: "./chart-detail.component.html",
styleUrls: ["./chart-detail.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartDetailComponent implements OnInit {
@Input() projectId: number;
@Input() project: Project;
@Input() chartName: string;
@Input() chartVersion: string;
@Input() roleName: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
loading = true;
isMember = false;
chartDetail: HelmChartDetail;
systemInfo: SystemInfo;
repoURL = "";
constructor(
private errorHandler: ErrorHandler,
private systemInfoService: SystemInfoService,
private helmChartService: HelmChartService,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
.then(systemInfo => {
let scheme = 'http://';
this.systemInfo = systemInfo;
if (this.systemInfo.has_ca_root) {
scheme = 'https://';
}
this.repoURL = `${scheme}${this.systemInfo.registry_url}`;
})
.catch(error => this.errorHandler.error(error));
this.refresh();
}
public get chartNameWithVersion() {
return `${this.chartName}:${this.chartVersion}`;
}
public get isChartExist() {
return this.chartDetail ? true : false;
}
refresh() {
this.loading = true;
this.helmChartService
.getChartDetail(this.project.name, this.chartName, this.chartVersion)
.finally(() => {
this.loading = false;
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
})
.subscribe(
chartDetail => {
this.chartDetail = chartDetail;
},
err => {
this.errorHandler.error(err);
}
);
}
downloadChart() {
if (!this.chartDetail ||
!this.chartDetail.metadata ||
!this.chartDetail.metadata.urls ||
this.chartDetail.metadata.urls.length < 1) {
return;
}
let filename = this.chartDetail.metadata.urls[0];
this.helmChartService.downloadChart(this.project.name, filename).subscribe(
res => {
downloadFile(res);
},
error => {
this.errorHandler.error(error);
},
);
}
}

View File

@ -0,0 +1,100 @@
<div>
<div class="row chart-tool">
<div class="toolbar">
<div class="row flex-items-xs-right option-right rightPos">
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'HELM_CHART.FILTER_FOR_CHARTS' | translate}}" [currentValue]="lastFilteredChartName"></hbr-filter>
<span class="card-btn" (click)="showCard(true)" (mouseenter)="mouseEnter('card') " (mouseleave)="mouseLeave('card')">
<clr-icon [ngClass]="{'is-highlight': isCardView || isHovering('card') }" shape="view-cards"></clr-icon>
</span>
<span class="list-btn" (click)="showCard(false)" (mouseenter)="mouseEnter('list') " (mouseleave)="mouseLeave('list')">
<clr-icon [ngClass]="{'is-highlight': !isCardView || isHovering('list') }" shape="view-list"></clr-icon>
</span>
<span class="filter-divider"></span>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div *ngIf="!isCardView" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="refresh($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRows">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasProjectAdminRole" (click)="onChartUpload($event)">
<clr-icon shape="upload" size="16"></clr-icon>&nbsp;{{'HELM_CHART.UPLOAD' | translate}}
</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'HELM_CHART.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.VERSIONS' | translate}}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.CREATED' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'HELM_CHART.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let chart of charts" [clrDgItem]="chart">
<clr-dg-cell>
<a href="javascript:void(0)" (click)="onChartClick(chart)">{{ chart.name }}</a>
</clr-dg-cell>
<clr-dg-cell>{{ chart.total_versions }}</clr-dg-cell>
<clr-dg-cell>{{ chart.created | date }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'HELM_CHART.OF' | translate}} {{pagination.totalItems}} {{'HELM_CHART.ITEMS'
| translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<div *ngIf="isCardView" class="row card-container">
<div *ngFor="let item of charts;" class="col-lg-3 col-md-4 col-sm-6">
<a let i=index; class="card clickable" (click)="onChartClick(item)">
<div class="card-header">
<div class="card-media-block wrap">
<div class="card-media-description">
<span class="card-media-title">{{item.name}}</span>
<p class="card-media-text">{{item.home}}</p>
</div>
</div>
</div>
<div class="card-block">
<div class="form-group">
<label>{{'HELM_CHART.VERSIONS' | translate}}</label>
<div>{{item.total_versions}}</div>
</div>
<div class="form-group">
<label>{{'HELM_CHART.CREATED' | translate}}</label>
<div>{{item.Created | date}}</div>
</div>
</div>
</a>
</div>
<div *ngIf="loading" [ngClass]="{'central-block-loading': isFirstPage, 'central-block-loading-more': !isFirstPage}">
<span class="vertical-helper"></span>
<div class="spinner"></div>
</div>
</div>
<clr-modal [(clrModalOpen)]="isUploadModalOpen" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'HELM_CHART.UPLOAD_TITLE' | translate}}</h3>
<div class="modal-body">
<form #chartUploadForm="ngForm" enctype="multipart/form-data" (ngSubmit)="upload()">
<section class="form-block">
<div class="form-group">
<label for="chart"> {{'HELM_CHART.CHART_FILE' | translate}} </label>
<input type="file" id="chart" name="chart" ngModel (change)="onChartFileChangeEvent($event)">
</div>
<div class="form-group">
<label for="prov"> {{'HELM_CHART.CHART_PROV' | translate}} </label>
<input type="file" id="prov" name="prov" ngModel (change)="onProvFileChangeEvent($event)">
</div>
</section>
<button type="submit" class="btn btn-secondary" [disabled]="isUploading">
<span *ngIf="!isUploading">{{'HELM_CHART.UPLOAD' | translate}}</span>
<span *ngIf="isUploading" class="spinner spinner-inline">
Loading...
</span>
</button>
</form>
</div>
</clr-modal>
</div>

View File

@ -0,0 +1,68 @@
.chart-tool {
position: relative;
.toolbar {
overflow: hidden;
.rightPos {
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
.filter-divider {
display: inline-block;
height: 16px;
width: 2px;
background-color: #cccccc;
padding-top: 12px;
padding-bottom: 12px;
position: relative;
top: 9px;
margin-right: 6px;
margin-left: 6px;
}
}
}
}
.card-container {
margin-top: 21px;
.card-header {
.card-media-block {
.card-media-description {
height: 45px;
p {
margin-top: 0;
}
.card-media-title {
overflow: hidden;
height: 24px;
}
.card-media-text {
overflow: hidden;
height: 21px
}
}
}
}
.card-block {
margin-top: 24px;
min-height: 100px;
.form-group {
display: flex;
label {
width: 100px;
}
}
}
}
.vertical-helper {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.spinner {
width: 100px;
height: 100px;
vertical-align: middle;
}

View File

@ -0,0 +1,171 @@
import {
Component,
Input,
OnInit,
ChangeDetectionStrategy,
Output,
EventEmitter,
ChangeDetectorRef
} from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { State } from "clarity-angular";
import { SystemInfo, SystemInfoService, HelmChartItem } from "../service/index";
import { ErrorHandler } from "../error-handler/error-handler";
import { toPromise, DEFAULT_PAGE_SIZE } from "../utils";
import { HelmChartService } from "../service/helm-chart.service";
@Component({
selector: "hbr-helm-chart",
templateUrl: "./helm-chart.component.html",
styleUrls: ["./helm-chart.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelmChartComponent implements OnInit {
signedCon: { [key: string]: any | string[] } = {};
@Input() projectId: number;
@Input() projectName = "unknown";
@Input() urlPrefix: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Output() chartClickEvt = new EventEmitter<any>();
@Output() chartDownloadEve = new EventEmitter<string>();
lastFilteredChartName: string;
charts: HelmChartItem[] = [];
chartsCopy: HelmChartItem[] = [];
systemInfo: SystemInfo;
selectedRows: HelmChartItem[] = [];
loading = true;
// For Upload
isUploading = false;
isUploadModalOpen = false;
provFile: File;
chartFile: File;
// For View swtich
isCardView: boolean;
cardHover = false;
listHover = false;
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage = 1;
totalCount = 0;
currentState: State;
constructor(
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private systemInfoService: SystemInfoService,
private helmChartService: HelmChartService,
private cdr: ChangeDetectorRef,
) {}
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : "";
}
ngOnInit(): void {
// Get system info for tag views
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
.then(systemInfo => (this.systemInfo = systemInfo))
.catch(error => this.errorHandler.error(error));
this.lastFilteredChartName = "";
this.refresh();
}
refresh() {
this.loading = true;
this.helmChartService
.getHelmCharts(this.projectName)
.finally(() => {
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 3000);
this.loading = false;
})
.subscribe(
charts => {
this.charts = charts;
this.chartsCopy = charts.map(x => Object.assign({}, x));
},
err => {
this.errorHandler.error(err);
}
);
}
onChartClick(item: HelmChartItem) {
this.chartClickEvt.emit(item.name);
}
onChartUpload() {
this.isUploadModalOpen = true;
}
upload() {
if (!this.chartFile && !this.provFile) {
return;
}
if (this.isUploading) { return; };
this.isUploading = true;
this.helmChartService
.uploadChart(this.projectName, this.chartFile, this.provFile)
.finally(() => {
this.isUploading = false;
this.isUploadModalOpen = false;
this.refresh();
})
.subscribe(() => {
this.translateService
.get("HELM_CHART.FILE_UPLOADED")
.subscribe(res => this.errorHandler.info(res));
},
err => this.errorHandler.error(err)
);
}
onChartFileChangeEvent(event) {
if (event.target.files && event.target.files.length > 0) {
this.chartFile = event.target.files[0];
}
}
onProvFileChangeEvent(event) {
if (event.target.files && event.target.files.length > 0) {
this.provFile = event.target.files[0];
}
}
showCard(cardView: boolean) {
if (this.isCardView === cardView) {
return;
}
this.isCardView = cardView;
}
mouseEnter(itemName: string) {
if (itemName === "card") {
this.cardHover = true;
} else {
this.listHover = true;
}
}
mouseLeave(itemName: string) {
if (itemName === "card") {
this.cardHover = false;
} else {
this.listHover = false;
}
}
isHovering(itemName: string) {
if (itemName === "card") {
return this.cardHover;
} else {
return this.listHover;
}
}
}

View File

@ -0,0 +1,23 @@
import { Type } from '@angular/core';
import { HelmChartComponent } from './helm-chart.component';
import { ChartVersionComponent } from './versions/helm-chart-version.component';
import { ChartDetailComponent } from './chart-detail/chart-detail.component';
import { ChartDetailSummaryComponent } from './chart-detail/chart-detail-summary.component';
import { ChartDetailDependencyComponent } from './chart-detail/chart-detail-dependency.component';
import { ChartDetailValueComponent } from './chart-detail/chart-detail-value.component';
export * from "./helm-chart.component";
export * from "./versions/helm-chart-version.component";
export * from "./chart-detail/chart-detail.component";
export * from "./chart-detail/chart-detail-summary.component";
export * from "./chart-detail/chart-detail-dependency.component";
export * from "./chart-detail/chart-detail-value.component";
export const HELMCHART_DIRECTIVE: Type<any>[] = [
HelmChartComponent,
ChartVersionComponent,
ChartDetailComponent,
ChartDetailSummaryComponent,
ChartDetailDependencyComponent,
ChartDetailValueComponent,
];

View File

@ -0,0 +1,136 @@
<div>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<div class="title-container">
<div class="chart-name-span">
{{chartName | translate}}
</div>
<div>
{{roleName | translate}}
</div>
</div>
</div>
</div>
<div class="row version-tool">
<div class="toolbar">
<div class="row flex-items-xs-right option-right rightPos">
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'HELM_CHART.FILTER_FOR_CHARTS' | translate}}" [currentValue]="lastFilteredVersionName"></hbr-filter>
<span class="card-btn" (click)="showCard(true)" (mouseenter)="mouseEnter('card') " (mouseleave)="mouseLeave('card')">
<clr-icon [ngClass]="{'is-highlight': isCardView || isHovering('card') }" shape="view-cards"></clr-icon>
</span>
<span class="list-btn" (click)="showCard(false)" (mouseenter)="mouseEnter('list') " (mouseleave)="mouseLeave('list')">
<clr-icon [ngClass]="{'is-highlight': !isCardView || isHovering('list') }" shape="view-list"></clr-icon>
</span>
<span class="filter-divider"></span>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div *ngIf="!isCardView" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="refresh($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRows">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary"
[disabled]="!hasProjectAdminRole"
(click)="versionUpload($event)">
<clr-icon shape="upload" size="16"></clr-icon>&nbsp;{{'HELM_CHART.UPLOAD' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary"
[disabled]="!(selectedRows.length===1)"
(click)="versionDownload()">
<clr-icon shape="download" size="16"></clr-icon>&nbsp;{{'HELM_CHART.DOWNLOAD' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary"
[disabled]="selectedRows.length<=0 || !hasProjectAdminRole"
(click)="openVersionDeleteModal(selectedRows)">
<clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'BUTTON.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'HELM_CHART.VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.ENGINE' | translate }}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.MAINTAINERS' | translate }}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.CREATED' | translate }}</clr-dg-column>
<clr-dg-row *ngFor="let v of chartVersions" [clrDgItem]="v">
<clr-dg-cell>
<span class="list-img"><img [src]="getImgLink(v)"/></span>
<a href="javascript:void(0)" (click)="onVersionClick(v)">{{ v.version }}</a>
</clr-dg-cell>
<clr-dg-cell>{{ v.engine }}</clr-dg-cell>
<clr-dg-cell>{{ getMaintainerString(v.maintainers) }}</clr-dg-cell>
<clr-dg-cell>{{ v.created | date}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'HELM_CHART.OF' | translate}} {{pagination.totalItems}} {{'HELM_CHART.VERSIONS'
| translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<div *ngIf="isCardView" class="row card-container">
<div *ngFor="let item of chartVersions;" class="col-lg-3 col-md-4 col-sm-6">
<a let i=index; class="card clickable" (click)="onVersionClick(item)">
<div class="card-header">
<div class="card-media-block">
<img [src]="getImgLink(item)"/>
<div class="card-media-description">
<span class="card-media-title">{{item.name}}</span>
<p class="card-media-text">{{item.home}}</p>
</div>
</div>
</div>
<div class="card-block">
<div class="form-group">
<label>{{'HELM_CHART.ENGINE' | translate}}</label>
<div>{{item.engine}}</div>
</div>
<div class="form-group">
<label>{{'HELM_CHART.MAINTAINERS' | translate}}</label>
<div>{{getMaintainerString(item.maintainers)}}</div>
</div>
<div class="form-group">
<label>{{'HELM_CHART.VERSION' | translate}}</label>
<div>{{item.appVersion}}</div>
</div>
</div>
<div class="card-footer">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button type="button" class="btn btn-link" (click)="versionDownload($event, item)">{{'HELM_CHART.DOWNLOAD' | translate}}</button>
<button type="button" class="btn btn-link" (click)="openVersionDeleteModal([item])">{{'BUTTON.DELETE' | translate}}</button>
</clr-dropdown>
</div>
</a>
</div>
<div *ngIf="loading" [ngClass]="{'central-block-loading': isFirstPage, 'central-block-loading-more': !isFirstPage}">
<span class="vertical-helper"></span>
<div class="spinner"></div>
</div>
</div>
<clr-modal [(clrModalOpen)]="isUploadModalOpen" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'HELM_CHART.UPLOAD_TITLE' | translate}}</h3>
<div class="modal-body">
<form #chartUploadForm="ngForm" enctype="multipart/form-data" (ngSubmit)="upload()">
<section class="form-block">
<div class="form-group">
<label for="chart"> {{'HELM_CHART.CHART_FILE' | translate}} </label>
<input type="file" id="chart" name="chart" ngModel (change)="onChartFileChangeEvent($event)">
</div>
<div class="form-group">
<label for="prov"> {{'HELM_CHART.CHART_PROV' | translate}} </label>
<input type="file" id="prov" name="prov" ngModel (change)="onProvFileChangeEvent($event)">
</div>
</section>
<button type="submit" class="btn btn-secondary" [disabled]="isUploading">
<span *ngIf="!isUploading">{{'HELM_CHART.UPLOAD' | translate}}</span>
<span *ngIf="isUploading" class="spinner spinner-inline">
Loading...
</span>
</button>
</form>
</div>
</clr-modal>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>

View File

@ -0,0 +1,96 @@
.title-container {
display: flex;
.chart-name-span {
border-right: 1px solid gray;
font-size: 27px;
font-weight: normal;
padding-right: 9px;
margin-right: 9px;
}
}
.version-tool {
position: relative;
.toolbar {
overflow: hidden;
.rightPos {
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
.filter-divider {
display: inline-block;
height: 16px;
width: 2px;
background-color: #cccccc;
padding-top: 12px;
padding-bottom: 12px;
position: relative;
top: 9px;
margin-right: 6px;
margin-left: 6px;
}
}
}
}
.card-container {
margin-top: 21px;
.card-header {
.card-media-block {
img {
height: 45px;
width: 45px;
}
.card-media-description {
height: 45px;
p {
margin-top: 0;
}
.card-media-title {
overflow: hidden;
height: 24px;
}
.card-media-text {
overflow: hidden;
height: 21px
}
}
}
}
.card-block {
margin-top: 24px;
min-height: 100px;
.form-group {
display: flex;
label {
width: 100px;
}
}
margin-top: 0px;
}
.card-footer {
padding-top: 6px;
padding-bottom: 6px;
}
}
.list-img {
img {
height: 24px;
width: 24px;
margin-right: 12px;
}
}
.vertical-helper {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.spinner {
width: 100px;
height: 100px;
vertical-align: middle;
}

View File

@ -0,0 +1,298 @@
import {
Component,
Input,
OnInit,
ViewChild,
ChangeDetectionStrategy,
ChangeDetectorRef,
Output,
EventEmitter
} from "@angular/core";
import { NgForm } from "@angular/forms";
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/forkJoin";
import { TranslateService } from "@ngx-translate/core";
import { State } from "clarity-angular";
import {
SystemInfo,
SystemInfoService,
HelmChartVersion,
HelmChartMaintainer
} from "./../../service/index";
import { ErrorHandler } from "./../../error-handler/error-handler";
import { toPromise, DEFAULT_PAGE_SIZE, downloadFile } from "../../utils";
import { OperationService } from "./../../operation/operation.service";
import { HelmChartService } from "./../../service/helm-chart.service";
import { ConfirmationAcknowledgement, ConfirmationDialogComponent, ConfirmationMessage } from "./../../confirmation-dialog";
import {
OperateInfo,
OperationState,
operateChanges
} from "./../../operation/operate";
import {
ConfirmationButtons,
ConfirmationTargets,
ConfirmationState,
DefaultHelmIcon
} from "../../shared/shared.const";
@Component({
selector: "hbr-helm-chart-version",
templateUrl: "./helm-chart-version.component.html",
styleUrls: ["./helm-chart-version.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartVersionComponent implements OnInit {
signedCon: { [key: string]: any | string[] } = {};
@Input() projectName: string;
@Input() chartName: string;
@Input() roleName: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Output() versionClickEvt = new EventEmitter<string>();
@Output() backEvt = new EventEmitter<any>();
lastFilteredVersionName: string;
chartVersions: HelmChartVersion[] = [];
versionsCopy: HelmChartVersion[] = [];
systemInfo: SystemInfo;
selectedRows: HelmChartVersion[] = [];
loading = true;
isCardView: boolean;
cardHover = false;
listHover = false;
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage = 1;
totalCount = 0;
currentState: State;
isUploading = false;
isUploadModalOpen = false;
chartFile: File;
provFile: File;
@ViewChild("confirmationDialog")
confirmationDialog: ConfirmationDialogComponent;
@ViewChild("chartUploadForm") form: NgForm;
constructor(
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private systemInfoService: SystemInfoService,
private helmChartService: HelmChartService,
private cdr: ChangeDetectorRef,
private operationService: OperationService,
) {}
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : "";
}
ngOnInit(): void {
// Get system info for tag views
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
.then(systemInfo => (this.systemInfo = systemInfo))
.catch(error => this.errorHandler.error(error));
this.refresh();
this.lastFilteredVersionName = "";
}
refresh() {
this.loading = true;
this.helmChartService
.getChartVersions(this.projectName, this.chartName)
.finally(() => {
this.loading = false;
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
})
.subscribe(
versions => {
this.chartVersions = versions;
this.versionsCopy = versions.map(x => Object.assign({}, x));
},
err => {
if (err.status && err.status === 404) {
this.backEvt.emit();
}
this.errorHandler.error(err);
}
);
}
getMaintainerString(maintainers: HelmChartMaintainer[]) {
if (!maintainers || maintainers.length < 1) {
return "";
}
let maintainer_string = maintainers[0].name;
if (maintainers.length > 1) {
maintainer_string = `${maintainer_string} (${maintainers.length - 1} others)`;
}
return maintainer_string;
}
onVersionClick(version: HelmChartVersion) {
this.versionClickEvt.emit(version.version);
}
deleteVersion(version: HelmChartVersion): Observable<any> {
// init operation info
let operateMsg = new OperateInfo();
operateMsg.name = "OPERATION.DELETE_CHART_VERSION";
operateMsg.data.id = version.digest;
operateMsg.state = OperationState.progressing;
operateMsg.data.name = `${version.name}:${version.version}`;
this.operationService.publishInfo(operateMsg);
return this.helmChartService
.deleteChartVersion(this.projectName, this.chartName, version.version)
.map(
() => operateChanges(operateMsg, OperationState.success),
err => operateChanges(operateMsg, OperationState.failure, err)
);
}
deleteVersions(versions: HelmChartVersion[]) {
if (versions && versions.length < 1) { return; }
let versionObs = versions.map(v => this.deleteVersion(v));
Observable.forkJoin(versionObs).finally(() => this.refresh()).subscribe();
}
versionDownload(item?: HelmChartVersion) {
let selectedVersion: HelmChartVersion;
if (item) {
selectedVersion = item;
} else {
// return if selected version less then 1
if (this.selectedRows.length < 1) {
return;
}
selectedVersion = this.selectedRows[0];
}
if (!selectedVersion) {
return;
}
let filename = selectedVersion.urls[0];
this.helmChartService.downloadChart(this.projectName, filename).subscribe(
res => {
downloadFile(res);
},
error => {
this.errorHandler.error(error);
}
);
}
versionUpload() {
this.isUploadModalOpen = true;
}
showCard(cardView: boolean) {
if (this.isCardView === cardView) {
return;
}
this.isCardView = cardView;
}
mouseEnter(itemName: string) {
if (itemName === "card") {
this.cardHover = true;
} else {
this.listHover = true;
}
}
mouseLeave(itemName: string) {
if (itemName === "card") {
this.cardHover = false;
} else {
this.listHover = false;
}
}
isHovering(itemName: string) {
if (itemName === "card") {
return this.cardHover;
} else {
return this.listHover;
}
}
upload() {
if (!this.chartFile && !this.provFile) {
return;
}
if (this.isUploading) { return; };
this.isUploading = true;
this.helmChartService
.uploadChart(this.projectName, this.chartFile, this.provFile)
.finally(() => {
this.isUploading = false;
this.isUploadModalOpen = false;
this.refresh();
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 3000);
})
.subscribe(
() => {
this.translateService.get("HELM_CHART.FILE_UPLOADED")
.subscribe(res => this.errorHandler.info(res));
},
err => this.errorHandler.error(err)
);
}
onChartFileChangeEvent(event) {
if (event.target.files && event.target.files.length > 0) {
this.chartFile = event.target.files[0];
}
}
onProvFileChangeEvent(event) {
if (event.target.files && event.target.files.length > 0) {
this.provFile = event.target.files[0];
}
}
openVersionDeleteModal(versions: HelmChartVersion[]) {
let versionNames = versions.map(v => v.name).join(",");
this.translateService.get("HELM_CHART.DELETE_CHART_VERSION").subscribe(key => {
let message = new ConfirmationMessage(
"HELM_CHART.DELETE_CHART_VERSION_TITLE",
key,
versionNames,
versions,
ConfirmationTargets.HELM_CHART,
ConfirmationButtons.DELETE_CANCEL
);
this.confirmationDialog.open(message);
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
});
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (
message &&
message.source === ConfirmationTargets.HELM_CHART &&
message.state === ConfirmationState.CONFIRMED
) {
let versions = message.data;
this.deleteVersions(versions);
}
}
getImgLink(v: HelmChartVersion) {
if (v.icon) {
return v.icon;
} else {
return DefaultHelmIcon;
}
}
}

View File

@ -26,3 +26,4 @@ export * from './gridview/index';
export * from './repository-gridview/index';
export * from './operation/index';
export * from './_animations/index';
export * from './helm-chart/index';

View File

@ -47,8 +47,15 @@
</div>
</div>
<hbr-gridview *ngIf="isCardView" #gridView style="position:relative;" [items]="repositories" [loading]="loading" [pageSize]="pageSize"
[currentPage]="currentPage" [totalCount]="totalCount" [expectScrollPercent]="90" [withAdmiral]="withAdmiral" (loadNextPageEvent)="loadNextPage()">
<hbr-gridview *ngIf="isCardView" #gridView style="position:relative;"
[items]="repositories"
[loading]="loading"
[pageSize]="pageSize"
[currentPage]="currentPage"
[totalCount]="totalCount"
[expectScrollPercent]="90"
[withAdmiral]="withAdmiral"
(loadNextPageEvent)="loadNextPage()">
<ng-template let-item="item">
<a class="card clickable" (click)="watchRepoClickEvt(item)">
<div class="card-header">
@ -84,7 +91,7 @@
<button *ngIf="withAdmiral" type="button" class="btn btn-link" clrDropdownItem (click)="itemAddInfoEvent($event, item)" [disabled]="!hasProjectAdminRole">
{{'REPOSITORY.ADDITIONAL_INFO' | translate}}
</button>
<button type="button" class="btn btn-link" clrDropdownItem (click)="deleteItemEvent($event, item)" [disabled]="!hasProjectAdminRole">
<button type="button" class="btn btn-link" clrDropdownItem (click)="deleteItemEvent($event, [item])" [disabled]="!hasProjectAdminRole">
{{'REPOSITORY.DELETE' | translate}}
</button>
</clr-dropdown-menu>

View File

@ -220,4 +220,22 @@ export interface IServiceConfig {
* @memberOf IServiceConfig
*/
labelEndpoint?: string;
/**
* The base endpoint of the service used to handle the helm chart.
* helm charts related endpoints will be built based on this endpoint.
* E.g:
* If the base endpoint is '/api/helmcharts',
* the helm chart endpoint will be '/api/helmcharts/:id'.
*
* @type {string}
* @memberOf IServiceConfig
*/
helmChartEndpoint?: string;
/**
* The base endpoint of the chart download url
* @type {string}
*/
downloadChartEndpoint?: string;
}

View File

@ -0,0 +1,236 @@
import { Injectable, Inject } from "@angular/core";
import { Http, Response, ResponseContentType } from "@angular/http";
import "rxjs/add/observable/of";
import { Observable } from "rxjs/Observable";
import { RequestQueryParams } from "./RequestQueryParams";
import { HelmChartItem, HelmChartVersion, HelmChartDetail } from "./interface";
import { SERVICE_CONFIG, IServiceConfig } from "../service.config";
import { HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS } from "../utils";
/**
* Define service methods for handling the helmchart related things.
* Loose couple with project module.
*
* @export
* @abstract
* @class RepositoryService
*/
export abstract class HelmChartService {
/**
* Get all helm charts info
* @param projectName Id of the project
* @param queryParams options params for query data
*/
abstract getHelmCharts(
projectName: string,
queryParams?: RequestQueryParams
): Observable<HelmChartItem[]>;
/**
* Delete an helmchart
* @param projectId Id of the project
* @param chartId ID of helmChart in this specific project
*/
abstract deleteHelmChart(projectId: number | string, chartId: number): Observable<any>;
/**
* Get all the versions of helmchart
* @param projectName Id of the project
* @param chartName ID of the helm chart
* @param queryParams option params for query
*/
abstract getChartVersions(
projectName: string,
chartName: string,
): Observable<HelmChartVersion[]>;
/**
* Delete a version of helmchart
* @param projectName ID of the project
* @param chartName ID of the chart you want to delete
* @param version name of the version
*/
abstract deleteChartVersion(projectName: string, chartName: string, version: string): Observable<any>;
/**
* Get the all details of an helmchart
* @param projectName ID of the project
* @param chartname ID of the chart
* @param version name of the chart's version
* @param queryParams options
*/
abstract getChartDetail(
projectName: string,
chartname: string,
version: string,
): Observable<HelmChartDetail>;
/**
* Download an specific verison
* @param projectName ID of the project
* @param filename ID of the helm chart
* @param version Name of version
* @param queryParams options
*/
abstract downloadChart(
projectName: string,
filename: string,
): Observable<any>;
/**
* Upload chart and prov files to chartmuseam
* @param projectName Name of the project
* @param chart chart file
* @param prov prov file
*/
abstract uploadChart (
projectName: string,
chart: File,
prov: File
): Observable<any>
}
/**
* Implement default service for helm chart.
*/
@Injectable()
export class HelmChartDefaultService extends HelmChartService {
constructor(
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig
) {
super();
}
private extractData(res: Response) {
if (res.text() === "") {
return [];
}
return res.json() || [];
}
private extractHelmItems(res: Response) {
if (res.text() === "") {
return [];
}
let charts = res.json();
if (charts) {
return charts.map( chart => {
return {
name: chart.Name,
total_versions: chart.total_versions,
created: chart.Created,
icon: chart.Icon,
home: chart.Home};
});
} else {
return [];
}
}
private handleErrorObservable(error: Response | any) {
console.error(error.message || error);
return Observable.throw(error.message || error);
}
public getHelmCharts(
projectName: string,
): Observable<HelmChartItem[]> {
if (!projectName) {
return Observable.throw("Bad argument, No project id to get helm charts");
}
return this.http
.get(`${this.config.helmChartEndpoint}/${projectName}/charts`, HTTP_GET_OPTIONS)
.map(response => {
return this.extractHelmItems(response);
})
.catch(error => {
return this.handleErrorObservable(error);
});
}
public deleteHelmChart(projectId: number | string, chartId: number): any {
if (!chartId) {
Observable.throw("Bad argument");
}
return this.http
.delete(`${this.config.helmChartEndpoint}/${projectId}/${chartId}`)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
public getChartVersions(
projectName: string,
chartName: string,
): Observable<HelmChartVersion[]> {
return this.http.get(`${this.config.helmChartEndpoint}/${projectName}/charts/${chartName}`, HTTP_GET_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
public deleteChartVersion(projectName: string, chartName: string, version: string): any {
return this.http.delete(`${this.config.helmChartEndpoint}/${projectName}/charts/${chartName}/${version}`, HTTP_JSON_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
public getChartDetail (
projectName: string,
chartName: string,
version: string,
): Observable<HelmChartDetail> {
return this.http.get(`${this.config.helmChartEndpoint}/${projectName}/charts/${chartName}/${version}`)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
public downloadChart(
projectName: string,
filename: string,
): Observable<any> {
return this.http.get(`${this.config.downloadChartEndpoint}/${projectName}/${filename}`, {
responseType: ResponseContentType.Blob,
})
.map(response => {
return {
filename: filename.split('/')[1],
data: response.blob()
};
})
.catch(this.handleErrorObservable);
}
public uploadChart(
projectName: string,
chart?: File,
prov?: File
): Observable<any> {
let formData = new FormData();
let uploadURL = `${this.config.helmChartEndpoint}/${projectName}/charts`;
if (chart) {
formData.append('chart', chart);
}
if (prov) {
formData.append('prov', prov);
if (!chart) {
uploadURL = `${this.config.helmChartEndpoint}/${projectName}/prov`;
}
}
return this.http.post(uploadURL, formData)
.map(reponse => this.extractData(reponse))
.catch(this.handleErrorObservable);
}
}

View File

@ -11,3 +11,4 @@ export * from './configuration.service';
export * from './job-log.service';
export * from './project.service';
export * from './label.service';
export * from './helm-chart.service';

View File

@ -296,3 +296,80 @@ export interface ScrollPosition {
sT: number;
cH: number;
}
export interface HelmChartItem {
name: string;
total_versions: number;
created: string;
icon: string;
home: string;
status?: string;
pulls?: number;
maintainer?: string;
}
export interface HelmChartVersion {
name: string;
home: string;
sources: string[];
version: string;
description: string;
keywords: string[];
maintainers: HelmChartMaintainer[];
engine: string;
icon: string;
appVersion: string;
urls: string[];
created: string;
digest: string;
}
export interface HelmChartDetail {
metadata: HelmChartMetaData;
dependencies: HelmChartDependency[];
values: any;
files: HelmchartFile;
security: HelmChartSecurity;
}
export interface HelmChartMetaData {
name: string;
home: string;
sources: string[];
version: string;
description: string;
keywords: string[];
maintainers: HelmChartMaintainer[];
engine: string;
icon: string;
appVersion: string;
urls: string[];
created?: string;
digest: string;
}
export interface HelmChartMaintainer {
name: string;
email: string;
}
export interface HelmChartDependency {
name: string;
version: string;
repository: string;
}
export interface HelmchartFile {
"README.MD": string;
"values.yaml": string;
}
export interface HelmChartSecurity {
signature: HelmChartSignature;
}
export interface HelmChartSignature {
signed: boolean;
prov_file: string;
}

View File

@ -40,7 +40,8 @@ export const enum ConfirmationTargets {
TAG,
CONFIG,
CONFIG_ROUTE,
CONFIG_TAB
CONFIG_TAB,
HELM_CHART
};
export const enum ActionType {
@ -87,3 +88,7 @@ export const LabelColor = [
{ 'color': '#F52F52', 'textColor': 'black' }, { 'color': '#FF5501', 'textColor': 'black' },
{ 'color': '#F57600', 'textColor': 'black' }, { 'color': '#FFDC0B', 'textColor': 'black' },
];
export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' };
export const DefaultHelmIcon = '/static/images/helm-logo.svg';

View File

@ -5,6 +5,7 @@ import { ClarityModule } from 'clarity-angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule, TranslateLoader, MissingTranslationHandler } from '@ngx-translate/core';
import { CookieService, CookieModule } from 'ngx-cookie';
import { MarkdownModule } from 'ngx-markdown';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { ClipboardModule } from '../third-party/ngx-clipboard/index';
@ -46,6 +47,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
ClipboardModule,
CookieModule.forRoot(),
ClarityModule.forRoot(),
MarkdownModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@ -63,9 +65,10 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
HttpModule,
FormsModule,
ReactiveFormsModule,
CookieModule,
ClipboardModule,
ClarityModule,
CookieModule,
MarkdownModule,
TranslateModule,
],
providers: [CookieService]

View File

@ -45,27 +45,3 @@ export const errorHandler = function (error: any): string {
}
}
};
export class CancelablePromise<T> {
constructor(promise: Promise<T>) {
this.wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
this.isCanceled ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
this.isCanceled ? reject({isCanceled: true}) : reject(error)
);
});
}
private wrappedPromise: Promise<T>;
private isCanceled: boolean;
getPromise(): Promise<T> {
return this.wrappedPromise;
}
cancel() {
this.isCanceled = true;
}
}

View File

@ -57,6 +57,12 @@ export const HTTP_GET_OPTIONS: RequestOptions = new RequestOptions({
})
});
export const FILE_UPLOAD_OPTION: RequestOptions = new RequestOptions({
headers: new Headers({
"Content-Type": 'multipart/form-data',
})
});
/**
* Build http request options
*
@ -288,3 +294,15 @@ export function clone(srcObj: any): any {
if (!srcObj) { return null; };
return JSON.parse(JSON.stringify(srcObj));
}
export function downloadFile(fileData) {
let url = window.URL.createObjectURL(fileData.data);
let a = document.createElement("a");
document.body.appendChild(a);
a.setAttribute("style", "display: none");
a.href = url;
a.download = fileData.filename;
a.click();
window.URL.revokeObjectURL(url);
a.remove();
};

13292
src/ui_ng/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"scripts": {
"start": "ng serve --ssl 1 --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json",
"lint": "tslint \"src/**/*.ts\"",
"lint:lib": "tslint \"lib/**/*.ts\"",
"lint:lib": "tslint \"lib/**/*.ts\" -e \"lib/dist/**/*\" ",
"test": "ng test --single-run",
"pree2e": "webdriver-manager update",
"e2e": "protractor",
@ -35,6 +35,7 @@
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
"ngx-markdown": "1.5.2",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",
"web-animations-js": "^2.2.1",

View File

@ -21,7 +21,11 @@ export default {
plugins: [
nodeResolve({jsnext: true, module: true, browser: true}),
commonjs({
include: ['node_modules/**'],
namedExports: {
'node_modules/ngx-markdown/dist/lib/index.js': ['MarkdownModule']
},
include: ['node_modules/**',
'node_modules/ngx-markdown/**'],
}),
uglify()
]

View File

@ -18,6 +18,7 @@ import { HttpModule } from '@angular/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from 'clarity-angular';
import { CookieModule } from 'ngx-cookie';
import { MarkdownModule } from 'ngx-markdown';
@NgModule({
imports: [
@ -26,6 +27,7 @@ import { CookieModule } from 'ngx-cookie';
HttpModule,
ClarityModule.forRoot(),
CookieModule.forRoot(),
MarkdownModule.forRoot(),
BrowserAnimationsModule
],
exports: [
@ -33,7 +35,8 @@ import { CookieModule } from 'ngx-cookie';
FormsModule,
HttpModule,
ClarityModule,
BrowserAnimationsModule
BrowserAnimationsModule,
MarkdownModule
]
})
export class CoreModule {

View File

@ -47,6 +47,9 @@ import { MemberComponent } from './project/member/member.component';
import {ProjectLabelComponent} from "./project/project-label/project-label.component";
import { ProjectConfigComponent } from './project/project-config/project-config.component';
import { ProjectRoutingResolver } from './project/project-routing-resolver.service';
import { ListChartsComponent } from './project/list-charts/list-charts.component';
import { ListChartVersionsComponent } from './project/list-chart-versions/list-chart-versions.component';
import { ChartDetailComponent } from './project/chart-detail/chart-detail.component';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -116,6 +119,22 @@ const harborRoutes: Routes = [
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/helm-charts/:chart/versions',
component: ListChartVersionsComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/helm-charts/:chart/versions/:version',
component: ChartDetailComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
@ -128,6 +147,10 @@ const harborRoutes: Routes = [
path: 'repositories',
component: RepositoryPageComponent
},
{
path: 'helm-charts',
component: ListChartsComponent
},
{
path: 'repositories/:repo/tags',
component: TagRepositoryComponent,

View File

@ -0,0 +1,16 @@
<div>
<div class="breadcrumb">
<a (click)="gotoProjectList()"> {{ 'SIDE_NAV.PROJECTS'| translate}} </a>
&lt;
<a (click)="gotoChartList()">{{ 'HELM_CHART.HELMCHARTS'| translate}}</a>
&lt;
<a (click)="gotoChartVersion()">{{ 'HELM_CHART.CHARTVERSIONS'| translate}}</a>
</div>
<hbr-chart-detail
[projectId]="projectId"
[project]="project"
[chartName]="chartName"
[chartVersion]="chartVersion"
[roleName]="roleName"
></hbr-chart-detail>
</div>

View File

@ -0,0 +1,6 @@
.breadcrumb a {
text-decoration: none;
cursor: pointer;
color: #007cbb;
font-size: 12px;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChartDetailComponent } from './chart-detail.component';
describe('ChartDetailComponent', () => {
let component: ChartDetailComponent;
let fixture: ComponentFixture<ChartDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChartDetailComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,55 @@
import { RoleMapping } from './../../shared/shared.const';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, OnInit } from "@angular/core";
import { Project } from '../project';
import { SessionService } from './../../shared/session.service';
import { SessionUser } from './../../shared/session-user';
@Component({
selector: "project-chart-detail",
templateUrl: "./chart-detail.component.html",
styleUrls: ["./chart-detail.component.scss"]
})
export class ChartDetailComponent implements OnInit {
projectId: number | string;
project: Project;
chartName: string;
chartVersion: string;
currentUser: SessionUser;
hasProjectAdminRole: boolean;
roleName: string;
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService
) {}
ngOnInit() {
// Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.params['id'];
this.chartName = this.route.snapshot.params['chart'];
this.chartVersion = this.route.snapshot.params['version'];
// Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser();
let resolverData = this.route.snapshot.data;
if (resolverData) {
this.project = <Project>(resolverData["projectResolver"]);
this.roleName = RoleMapping[this.project.role_name];
this.hasProjectAdminRole = this.project.has_project_admin_role;
}
}
gotoProjectList() {
this.router.navigateByUrl("/harbor/projects");
}
gotoChartList() {
this.router.navigateByUrl(`/harbor/projects/${this.projectId}/helm-charts`);
}
gotoChartVersion() {
this.router.navigateByUrl(`/harbor/projects/${this.projectId}/helm-charts/${this.chartName}/versions`);
}
}

View File

@ -0,0 +1,16 @@
<div>
<div class="breadcrumb">
<a href="javascript:void(0)" (click)="gotoProjectList()"> {{ 'SIDE_NAV.PROJECTS'| translate}} </a>
&lt;
<a href="javascript:void(0)" (click)="gotoChartList()">{{ 'HELM_CHART.HELMCHARTS'| translate}}</a>
</div>
<hbr-helm-chart-version
[projectName]='projectName'
[chartName]='chartName'
[roleName]='roleName'
[hasSignedIn]='hasSignedIn'
[hasProjectAdminRole]='hasProjectAdminRole'
(versionClickEvt)='onVersionClick($event)'
(backEvt)='gotoChartList()'>
</hbr-helm-chart-version>
</div>

View File

@ -0,0 +1,6 @@
.breadcrumb a {
text-decoration: none;
cursor: pointer;
color: #007cbb;
font-size: 12px;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListChartVersionsComponent } from './list-chart-versions.component';
describe('ListChartVersionsComponent', () => {
let component: ListChartVersionsComponent;
let fixture: ComponentFixture<ListChartVersionsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ListChartVersionsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListChartVersionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,60 @@
import { Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { Project } from './../project';
import { SessionUser } from './../../shared/session-user';
import { SessionService } from './../../shared/session.service';
import { RoleMapping } from '../../shared/shared.const';
@Component({
selector: 'list-chart-version',
templateUrl: './list-chart-versions.component.html',
styleUrls: ['./list-chart-versions.component.scss']
})
export class ListChartVersionsComponent implements OnInit {
loading = false;
projectId: number;
projectName: string;
chartName: string;
roleName: string;
hasSignedIn: boolean;
hasProjectAdminRole: boolean;
currentUser: SessionUser;
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService) {}
ngOnInit() {
// Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.params['id'];
this.chartName = this.route.snapshot.params['chart'];
// Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser();
let resolverData = this.route.snapshot.data;
if (resolverData) {
let project = <Project>(resolverData["projectResolver"]);
this.hasProjectAdminRole = project.has_project_admin_role;
this.roleName = RoleMapping[project.role_name];
this.projectName = project.name;
}
}
onVersionClick(version: string) {
this.router.navigateByUrl(`${this.router.url}/${version}`);
}
gotoProjectList() {
this.router.navigateByUrl('/harbor/projects');
}
gotoChartList() {
this.router.navigateByUrl(`/harbor/projects/${this.projectId}/helm-charts`);
}
}

View File

@ -0,0 +1,8 @@
<hbr-helm-chart
[projectId]='projectId'
[projectName]='projectName'
[urlPrefix]='urlPrefix'
[hasSignedIn]='hasSignedIn'
[hasProjectAdminRole]='hasProjectAdminRole'
(chartClickEvt)='onChartClick($event)'>
</hbr-helm-chart>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListChartsComponent } from './list-charts.component';
describe('ListChartsComponent', () => {
let component: ListChartsComponent;
let fixture: ComponentFixture<ListChartsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ListChartsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListChartsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,44 @@
import { Project } from '../../project/project';
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from '@angular/router';
import { SessionService } from './../../shared/session.service';
import { SessionUser } from './../../shared/session-user';
@Component({
selector: "project-list-charts",
templateUrl: "./list-charts.component.html",
styleUrls: ["./list-charts.component.scss"]
})
export class ListChartsComponent implements OnInit {
projectId: number;
projectName: string;
urlPrefix: string;
hasSignedIn: boolean;
hasProjectAdminRole: boolean;
currentUser: SessionUser;
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService) {}
ngOnInit() {
// Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.parent.params["id"];
// Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser();
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
let project = <Project>(resolverData["projectResolver"]);
this.projectName = project.name;
this.hasProjectAdminRole = project.has_project_admin_role;
}
}
onChartClick(chartName: string) {
this.router.navigateByUrl(`${this.router.url}/${chartName}/versions`);
}
}

View File

@ -1,7 +1,10 @@
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewProject()" *ngIf="projectCreationRestriction"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'PROJECT.NEW_PROJECT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length && (isSystemAdmin || canDelete))" (click)="deleteProjects(selectedRow)" ><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'PROJECT.DELETE' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewProject()" *ngIf="projectCreationRestriction">
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'PROJECT.NEW_PROJECT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length && (isSystemAdmin || canDelete))"
(click)="deleteProjects(selectedRow)">
<clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="accessLevelComparator">{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
@ -9,14 +12,17 @@
<clr-dg-column [clrDgSortBy]="repoCountComparator">{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects" [clrDgItem]="p">
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>
<a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a>
</clr-dg-cell>
<clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="showRoleInfo">{{roleInfo[p.current_user_role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}}
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{pagination.totalItems
}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -7,6 +7,9 @@
<li class="nav-item">
<a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="helm-charts" routerLinkActive="active">{{'PROJECT_DETAIL.HELMCHART' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>

View File

@ -34,6 +34,9 @@ import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
import { ProjectLabelComponent } from "../project/project-label/project-label.component";
import { ListChartsComponent } from './list-charts/list-charts.component';
import { ListChartVersionsComponent } from './list-chart-versions/list-chart-versions.component';
import { ChartDetailComponent } from './chart-detail/chart-detail.component';
@NgModule({
imports: [
@ -52,7 +55,10 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
AddMemberComponent,
TargetExistsValidatorDirective,
ProjectLabelComponent,
AddGroupComponent
AddGroupComponent,
ListChartsComponent,
ListChartVersionsComponent,
ChartDetailComponent
],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -1,7 +1,7 @@
<div>
<div class="arrow-block" *ngIf="!withAdmiral">
<a (click)="goProBack()">< {{'SIDE_NAV.PROJECTS'| translate}}</a>
<a (click)="watchGoBackEvt(projectId)">< {{'REPOSITORY.REPOSITORIES'| translate}}</a>
<div class="breadcrumb" *ngIf="!withAdmiral">
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<a (click)="watchGoBackEvt(projectId)">&lt; {{'REPOSITORY.REPOSITORIES'| translate}}</a>
</div>
<hbr-repository [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isGuest]="isGuest"
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)"></hbr-repository>

View File

@ -1,4 +1,6 @@
.sub-header-title {
margin-top: 12px;
.breadcrumb a {
text-decoration: none;
cursor: pointer;
color: #007cbb;
font-size: 12px;
}
.arrow-block a{text-decoration: none; cursor: pointer; cursor: pointer; color: #007cbb; font-size: 12px;}

View File

@ -58,7 +58,23 @@ const uiLibConfig: IServiceConfig = {
langCookieKey: "harbor-lang",
langMessageLoader: "http",
langMessagePathForHttpLoader: "i18n/lang/",
langMessageFileSuffixForHttpLoader: "-lang.json"
langMessageFileSuffixForHttpLoader: "-lang.json",
systemInfoEndpoint: "/api/systeminfo",
repositoryBaseEndpoint: "/api/repositories",
logBaseEndpoint: "/api/logs",
targetBaseEndpoint: "/api/targets",
replicationBaseEndpoint: "/api/replications",
replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
projectPolicyEndpoint: "/api/projects/configs",
projectBaseEndpoint: "/api/projects",
localI18nMessageVariableMap: {},
configurationEndpoint: "/api/configurations",
scanJobEndpoint: "/api/jobs/scan",
labelEndpoint: "/api/labels",
helmChartEndpoint: "/api/chartrepo",
downloadChartEndpoint: "/chartrepo"
};
@NgModule({

View File

@ -185,7 +185,8 @@
"LOGS": "Logs",
"LABELS": "Labels",
"PROJECTS": "Projects",
"CONFIG": "Configuration"
"CONFIG": "Configuration",
"HELMCHART": "Helm Charts"
},
"PROJECT_CONFIG": {
"REGISTRY": "Project registry",
@ -311,7 +312,6 @@
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping endpoint.",
"NAME": "Name",
"STATUS": "Status",
"PROJECT": "Project",
"NAME_IS_REQUIRED": "Name is required.",
"DESCRIPTION": "Description",
@ -470,6 +470,50 @@
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info"
},
"HELM_CHART": {
"HELMCHARTS": "Charts",
"CHARTVERSIONS": "Versions",
"UPLOAD_TITLE": "Upload chart files",
"CHART_FILE": "Chart File",
"CHART_PROV": "Prov File",
"DOWNLOAD": "Download",
"SUMMARY": "Summary",
"DEPENDENCIES": "Dependencies",
"VALUES": "Values",
"OVERVIEW": "Overview",
"HOME": "Home",
"SRC_REPO": "Srouce Repository",
"CREATED": "Created Time",
"MAINTAINERS": "Maintainers",
"PULLS": "Pull Count",
"VERSION": "Version",
"INSTALL": "Install",
"INSTALL_CHART": "Install Chart",
"NAME": "Name",
"REPO": "Repository",
"FILTER_FOR_CHARTS": "Filter for charts",
"DELETE": "Delete",
"OF": "of",
"VERSIONS": "versions",
"IMAGES": "Images",
"ENGINE": "Engine",
"ACTION": "Action",
"UPLOAD": "Upload",
"DELETE_CHART_VERSION_TITLE": "Delete Chart Versions",
"DELETE_CHART_VERSION": "Do you want to delete version {{param}}?",
"IMPORT": "Import",
"EXPORT": "Export",
"ADD_REPO": "Add Repo",
"SHOW_KV": "Key Value Pairs",
"SHOW_YAML": "YAML File",
"PLACEHOLDER": "No Item",
"FILE_UPLOADED": "File upload successfully",
"SIGNED": "Signed",
"UNSIGNED": "Unsigned",
"ITEMS": "charts",
"NO_README": "No readme file provided by this charts.",
"SECURITY": "Security"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
},
@ -725,6 +769,8 @@
"DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete user member",
"DELETE_GROUP": "Delete group member",
"DELETE_CHART_VERSION": "Delete Chart Version",
"DELETE_CHART": "Delete Chart",
"SWITCH_ROLE": "Switch role",
"ADD_GROUP": "Add group member",
"ADD_USER": "Add user member",

View File

@ -311,7 +311,6 @@
"TEST_CONNECTION_SUCCESS": "Conexión comprobada satisfactoriamente.",
"TEST_CONNECTION_FAILURE": "Fallo al conectar con el endpoint.",
"NAME": "Nombre",
"STATUS": "Status",
"PROJECT": "Proyecto",
"NAME_IS_REQUIRED": "El nombre es obligatorio.",
"DESCRIPTION": "Descripción",

View File

@ -292,7 +292,6 @@
"TEST_CONNECTION_SUCCESS": "Connexion testée avec succès.",
"TEST_CONNECTION_FAILURE": "Echec du ping du point final.",
"NAME": "Nom",
"STATUS": "Status",
"PROJECT": "Projet",
"NAME_IS_REQUIRED": "Le nom est obligatoire.",
"DESCRIPTION": "Description",

View File

@ -311,7 +311,6 @@
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
"TEST_CONNECTION_FAILURE": "测试连接失败。",
"NAME": "名称",
"STATUS": "状态",
"PROJECT": "项目",
"NAME_IS_REQUIRED": "名称为必填项。",
"DESCRIPTION": "描述",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -19,7 +19,7 @@ Library OperatingSystem
*** Variables ***
${HARBOR_VERSION} v1.1.1
${CLAIR_BUILDER} 1.4.0
${CLAIR_BUILDER} 1.6.0
${GOLANG_VERSION} 1.9.2
*** Keywords ***

View File

@ -7,7 +7,17 @@ cd /harbor_src
mv /harbor_resources/node_modules ./
npm install -q --no-progress
npm run lint
npm run lint:lib
## Build harbor-ui and link it
npm run build:lib
## Link harbor-ui
chmod -R +xr /harbor_src/lib/dist
cd /harbor_src/lib/dist
npm link
cd /harbor_src
npm link harbor-ui
npm run build
npm run test > ./npm-ut-test-results
rm -rf /harbor_src/node_modules