Commit 15d813d4 authored by AllForNothing's avatar AllForNothing
Browse files

Add p2p-preheat policy UI


Signed-off-by: default avatarAllForNothing <sshijun@vmware.com>
parent a6c7e15d
......@@ -18,7 +18,7 @@
}}
</button>
<button id="set-default"
[disabled]="!(selectedRow && selectedRow.length === 1 && !selectedRow[0].default && !selectedRow[0].enabled)"
[disabled]="!(selectedRow && selectedRow.length === 1 && !selectedRow[0].default && selectedRow[0].enabled)"
class="btn btn-secondary"
(click)="setAsDefault()">{{'SCANNER.SET_AS_DEFAULT' | translate}}</button>
<clr-dropdown
......@@ -113,9 +113,14 @@
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="instance.status === 'Healthy'">
<span *ngSwitchCase="true" class="label label-success">{{ instance.status }}</span>
<span *ngSwitchDefault class="label label-danger">{{ instance.status }}</span>
<clr-dg-cell>
<span *ngIf="!instance.hasCheckHealth;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="instance.pingStatus === 'Healthy';else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>{{ instance.enabled || false }}</clr-dg-cell>
<clr-dg-cell>{{ instance.auth_mode }}</clr-dg-cell>
......@@ -124,7 +129,7 @@
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount" [(clrDgPage)]="currentPage">
<span *ngIf="pagination.totalItems">{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }}{{ 'HELM_CHART.OF' | translate }}</span>
<span *ngIf="pagination.totalItems">{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} {{ 'HELM_CHART.OF' | translate }} </span>
<span>{{ pagination.totalItems }} {{ 'HELM_CHART.ITEMS' | translate }}</span>
</clr-dg-pagination>
</clr-dg-footer>
......
......@@ -90,6 +90,9 @@ describe('DistributionInstanceComponent', () => {
},
ListProviders() {
return of(mockedProviders).pipe(delay(10));
},
PingInstances() {
return of(true);
}
};
......
......@@ -27,6 +27,7 @@ import { clone, DEFAULT_PAGE_SIZE } from '../../../lib/utils/utils';
import { Instance } from "../../../../ng-swagger-gen/models/instance";
import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service";
import { Metadata } from '../../../../ng-swagger-gen/models/metadata';
import { FrontInstance, HEALTHY, UNHEALTHY } from '../distribution-interface';
interface MultiOperateData {
operation: string;
......@@ -38,14 +39,15 @@ const KRAKEN_ICON: string = 'images/kraken-logo-color.svg';
const ONE_THOUSAND: number = 1000;
const KRAKEN: string = 'kraken';
@Component({
selector: 'dist-instances',
templateUrl: './distribution-instances.component.html',
styleUrls: ['./distribution-instances.component.scss']
})
export class DistributionInstancesComponent implements OnInit, OnDestroy {
instances: Instance[] = [];
selectedRow: Instance[] = [];
instances: FrontInstance[] = [];
selectedRow: FrontInstance[] = [];
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 1;
......@@ -87,7 +89,6 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.loadData();
this.getProviders();
}
......@@ -132,10 +133,24 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
response.headers.get('x-total-count')
);
this.instances = response.body as Instance[];
this.pingInstances();
},
err => this.msgHandler.error(err)
);
}
pingInstances() {
if (this.instances && this.instances.length) {
this.instances.forEach((item, index) => {
this.disService.PingInstances({instance: this.handleInstance(item)})
.pipe(finalize(() => this.instances[index].hasCheckHealth = true))
.subscribe(res => {
this.instances[index].pingStatus = HEALTHY;
}, error => {
this.instances[index].pingStatus = UNHEALTHY;
});
});
}
}
refresh() {
this.queryString = null;
......@@ -170,7 +185,7 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
const instance: Instance = clone(this.selectedRow[0]);
instance.default = true;
this.disService.UpdateInstance({
instance: instance,
instance: this.handleInstance(instance),
preheatInstanceName: this.selectedRow[0].name
})
.subscribe(
......@@ -309,7 +324,7 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
copiedInstance.enabled = true;
return this.disService
.UpdateInstance({
instance: copiedInstance,
instance: this.handleInstance(copiedInstance),
preheatInstanceName: instance.name
})
.pipe(
......@@ -327,7 +342,7 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
this.msgHandler.error(msg + ': ' + errMsg);
});
});
return observableThrowError(message);
return observableThrowError(error);
})
);
}
......@@ -343,7 +358,7 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
copiedInstance.enabled = false;
return this.disService
.UpdateInstance({
instance: copiedInstance,
instance: this.handleInstance(copiedInstance),
preheatInstanceName: instance.name
})
.pipe(
......@@ -361,7 +376,7 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
this.msgHandler.error(msg + ': ' + errMsg);
});
});
return observableThrowError(message);
return observableThrowError(error);
})
);
}
......@@ -379,4 +394,13 @@ export class DistributionInstancesComponent implements OnInit, OnDestroy {
}
}
}
handleInstance(instance: FrontInstance): FrontInstance {
if (instance) {
const copyOne: FrontInstance = clone(instance);
delete copyOne.hasCheckHealth;
delete copyOne.pingStatus;
return copyOne;
}
return instance;
}
}
import { Instance } from '../../../ng-swagger-gen/models/instance';
export class AuthMode {
static NONE = 'NONE';
static BASIC = 'BASIC';
......@@ -14,3 +16,11 @@ export enum PreheatingStatusEnum {
SUCCESS = 'SUCCESS',
FAIL = 'FAIL',
}
export interface FrontInstance extends Instance {
hasCheckHealth?: boolean;
pingStatus?: string;
}
export const HEALTHY: string = 'Healthy';
export const UNHEALTHY: string = 'Unhealthy';
......@@ -12,7 +12,7 @@
<label class="required">{{
'DISTRIBUTION.PROVIDER' | translate
}}</label>
<select
<select class="width-280"
clrSelect
name="provider"
id="provider"
......@@ -37,13 +37,12 @@
<label class="required clr-control-label" for="name">{{
'DISTRIBUTION.NAME' | translate
}}</label>
<input
<input class="width-280"
clrInput
required
type="text"
id="name"
autocomplete="off"
placeholder="{{ 'DISTRIBUTION.SETUP.NAME_PLACEHOLDER' | translate }}"
[(ngModel)]="model.name"
name="name"
[disabled]="editingMode"
......@@ -60,11 +59,8 @@
clrTextarea
type="text"
id="description"
class="inputWidth"
class="width-280"
row="3"
placeholder="{{
'DISTRIBUTION.SETUP.DESCRIPTION_PLACEHOLDER' | translate
}}"
[(ngModel)]="model.description"
[ngModelOptions]="{ standalone: true }"
></textarea>
......@@ -75,15 +71,13 @@
<label class="required clr-control-label" for="endpoint">{{
'DISTRIBUTION.ENDPOINT' | translate
}}</label>
<input
<input class="width-280"
clrInput
required
pattern="^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.*?)*$"
type="text"
id="endpoint"
placeholder="{{
'DISTRIBUTION.SETUP.ENDPOINT_PLACEHOLDER' | translate
}}"
placeholder="http(s)://192.168.1.1"
[(ngModel)]="model.endpoint"
name="endpoint"
autocomplete="off"
......@@ -92,23 +86,6 @@
'TOOLTIP.ENDPOINT_FORMAT' | translate
}}</clr-control-error>
</clr-input-container>
<!-- 5. enabled -->
<clr-checkbox-container *ngIf="!editingMode">
<label for="enabled">
<span>{{ 'DISTRIBUTION.ENABLED' | translate }}</span>
</label>
<clr-checkbox-wrapper>
<input
clrCheckbox
id="enabled"
name="enabled"
type="checkbox"
[(ngModel)]="model.enabled"
/>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<!-- auth mode -->
<clr-radio-container clrInline>
<label>{{ 'DISTRIBUTION.AUTH_MODE' | translate }}</label>
......@@ -152,7 +129,6 @@
<label for="token_mode">OAuth</label>
</clr-radio-wrapper>
</clr-radio-container>
<!-- auth data -->
<span *ngIf="model.auth_mode == 'BASIC'">
<clr-input-container>
......@@ -160,6 +136,7 @@
'DISTRIBUTION.USERNAME' | translate
}}</label>
<input
class="width-280"
clrInput
required
type="text"
......@@ -180,6 +157,7 @@
'DISTRIBUTION.PASSWORD' | translate
}}</label>
<input
class="width-280"
clrInput
required
type="password"
......@@ -202,6 +180,7 @@
'DISTRIBUTION.TOKEN' | translate
}}</label>
<input
class="width-280"
clrInput
required
type="text"
......@@ -219,9 +198,41 @@
</clr-input-container>
</span>
<span *ngIf="model.auth_mode == 'NONE'"></span>
<!-- 5. enabled -->
<div class="clr-form-control">
<label class="clr-control-label">{{"SCANNER.OPTIONS" | translate}}</label>
<div class="clr-control-container padding-top-3">
<clr-checkbox-wrapper>
<input
clrCheckbox
id="enabled"
name="enabled"
type="checkbox"
[(ngModel)]="model.enabled"
/>
<label for="enabled"><span>{{ 'DISTRIBUTION.ENABLED' | translate }}</span></label>
</clr-checkbox-wrapper>
<clr-checkbox-wrapper>
<input name="insecure" clrCheckbox
type="checkbox" id="insecure"
[(ngModel)]="model.insecure"
>
<label for="insecure">{{"SCANNER.SKIP" | translate}}
<clr-tooltip>
<clr-icon class="color-57" clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
{{'SCANNER.SKIP_CERT_VERIFY' | translate}}
</clr-tooltip-content>
</clr-tooltip>
</label>
</clr-checkbox-wrapper>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button id="button-test" type="button" [clrLoading]="checkBtnState" class="btn btn-outline" (click)="onTestEndpoint()" [disabled]="!isValid || onTesting">{{'SCANNER.TEST_CONNECTION' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()">
{{ 'BUTTON.CANCEL' | translate }}
</button>
......
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { NgForm, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { errorHandler } from '../../../lib/utils/shared/shared.utils';
import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service";
import { Instance } from "../../../../ng-swagger-gen/models/instance";
import { AuthMode } from "../distribution-interface";
import { PreheatService } from '../../../../ng-swagger-gen/services/preheat.service';
import { Instance } from '../../../../ng-swagger-gen/models/instance';
import { AuthMode, FrontInstance, HEALTHY } from '../distribution-interface';
import { clone } from '../../../lib/utils/utils';
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { ClrLoadingState } from "@clr/angular";
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { ClrLoadingState } from '@clr/angular';
import { Metadata } from '../../../../ng-swagger-gen/models/metadata';
import { operateChanges, OperateInfo, OperationState } from '../../../lib/components/operation/operate';
import { OperationService } from '../../../lib/components/operation/operation.service';
import { finalize } from 'rxjs/operators';
const DEFAULT_PROVIDER: string = 'dragonfly';
@Component({
selector: 'dist-setup-modal',
......@@ -32,7 +35,8 @@ export class DistributionSetupModalComponent implements OnInit {
@Output()
refresh: EventEmitter<any> = new EventEmitter<any>();
checkBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onTesting: boolean = false;
constructor(
private distributionService: PreheatService,
private msgHandler: MessageHandlerService,
......@@ -43,7 +47,6 @@ export class DistributionSetupModalComponent implements OnInit {
ngOnInit() {
this.reset();
}
public get isValid(): boolean {
return this.instanceForm && this.instanceForm.valid;
}
......@@ -92,6 +95,7 @@ export class DistributionSetupModalComponent implements OnInit {
name: '',
endpoint: '',
enabled: true,
insecure: true,
vendor: '',
auth_mode: AuthMode.NONE,
auth_info: this.authData
......@@ -118,7 +122,7 @@ export class DistributionSetupModalComponent implements OnInit {
instance.description = this.model.description;
instance.auth_mode = this.model.auth_mode;
instance.auth_info = this.model.auth_info;
this.distributionService.UpdateInstance({preheatInstanceName: this.model.name, instance: instance
this.distributionService.UpdateInstance({preheatInstanceName: this.model.name, instance: this.handleInstance(instance)
}).subscribe(
response => {
this.translate.get('DISTRIBUTION.UPDATE_SUCCESS').subscribe(msg => {
......@@ -185,6 +189,14 @@ export class DistributionSetupModalComponent implements OnInit {
this.model = clone(data);
this.originModelForEdit = clone(data);
this.authData = this.model.auth_info || {};
} else {
if (this.providers && this.providers.length) {
this.providers.forEach(item => {
if (item.id === DEFAULT_PROVIDER) {
this.model.vendor = item.id;
}
});
}
}
}
......@@ -196,6 +208,14 @@ export class DistributionSetupModalComponent implements OnInit {
if ( this.model.endpoint !== this.originModelForEdit.endpoint) {
return true;
}
// tslint:disable-next-line:triple-equals
if ( this.model.enabled != this.originModelForEdit.enabled) {
return true;
}
// tslint:disable-next-line:triple-equals
if ( this.model.insecure != this.originModelForEdit.insecure) {
return true;
}
if (this.model.auth_mode !== this.originModelForEdit.auth_mode) {
return true;
} else {
......@@ -217,4 +237,32 @@ export class DistributionSetupModalComponent implements OnInit {
}
return true;
}
onTestEndpoint() {
this.onTesting = true;
this.checkBtnState = ClrLoadingState.LOADING;
const instance: Instance = clone(this.model);
instance.id = 0;
this.distributionService.PingInstances({
instance: this.handleInstance(instance)
}).pipe(finalize(() => this.onTesting = false))
.subscribe(res => {
this.checkBtnState = ClrLoadingState.SUCCESS;
this.inlineAlert.showInlineSuccess({
message: "SCANNER.TEST_PASS"
});
}, error => {
this.inlineAlert.showInlineError('P2P_PROVIDER.TEST_FAILED');
this.checkBtnState = ClrLoadingState.ERROR;
});
}
handleInstance(instance: FrontInstance): Instance {
if (instance) {
const copyOne: FrontInstance = clone(instance);
delete copyOne.hasCheckHealth;
delete copyOne.pingStatus;
return copyOne;
}
return instance;
}
}
......@@ -64,6 +64,9 @@ import { ReplicationTasksComponent } from "../lib/components/replication/replica
import { ReplicationTasksRoutingResolverService } from "./services/routing-resolvers/replication-tasks-routing-resolver.service";
import { ArtifactDetailRoutingResolverService } from "./services/routing-resolvers/artifact-detail-routing-resolver.service";
import { DistributionInstancesComponent } from './distribution/distribution-instances/distribution-instances.component';
import { PolicyComponent } from './project/p2p-provider/policy/policy.component';
import { TaskListComponent } from './project/p2p-provider/task-list/task-list.component';
import { P2pProviderComponent } from './project/p2p-provider/p2p-provider.component';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
......@@ -327,6 +330,28 @@ const harborRoutes: Routes = [
},
component: ScannerComponent
},
{
path: 'p2p-provider',
canActivate: [MemberPermissionGuard],
data: {
permissionParam: {
resource: USERSTATICPERMISSION.P2P_PROVIDER.KEY,
action: USERSTATICPERMISSION.P2P_PROVIDER.VALUE.READ
}
},
component: P2pProviderComponent,
children: [
{
path: 'policies',
component: PolicyComponent
},
{
path: ':preheatPolicyName/executions/:executionId/tasks',
component: TaskListComponent
},
{ path: '', redirectTo: 'policies', pathMatch: 'full' },
],
},
{
path: '',
redirectTo: 'repositories',
......
<clr-modal [(clrModalOpen)]="isOpen" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 *ngIf="isEdit" class="modal-title">{{'P2P_PROVIDER.EDIT_POLICY' | translate}}</h3>
<h3 *ngIf="!isEdit" class="modal-title">{{'P2P_PROVIDER.ADD_POLICY' | translate}}</h3>
<div class="modal-body">
<div class="align-center">
<inline-alert class="modal-title"></inline-alert>
<form #policyForm="ngForm" class="clr-form clr-form-horizontal">
<section class="form-block">
<!-- provider -->
<clr-select-container>
<label class="clr-control-label required width-6rem">{{'P2P_PROVIDER.PROVIDER' | translate}}</label>
<select class="width-380" [disabled]="loading" [class.clr-error]="provider.errors && provider.errors.required && (provider.dirty || provider.touched)"
#provider="ngModel" clrSelect
name="provider" id="provider"
[(ngModel)]="policy.provider_id"
required>
<option class="display-none" value=""></option>
<option [selected]="policy.provider_id == provider.id" *ngFor="let provider of providers" value="{{provider.id}}">{{provider.provider}}</option>
</select>
<clr-control-error *ngIf="provider.errors && provider.errors.required && (provider.dirty || provider.touched)">
{{'P2P_PROVIDER.PROVIDER_REQUIRED' | translate}}
</clr-control-error>
</clr-select-container>
<div class="clr-form-control mt-0" *ngIf="isSystemAdmin() &&!(providers && providers.length)">
<label class="clr-control-label width-6rem"></label>
<div class="clr-control-container width-380">
<div class="space-between">
<span class="alert-label">{{"P2P_PROVIDER.NO_PROVIDER" | translate}}</span>
<a class="go-link" routerLink="/harbor/distribution/instances">{{'P2P_PROVIDER.PROVIDER' | translate}}</a>
</div>
</div>
</div>
<!-- name -->
<div class="clr-form-control">
<label for="name" class="clr-control-label required width-6rem">{{'P2P_PROVIDER.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="name.errors && name.errors.required && (name.dirty || name.touched)">
<div class="clr-input-wrapper">
<input pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" [disabled]="loading" autocomplete="off" class="clr-input width-380" type="text" id="name" [(ngModel)]="policy.name"
size="30" name="name" #name="ngModel" required>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)" class="tooltip-content">
{{'P2P_PROVIDER.NAME_REQUIRED' | translate}}
</clr-control-error>
</div>
</div>
<!-- description -->
<div class="clr-form-control">
<label for="description" class="clr-control-label width-6rem">{{'P2P_PROVIDER.DESCRIPTION' | translate}}</label>
<div class="clr-control-container">
<textarea autocomplete="off" class="clr-textarea width-380" type="text" id="description" #description="ngModel" [disabled]="loading" [(ngModel)]="policy.description"
name="description"></textarea>
</div>
</div>
<!-- filters-repo -->
<div class="clr-form-control">
<label for="repo" class="clr-control-label required width-6rem">{{'P2P_PROVIDER.FILTERS' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="repo.errors && repo.errors.required && (repo.dirty || repo.touched)">
<div class="clr-input-wrapper">
<label class="sub-label">{{'P2P_PROVIDER.REPOS' | translate}}</label>
<input placeholder="**" [disabled]="loading" autocomplete="off" class="clr-input width-290" type="text" id="repo" [(ngModel)]="repos"
size="30" name="repo" #repo="ngModel" required>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-helper class="margin-left-90px opacity-08">{{'TAG_RETENTION.REP_SEPARATOR' | translate}}</clr-control-helper>
<clr-control-error class="margin-left-90px" *ngIf="repo.errors && repo.errors.required && (repo.dirty || repo.touched)">
{{'P2P_PROVIDER.REPO_REQUIRED' | translate}}
</clr-control-error>
</div>
<