Feature of helm chart UI

1. Add Charts list view
2. Add Charts card view
3. Add Chart version list view
4. Add chart version card view
5. Add Chart Detail Summary
6. Add Chart Detail Value
6. Add Chart Detail Deps
7. Update nodeclarity Dockerfile
8. Add markdown support
9. Add package-lock file to src
This commit is contained in:
Deng, Qian 2018-07-11 18:21:33 +08:00
parent 7a24dcdb05
commit 8feb49c64e
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,22 +1,28 @@
<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>
</clr-dg-action-bar>
<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>
</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>
<clr-dg-column *ngIf="showRoleInfo" [clrDgSortBy]="roleComparator">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<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-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>{{ (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;
}
.arrow-block a{text-decoration: none; cursor: pointer; cursor: pointer; color: #007cbb; font-size: 12px;}
.breadcrumb a {
text-decoration: none;
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
npm run test > ./npm-ut-test-results
rm -rf /harbor_src/node_modules