mirror of https://github.com/goharbor/harbor.git
Enhance webhook UI (#18291)
1. Add execution list for a certain webhook policy 2. Add task list for a certain execution Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
5c0266e719
commit
ba9078f463
|
@ -1,12 +1,7 @@
|
|||
<div class="replication-tasks mt-1">
|
||||
<div class="replication-tasks mt-2">
|
||||
<section class="overview-section">
|
||||
<div class="title-wrapper">
|
||||
<div>
|
||||
<div>
|
||||
<a (click)="onBack()" class="onBack"
|
||||
><{{ 'P2P_PROVIDER.P2P_PROVIDER' | translate }}</a
|
||||
>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
<div>
|
||||
<h2 class="custom-h2 h2-style">
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
.overview-section {
|
||||
.title-wrapper {
|
||||
/* stylelint-disable */
|
||||
.onBack{
|
||||
color: #007cbb;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -328,15 +328,7 @@ export class TaskListComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
);
|
||||
}
|
||||
onBack(): void {
|
||||
this.router.navigate([
|
||||
'harbor',
|
||||
'projects',
|
||||
`${this.projectId}`,
|
||||
'p2p-provider',
|
||||
'policies',
|
||||
]);
|
||||
}
|
||||
|
||||
// refresh icon
|
||||
refreshTasks(): void {
|
||||
this.currentPage = 1;
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
<a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle">{{
|
||||
'PROJECT_DETAIL.PROJECTS' | translate
|
||||
}}</a>
|
||||
<ng-container *ngIf="hasSignedIn && isWebhookTaskListPage()">
|
||||
<span class="back-icon"><</span>
|
||||
<a (click)="backToWebhook()" class="backStyle">{{
|
||||
'PROJECT_DETAIL.WEBHOOKS' | translate
|
||||
}}</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hasSignedIn && isP2pPreheatTaskListPage()">
|
||||
<span class="back-icon"><</span>
|
||||
<a (click)="backToP2pPreheat()" class="backStyle">{{
|
||||
'P2P_PROVIDER.P2P_PROVIDER' | translate
|
||||
}}</a>
|
||||
</ng-container>
|
||||
|
||||
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']" class="backStyle">
|
||||
{{ 'SEARCH.BACK' | translate }}</a
|
||||
>
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../services/event-service/event.service';
|
||||
import { RouteConfigId } from '../../../route-reuse-strategy/harbor-route-reuse-strategy';
|
||||
|
||||
@Component({
|
||||
selector: 'project-detail',
|
||||
|
@ -472,4 +473,39 @@ export class ProjectDetailComponent
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
backToWebhook() {
|
||||
this.router.navigate([
|
||||
'harbor',
|
||||
'projects',
|
||||
`${this.projectId}`,
|
||||
'webhook',
|
||||
]);
|
||||
}
|
||||
|
||||
isWebhookTaskListPage(): boolean {
|
||||
return (
|
||||
this.route?.firstChild?.firstChild?.snapshot.data[
|
||||
'routeConfigId'
|
||||
] === RouteConfigId.WEBHOOK_TASKS_PAGE
|
||||
);
|
||||
}
|
||||
|
||||
backToP2pPreheat(): void {
|
||||
this.router.navigate([
|
||||
'harbor',
|
||||
'projects',
|
||||
`${this.projectId}`,
|
||||
'p2p-provider',
|
||||
'policies',
|
||||
]);
|
||||
}
|
||||
|
||||
isP2pPreheatTaskListPage(): boolean {
|
||||
return (
|
||||
this.route?.firstChild?.firstChild?.snapshot.data[
|
||||
'routeConfigId'
|
||||
] === RouteConfigId.P2P_TASKS_PAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,6 +101,23 @@
|
|||
</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
<!-- payload format -->
|
||||
<clr-select-container *ngIf="webhook.targets[0].type === 'http'">
|
||||
<label>{{ 'WEBHOOK.PAYLOAD_FORMAT' | translate }}</label>
|
||||
<select
|
||||
class="width-238"
|
||||
clrSelect
|
||||
name="payloadFormat"
|
||||
id="payload-format"
|
||||
[(ngModel)]="webhook.targets[0].payload_format"
|
||||
[disabled]="checking">
|
||||
<option
|
||||
*ngFor="let item of getPayLoadFormats()"
|
||||
value="{{ item }}">
|
||||
{{ getI18nKey(item) | translate }}
|
||||
</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label required">{{
|
||||
|
|
|
@ -15,12 +15,18 @@ import {
|
|||
finalize,
|
||||
switchMap,
|
||||
} from 'rxjs/operators';
|
||||
import { ProjectWebhookService } from '../webhook.service';
|
||||
import {
|
||||
PAYLOAD_FORMATS,
|
||||
PAYLOAD_FORMAT_I18N_MAP,
|
||||
ProjectWebhookService,
|
||||
} from '../webhook.service';
|
||||
import { compareValue } from '../../../../shared/units/utils';
|
||||
import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component';
|
||||
import { WebhookService } from '../../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { WebhookPolicy } from '../../../../../../ng-swagger-gen/models/webhook-policy';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { SupportedWebhookEventTypes } from '../../../../../../ng-swagger-gen/models/supported-webhook-event-types';
|
||||
import { PayloadFormatType } from '../../../../../../ng-swagger-gen/models/payload-format-type';
|
||||
|
||||
@Component({
|
||||
selector: 'add-webhook-form',
|
||||
|
@ -40,6 +46,7 @@ export class AddWebhookFormComponent implements OnInit, OnDestroy {
|
|||
type: 'http',
|
||||
address: '',
|
||||
skip_cert_verify: true,
|
||||
payload_format: PAYLOAD_FORMATS[0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -51,7 +58,7 @@ export class AddWebhookFormComponent implements OnInit, OnDestroy {
|
|||
@ViewChild('webhookForm', { static: true }) currentForm: NgForm;
|
||||
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
|
||||
@Input()
|
||||
metadata: any;
|
||||
metadata: SupportedWebhookEventTypes;
|
||||
@Output() notify = new EventEmitter<WebhookPolicy>();
|
||||
checkNameOnGoing: boolean = false;
|
||||
isNameExisting: boolean = false;
|
||||
|
@ -204,4 +211,28 @@ export class AddWebhookFormComponent implements OnInit, OnDestroy {
|
|||
eventTypeToText(eventType: string): string {
|
||||
return this.projectWebhookService.eventTypeToText(eventType);
|
||||
}
|
||||
|
||||
getPayLoadFormats(): PayloadFormatType[] {
|
||||
if (
|
||||
this.metadata?.payload_formats?.length &&
|
||||
this.webhook.targets[0].type
|
||||
) {
|
||||
for (let i = 0; i < this.metadata.payload_formats.length; i++) {
|
||||
if (
|
||||
this.metadata.payload_formats[i].notify_type ===
|
||||
this.webhook.targets[0].type
|
||||
) {
|
||||
return this.metadata.payload_formats[i].formats;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getI18nKey(v: string): string {
|
||||
if (v && PAYLOAD_FORMAT_I18N_MAP[v]) {
|
||||
return PAYLOAD_FORMAT_I18N_MAP[v];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:host::ng-deep.modal-dialog {
|
||||
width: 30rem;
|
||||
height: 35rem;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from '@angular/core';
|
||||
import { AddWebhookFormComponent } from '../add-webhook-form/add-webhook-form.component';
|
||||
import { WebhookPolicy } from '../../../../../../ng-swagger-gen/models/webhook-policy';
|
||||
import { SupportedWebhookEventTypes } from '../../../../../../ng-swagger-gen/models/supported-webhook-event-types';
|
||||
|
||||
@Component({
|
||||
selector: 'add-webhook',
|
||||
|
@ -22,7 +23,7 @@ export class AddWebhookComponent {
|
|||
@Input() projectId: number;
|
||||
webhook: WebhookPolicy;
|
||||
@Input()
|
||||
metadata: any;
|
||||
metadata: SupportedWebhookEventTypes;
|
||||
@ViewChild(AddWebhookFormComponent)
|
||||
addWebhookFormComponent: AddWebhookFormComponent;
|
||||
@Output() notify = new EventEmitter<WebhookPolicy>();
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid
|
||||
[clrDgLoading]="loading"
|
||||
(clrDgRefresh)="clrLoadExecutions($event, true, null)">
|
||||
<clr-dg-action-bar class="mt-2">
|
||||
<div class="flex-end">
|
||||
<h4 class="mt-0">
|
||||
{{ 'P2P_PROVIDER.EXECUTIONS' | translate }}
|
||||
</h4>
|
||||
<span class="refresh-btn">
|
||||
<clr-icon
|
||||
shape="refresh"
|
||||
(click)="refreshExecutions(true, null)"
|
||||
[hidden]="loading"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'id'">{{
|
||||
'REPLICATION.ID' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{
|
||||
'JOB_SERVICE_DASHBOARD.VENDOR_TYPE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'status'">{{
|
||||
'DESTINATION.STATUS' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'status'">{{
|
||||
'WEBHOOK.EVENT_TYPE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'WEBHOOK.PAYLOAD_DATA' | translate }}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'start_time'">{{
|
||||
'REPLICATION.CREATION_TIME' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'end_time'">{{
|
||||
'REPLICATION.END_TIME' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{
|
||||
'P2P_PROVIDER.JOB_PLACEHOLDER' | translate
|
||||
}}</clr-dg-placeholder>
|
||||
<clr-dg-row
|
||||
*ngFor="let execution of executions"
|
||||
[clrDgItem]="execution">
|
||||
<clr-dg-cell>
|
||||
<a href="javascript:void(0)" (click)="goToLink(execution.id)">{{
|
||||
execution.id
|
||||
}}</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
{{ execution.vendor_type }}
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell
|
||||
>{{ execution.status }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
*ngIf="execution.status_message"
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="20"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
[clrPosition]="'left'"
|
||||
clrSize="md"
|
||||
*clrIfOpen>
|
||||
<span>{{ execution.status_message }}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip></clr-dg-cell
|
||||
>
|
||||
<clr-dg-cell>
|
||||
<span class="label-flex">
|
||||
{{ eventTypeToText(execution?.extra_attrs?.event_type) }}
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="flex">
|
||||
<clr-signpost>
|
||||
<a class="btn btn-link link-normal" clrSignpostTrigger>{{
|
||||
toString(toJson(execution?.extra_attrs?.payload))
|
||||
}}</a>
|
||||
<clr-signpost-content
|
||||
class="pre"
|
||||
[clrPosition]="'top-middle'"
|
||||
*clrIfOpen>
|
||||
<hbr-copy-input
|
||||
[iconMode]="true"
|
||||
[defaultValue]="
|
||||
toString(execution?.extra_attrs?.payload)
|
||||
"></hbr-copy-input>
|
||||
<pre
|
||||
[innerHTML]="
|
||||
toJson(execution?.extra_attrs?.payload)
|
||||
| json
|
||||
| markdown
|
||||
"></pre>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
execution.start_time | harborDatetime: 'short'
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
execution.end_time | harborDatetime: 'short'
|
||||
}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[(clrDgPage)]="currentPage"
|
||||
[clrDgPageSize]="pageSize"
|
||||
[clrDgTotalItems]="total">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
|
||||
'PAGINATION.PAGE_SIZE' | translate
|
||||
}}</clr-dg-page-size>
|
||||
<span *ngIf="total"
|
||||
>{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }}
|
||||
{{ 'REPLICATION.OF' | translate }}</span
|
||||
>
|
||||
{{ total }} {{ 'REPLICATION.ITEMS' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
.link-normal {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 10rem;
|
||||
text-transform: unset;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pre {
|
||||
min-width: 25rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.label-flex {
|
||||
width: 9rem;
|
||||
letter-spacing: 0;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
border: 1px solid;
|
||||
border-radius: 9px;
|
||||
padding: 0 7px 0 10px;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ExecutionsComponent } from './executions.component';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { WebhookService } from '../../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { Execution } from '../../../../../../ng-swagger-gen/models/execution';
|
||||
import { of } from 'rxjs';
|
||||
import { WebhookPolicy } from '../../../../../../ng-swagger-gen/models/webhook-policy';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { ProjectWebhookService } from '../webhook.service';
|
||||
|
||||
describe('ExecutionsComponent', () => {
|
||||
let component: ExecutionsComponent;
|
||||
let fixture: ComponentFixture<ExecutionsComponent>;
|
||||
|
||||
const mockedExecutions: Execution[] = [
|
||||
{
|
||||
end_time: '2023-02-28T03:54:01Z',
|
||||
extra_attrs: {
|
||||
event_data: {
|
||||
replication: {
|
||||
artifact_type: 'image',
|
||||
authentication_type: 'basic',
|
||||
dest_resource: {
|
||||
endpoint: 'https://nightly-oidc.harbor.io',
|
||||
namespace: 'library',
|
||||
registry_type: 'harbor',
|
||||
},
|
||||
execution_timestamp: 1677556395,
|
||||
harbor_hostname: 'nightly-oidc.harbor.io',
|
||||
job_status: 'Success',
|
||||
override_mode: true,
|
||||
policy_creator: 'admin',
|
||||
src_resource: {
|
||||
endpoint: 'https://hub.docker.com',
|
||||
namespace: 'library',
|
||||
registry_name: 'docker hub',
|
||||
registry_type: 'docker-hub',
|
||||
},
|
||||
successful_artifact: [
|
||||
{
|
||||
name_tag: 'redis [1 item(s) in total]',
|
||||
status: 'Success',
|
||||
type: 'image',
|
||||
},
|
||||
],
|
||||
trigger_type: 'MANUAL',
|
||||
},
|
||||
},
|
||||
occur_at: 1677556415,
|
||||
operator: 'MANUAL',
|
||||
type: 'REPLICATION',
|
||||
},
|
||||
id: 30,
|
||||
metrics: { success_task_count: 1, task_count: 1 },
|
||||
start_time: '2023-02-28T03:53:35Z',
|
||||
status: 'Success',
|
||||
trigger: 'EVENT',
|
||||
vendor_id: 2,
|
||||
vendor_type: 'WEBHOOK',
|
||||
},
|
||||
{
|
||||
end_time: '2023-02-28T03:53:40Z',
|
||||
extra_attrs: {
|
||||
event_data: {
|
||||
repository: {
|
||||
date_created: 1677556403,
|
||||
name: 'redis',
|
||||
namespace: 'library',
|
||||
repo_full_name: 'library/redis',
|
||||
repo_type: 'public',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
digest: 'sha256:6a59f1cbb8d28ac484176d52c473494859a512ddba3ea62a547258cf16c9b3ae',
|
||||
resource_url:
|
||||
'nightly-oidc.harbor.io/library/redis:latest',
|
||||
tag: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
occur_at: 1677556415,
|
||||
operator: 'harbor-jobservice',
|
||||
type: 'PUSH_ARTIFACT',
|
||||
},
|
||||
id: 28,
|
||||
metrics: { success_task_count: 1, task_count: 1 },
|
||||
start_time: '2023-02-28T03:53:35Z',
|
||||
status: 'Success',
|
||||
trigger: 'EVENT',
|
||||
vendor_id: 2,
|
||||
vendor_type: 'WEBHOOK',
|
||||
},
|
||||
];
|
||||
|
||||
const mockedWebhookService = {
|
||||
ListExecutionsOfWebhookPolicyResponse() {
|
||||
return of(
|
||||
new HttpResponse<Array<Execution>>({
|
||||
headers: new HttpHeaders({
|
||||
'x-total-count': '2',
|
||||
}),
|
||||
body: mockedExecutions,
|
||||
})
|
||||
).pipe(delay(0));
|
||||
},
|
||||
};
|
||||
|
||||
const mockedWebhookPolicy: WebhookPolicy = {
|
||||
id: 1,
|
||||
project_id: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [ExecutionsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: WebhookService,
|
||||
useValue: mockedWebhookService,
|
||||
},
|
||||
ProjectWebhookService,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ExecutionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.selectedWebhook = mockedWebhookPolicy;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render execution list and no timeout', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
|
||||
expect(rows.length).toEqual(2);
|
||||
expect(component.timeout).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,175 @@
|
|||
import { Component, Input, OnDestroy } from '@angular/core';
|
||||
import { WebhookPolicy } from '../../../../../../ng-swagger-gen/models/webhook-policy';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { WebhookService } from '../../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import {
|
||||
getPageSizeFromLocalStorage,
|
||||
getSortingString,
|
||||
PageSizeMapKeys,
|
||||
setPageSizeToLocalStorage,
|
||||
} from '../../../../shared/units/utils';
|
||||
import { Execution } from '../../../../../../ng-swagger-gen/models/execution';
|
||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
|
||||
import {
|
||||
EXECUTION_STATUS,
|
||||
TIME_OUT,
|
||||
} from '../../p2p-provider/p2p-provider.service';
|
||||
import { ProjectWebhookService } from '../webhook.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-executions',
|
||||
templateUrl: './executions.component.html',
|
||||
styleUrls: ['./executions.component.scss'],
|
||||
})
|
||||
export class ExecutionsComponent implements OnDestroy {
|
||||
@Input()
|
||||
selectedWebhook: WebhookPolicy;
|
||||
executions: Execution[] = [];
|
||||
loading: boolean = true;
|
||||
currentPage: number = 1;
|
||||
pageSize: number = getPageSizeFromLocalStorage(
|
||||
PageSizeMapKeys.WEBHOOK_EXECUTIONS_COMPONENT
|
||||
);
|
||||
total: number = 0;
|
||||
state: ClrDatagridStateInterface;
|
||||
timeout: any;
|
||||
constructor(
|
||||
private webhookService: WebhookService,
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private router: Router,
|
||||
private projectWebhookService: ProjectWebhookService
|
||||
) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.clearLoop();
|
||||
}
|
||||
clrLoadExecutions(
|
||||
state: ClrDatagridStateInterface,
|
||||
withLoading: boolean,
|
||||
policyId: number
|
||||
) {
|
||||
if (state) {
|
||||
this.state = state;
|
||||
}
|
||||
if (state && state.page) {
|
||||
this.pageSize = state.page.size;
|
||||
setPageSizeToLocalStorage(
|
||||
PageSizeMapKeys.WEBHOOK_EXECUTIONS_COMPONENT,
|
||||
this.pageSize
|
||||
);
|
||||
}
|
||||
let q: string;
|
||||
if (state && state.filters && state.filters.length) {
|
||||
q = encodeURIComponent(
|
||||
`${state.filters[0].property}=~${state.filters[0].value}`
|
||||
);
|
||||
}
|
||||
let sort: string;
|
||||
if (state && state.sort && state.sort.by) {
|
||||
sort = getSortingString(state);
|
||||
} else {
|
||||
// sort by start_time desc by default
|
||||
sort = `-start_time`;
|
||||
}
|
||||
if (withLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this.webhookService
|
||||
.ListExecutionsOfWebhookPolicyResponse({
|
||||
webhookPolicyId: policyId ? policyId : this.selectedWebhook.id,
|
||||
projectNameOrId: this.selectedWebhook.project_id.toString(),
|
||||
pageSize: this.pageSize,
|
||||
page: this.currentPage,
|
||||
sort: sort,
|
||||
q: q,
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: res => {
|
||||
if (res.headers) {
|
||||
let xHeader: string = res.headers.get('X-Total-Count');
|
||||
if (xHeader) {
|
||||
this.total = parseInt(xHeader, 0);
|
||||
}
|
||||
}
|
||||
this.executions = res.body || [];
|
||||
this.setLoop();
|
||||
},
|
||||
error: err => {
|
||||
this.messageHandlerService.error(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
goToLink(id: number) {
|
||||
const linkUrl = [
|
||||
'harbor',
|
||||
'projects',
|
||||
`${this.selectedWebhook.project_id}`,
|
||||
'webhook',
|
||||
`${this.selectedWebhook.id}`,
|
||||
'executions',
|
||||
`${id}`,
|
||||
'tasks',
|
||||
];
|
||||
this.router.navigate(linkUrl);
|
||||
}
|
||||
toString(v: any) {
|
||||
if (v) {
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
toJson(v: any) {
|
||||
if (v) {
|
||||
return JSON.parse(v);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
refreshExecutions(shouldReset: boolean, policyId: number) {
|
||||
if (shouldReset) {
|
||||
this.executions = [];
|
||||
this.currentPage = 1;
|
||||
}
|
||||
this.clrLoadExecutions(this.state, true, policyId);
|
||||
}
|
||||
|
||||
clearLoop() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
setLoop() {
|
||||
this.clearLoop();
|
||||
if (this.executions && this.executions.length) {
|
||||
for (let i = 0; i < this.executions.length; i++) {
|
||||
if (this.willChangStatus(this.executions[i].status)) {
|
||||
if (!this.timeout) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.clrLoadExecutions(this.state, false, null);
|
||||
}, TIME_OUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willChangStatus(status: string): boolean {
|
||||
return (
|
||||
status === EXECUTION_STATUS.PENDING ||
|
||||
status === EXECUTION_STATUS.RUNNING ||
|
||||
status === EXECUTION_STATUS.SCHEDULED
|
||||
);
|
||||
}
|
||||
|
||||
eventTypeToText(eventType: any): string {
|
||||
return this.projectWebhookService.eventTypeToText(eventType);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<h4>{{ 'WEBHOOK.LAST_TRIGGER' | translate }}</h4>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column [clrDgField]="'event_type'">{{
|
||||
'WEBHOOK.TYPE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'WEBHOOK.STATUS' | translate }}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'WEBHOOK.LAST_TRIGGERED' | translate }}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let item of lastTriggers">
|
||||
<clr-dg-cell>{{ eventTypeToText(item.event_type) }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div *ngIf="item?.enabled" class="icon-wrap">
|
||||
<clr-icon
|
||||
shape="check-circle"
|
||||
size="20"
|
||||
class="is-success enabled-icon"></clr-icon>
|
||||
<span>{{ 'WEBHOOK.ENABLED' | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="!item?.enabled" class="icon-wrap">
|
||||
<clr-icon
|
||||
shape="exclamation-triangle"
|
||||
size="20"
|
||||
class="is-warning"></clr-icon>
|
||||
<span>{{ 'WEBHOOK.DISABLED' | translate }}</span>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
item.last_trigger_time | harborDatetime: 'short'
|
||||
}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-placeholder>
|
||||
{{ 'WEBHOOK.NO_TRIGGER' | translate }}
|
||||
</clr-dg-placeholder>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="lastTriggers.length"
|
||||
>1 - {{ lastTriggers.length }} {{ 'WEBHOOK.OF' | translate }}
|
||||
</span>
|
||||
{{ lastTriggers.length }} {{ 'WEBHOOK.ITEMS' | translate }}
|
||||
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
|
@ -1,63 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { LastTriggerComponent } from './last-trigger.component';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
import { ProjectWebhookService } from '../webhook.service';
|
||||
import { WebhookLastTrigger } from '../../../../../../ng-swagger-gen/models/webhook-last-trigger';
|
||||
|
||||
describe('LastTriggerComponent', () => {
|
||||
const mokedTriggers: WebhookLastTrigger[] = [
|
||||
{
|
||||
policy_name: 'http',
|
||||
enabled: true,
|
||||
event_type: 'pullImage',
|
||||
creation_time: null,
|
||||
last_trigger_time: null,
|
||||
},
|
||||
{
|
||||
policy_name: 'slack',
|
||||
enabled: true,
|
||||
event_type: 'pullImage',
|
||||
creation_time: null,
|
||||
last_trigger_time: null,
|
||||
},
|
||||
];
|
||||
const mockWebhookService = {
|
||||
eventTypeToText(eventType: string) {
|
||||
return eventType;
|
||||
},
|
||||
};
|
||||
let component: LastTriggerComponent;
|
||||
let fixture: ComponentFixture<LastTriggerComponent>;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [LastTriggerComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ProjectWebhookService,
|
||||
useValue: mockWebhookService,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LastTriggerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should render one row', async () => {
|
||||
component.inputLastTriggers = mokedTriggers;
|
||||
component.webhookName = 'slack';
|
||||
component.ngOnChanges({
|
||||
inputLastTriggers: new SimpleChange([], mokedTriggers, true),
|
||||
});
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row');
|
||||
expect(rows.length).toEqual(1);
|
||||
});
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ProjectWebhookService } from '../webhook.service';
|
||||
import { WebhookLastTrigger } from '../../../../../../ng-swagger-gen/models/webhook-last-trigger';
|
||||
|
||||
@Component({
|
||||
selector: 'last-trigger',
|
||||
templateUrl: 'last-trigger.component.html',
|
||||
styleUrls: ['./last-trigger.component.scss'],
|
||||
})
|
||||
export class LastTriggerComponent implements OnChanges {
|
||||
@Input() inputLastTriggers: WebhookLastTrigger[];
|
||||
@Input() webhookName: string;
|
||||
lastTriggers: WebhookLastTrigger[] = [];
|
||||
constructor(private webhookService: ProjectWebhookService) {}
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes && changes['inputLastTriggers']) {
|
||||
this.lastTriggers = [];
|
||||
this.inputLastTriggers.forEach(item => {
|
||||
if (this.webhookName === item.policy_name) {
|
||||
this.lastTriggers.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
eventTypeToText(eventType: string): string {
|
||||
return this.webhookService.eventTypeToText(eventType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
<div class="replication-tasks mt-3">
|
||||
<section class="overview-section">
|
||||
<div class="title-wrapper">
|
||||
<div>
|
||||
<div class="title-block">
|
||||
<div class="first-item">
|
||||
<h2 class="custom-h2 h2-style mt-0">
|
||||
{{ 'P2P_PROVIDER.EXECUTIONS' | translate }}
|
||||
</h2>
|
||||
<span class="id-divider"></span>
|
||||
<h2 class="custom-h2 h2-style mt-0">
|
||||
{{ executionId }}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="status-progress"
|
||||
*ngIf="execution && isInProgress()">
|
||||
<span class="spinner spinner-inline"></span>
|
||||
<span>{{
|
||||
'REPLICATION.IN_PROGRESS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-success"
|
||||
*ngIf="execution && isSuccess()">
|
||||
<clr-icon
|
||||
size="18"
|
||||
shape="success-standard"
|
||||
class="color-green"></clr-icon>
|
||||
<span>{{ 'REPLICATION.SUCCESS' | translate }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-failed"
|
||||
*ngIf="execution && isFailed()">
|
||||
<clr-icon
|
||||
size="18"
|
||||
shape="error-standard"
|
||||
class="color-red"></clr-icon>
|
||||
<span>{{ 'REPLICATION.FAILURE' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="execution-block">
|
||||
<div class="executions-detail">
|
||||
<div>
|
||||
<label
|
||||
>{{ 'REPLICATION.CREATION_TIME' | translate }} :</label
|
||||
>
|
||||
<span>{{ startTime() | harborDatetime: 'short' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-block">
|
||||
<section class="execution-detail-label">
|
||||
<section class="detail-row">
|
||||
<div class="num-success common-style"></div>
|
||||
<label class="detail-span">{{
|
||||
'REPLICATION.SUCCESS' | translate
|
||||
}}</label>
|
||||
<div class="execution-details">{{ successNum() }}</div>
|
||||
</section>
|
||||
<section class="detail-row">
|
||||
<div class="num-failed common-style"></div>
|
||||
<label class="detail-span">{{
|
||||
'REPLICATION.FAILURE' | translate
|
||||
}}</label>
|
||||
<div class="execution-details">{{ failedNum() }}</div>
|
||||
</section>
|
||||
<section class="detail-row">
|
||||
<div class="num-progress common-style"></div>
|
||||
<label class="detail-span">{{
|
||||
'REPLICATION.IN_PROGRESS' | translate
|
||||
}}</label>
|
||||
<div class="execution-details">{{ progressNum() }}</div>
|
||||
</section>
|
||||
<section class="detail-row">
|
||||
<div class="num-stopped common-style"></div>
|
||||
<label class="detail-span">{{
|
||||
'REPLICATION.STOPPED' | translate
|
||||
}}</label>
|
||||
<div class="execution-details">{{ stoppedNum() }}</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="tasks-detail">
|
||||
<clr-datagrid
|
||||
(clrDgRefresh)="clrLoadTasks($event, true)"
|
||||
[clrDgLoading]="loading">
|
||||
<clr-dg-action-bar>
|
||||
<div class="flex-end">
|
||||
<h3 class="modal-title mt-0">
|
||||
{{ 'P2P_PROVIDER.TASKS' | translate }}
|
||||
</h3>
|
||||
<span class="refresh-btn">
|
||||
<clr-icon
|
||||
shape="refresh"
|
||||
(click)="refreshTasks()"
|
||||
[hidden]="loading"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'id'">{{
|
||||
'REPLICATION.TASK_ID' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'P2P_PROVIDER.ID' | translate }}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'status'">{{
|
||||
'DESTINATION.STATUS' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="'creation_time'">{{
|
||||
'DESTINATION.CREATION_TIME' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'REPLICATION.LOGS' | translate }}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{
|
||||
'P2P_PROVIDER.TASKS_PLACEHOLDER' | translate
|
||||
}}</clr-dg-placeholder>
|
||||
<clr-dg-row *ngFor="let t of tasks" [clrDgItem]="t">
|
||||
<clr-dg-cell>{{ t.id }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ t.execution_id }}</clr-dg-cell>
|
||||
<clr-dg-cell
|
||||
>{{ t.status
|
||||
}}<clr-tooltip>
|
||||
<clr-icon
|
||||
*ngIf="t.status_message"
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="20"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
[clrPosition]="'left'"
|
||||
clrSize="md"
|
||||
*clrIfOpen>
|
||||
<span>{{ t.status_message }}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip></clr-dg-cell
|
||||
>
|
||||
<clr-dg-cell>{{
|
||||
t.creation_time | harborDatetime: 'short'
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
[href]="viewLog(t.id)"
|
||||
*ngIf="t.status !== 'Initialized'">
|
||||
<clr-icon shape="list"></clr-icon>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[(clrDgPage)]="currentPage"
|
||||
[clrDgPageSize]="pageSize"
|
||||
[clrDgTotalItems]="totalCount">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
|
||||
'PAGINATION.PAGE_SIZE' | translate
|
||||
}}</clr-dg-page-size>
|
||||
<span *ngIf="totalCount"
|
||||
>{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }}
|
||||
{{ 'REPLICATION.OF' | translate }}</span
|
||||
>
|
||||
{{ totalCount }} {{ 'REPLICATION.ITEMS' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,174 @@
|
|||
.replication-tasks {
|
||||
.overview-section {
|
||||
.title-wrapper {
|
||||
/* stylelint-disable */
|
||||
.onBack{
|
||||
color: #007cbb;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
>div:first-child {
|
||||
min-width: 384px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
>div:nth-child(2) {
|
||||
width: 140px;
|
||||
|
||||
span {
|
||||
color: #007cbb;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.id-divider {
|
||||
display: inline-block;
|
||||
height: 25px;
|
||||
width: 2px;
|
||||
background-color: #ccc;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.h2-style {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.execution-block {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.execution-detail-label {
|
||||
margin-right: 10px;
|
||||
text-align: left;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
height: 27px;
|
||||
|
||||
.common-style {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.num-success {
|
||||
background-color: #308700;
|
||||
}
|
||||
|
||||
.num-failed {
|
||||
background-color: #C92101;
|
||||
}
|
||||
|
||||
.num-progress {
|
||||
background-color: #1C5898;
|
||||
}
|
||||
|
||||
.num-stopped {
|
||||
background-color: #A1A1A1;
|
||||
}
|
||||
|
||||
.detail-span {
|
||||
flex:0 0 100px;
|
||||
margin: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.execution-details {
|
||||
width: 200px;
|
||||
margin: 8px 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.executions-detail {
|
||||
width: 391px;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
>div {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasks-detail {
|
||||
margin-top: 65px;
|
||||
|
||||
.action-select {
|
||||
padding-right: 18px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.filter-tag {
|
||||
float: left;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
.resource-width {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-top-16px {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
margin-top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.button-stop {
|
||||
margin-top: 16px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pre {
|
||||
min-width: 25rem;
|
||||
max-width: 100%;
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TasksComponent } from './tasks.component';
|
||||
import { SharedTestingModule } from '../../../../shared/shared.module';
|
||||
import { Execution } from '../../../../../../ng-swagger-gen/models/execution';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { Task } from '../../../../../../ng-swagger-gen/models/task';
|
||||
import { of } from 'rxjs';
|
||||
import { WebhookService } from '../../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
describe('TasksComponent', () => {
|
||||
let component: TasksComponent;
|
||||
let fixture: ComponentFixture<TasksComponent>;
|
||||
|
||||
const mockedExecutions: Execution[] = [
|
||||
{
|
||||
end_time: '2023-02-28T03:54:01Z',
|
||||
extra_attrs: {
|
||||
event_data: {
|
||||
replication: {
|
||||
artifact_type: 'image',
|
||||
authentication_type: 'basic',
|
||||
dest_resource: {
|
||||
endpoint: 'https://nightly-oidc.harbor.io',
|
||||
namespace: 'library',
|
||||
registry_type: 'harbor',
|
||||
},
|
||||
execution_timestamp: 1677556395,
|
||||
harbor_hostname: 'nightly-oidc.harbor.io',
|
||||
job_status: 'Success',
|
||||
override_mode: true,
|
||||
policy_creator: 'admin',
|
||||
src_resource: {
|
||||
endpoint: 'https://hub.docker.com',
|
||||
namespace: 'library',
|
||||
registry_name: 'docker hub',
|
||||
registry_type: 'docker-hub',
|
||||
},
|
||||
successful_artifact: [
|
||||
{
|
||||
name_tag: 'redis [1 item(s) in total]',
|
||||
status: 'Success',
|
||||
type: 'image',
|
||||
},
|
||||
],
|
||||
trigger_type: 'MANUAL',
|
||||
},
|
||||
},
|
||||
occur_at: 1677556415,
|
||||
operator: 'MANUAL',
|
||||
type: 'REPLICATION',
|
||||
},
|
||||
id: 30,
|
||||
metrics: { success_task_count: 1, task_count: 1 },
|
||||
start_time: '2023-02-28T03:53:35Z',
|
||||
status: 'Success',
|
||||
trigger: 'EVENT',
|
||||
vendor_id: 2,
|
||||
vendor_type: 'WEBHOOK',
|
||||
},
|
||||
];
|
||||
|
||||
const tasks: Array<Task> = [
|
||||
{
|
||||
creation_time: '2023-02-28T03:53:35Z',
|
||||
end_time: '2023-02-28T03:54:01Z',
|
||||
execution_id: 30,
|
||||
extra_attrs: {
|
||||
event_data: {
|
||||
replication: {
|
||||
artifact_type: 'image',
|
||||
authentication_type: 'basic',
|
||||
dest_resource: {
|
||||
endpoint: 'https://nightly-oidc.harbor.io',
|
||||
namespace: 'library',
|
||||
registry_type: 'harbor',
|
||||
},
|
||||
execution_timestamp: 1677556395,
|
||||
harbor_hostname: 'nightly-oidc.harbor.io',
|
||||
job_status: 'Success',
|
||||
override_mode: true,
|
||||
policy_creator: 'admin',
|
||||
src_resource: {
|
||||
endpoint: 'https://hub.docker.com',
|
||||
namespace: 'library',
|
||||
registry_name: 'docker hub',
|
||||
registry_type: 'docker-hub',
|
||||
},
|
||||
successful_artifact: [
|
||||
{
|
||||
name_tag: 'redis [1 item(s) in total]',
|
||||
status: 'Success',
|
||||
type: 'image',
|
||||
},
|
||||
],
|
||||
trigger_type: 'MANUAL',
|
||||
},
|
||||
},
|
||||
occur_at: 1677556415,
|
||||
operator: 'MANUAL',
|
||||
type: 'REPLICATION',
|
||||
},
|
||||
id: 30,
|
||||
run_count: 1,
|
||||
start_time: '2023-02-28T03:53:35Z',
|
||||
status: 'Success',
|
||||
update_time: '2023-02-28T03:54:01Z',
|
||||
},
|
||||
];
|
||||
|
||||
const mockedWebhookService = {
|
||||
ListExecutionsOfWebhookPolicy() {
|
||||
return of(mockedExecutions).pipe(delay(0));
|
||||
},
|
||||
ListTasksOfWebhookExecutionResponse() {
|
||||
return of(
|
||||
new HttpResponse<Array<Task>>({
|
||||
headers: new HttpHeaders({
|
||||
'x-total-count': '1',
|
||||
}),
|
||||
body: tasks,
|
||||
})
|
||||
).pipe(delay(0));
|
||||
},
|
||||
};
|
||||
|
||||
const mockActivatedRoute = {
|
||||
snapshot: {
|
||||
params: {
|
||||
policyId: 1,
|
||||
executionId: 1,
|
||||
},
|
||||
parent: {
|
||||
parent: {
|
||||
params: { id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [TasksComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: WebhookService,
|
||||
useValue: mockedWebhookService,
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TasksComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render task list and no timeout', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
|
||||
expect(rows.length).toEqual(1);
|
||||
expect(component.timeoutForTaskList).toBeFalsy();
|
||||
expect(component.executionTimeout).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should show success state', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
const successState =
|
||||
fixture.nativeElement.querySelectorAll('.status-success');
|
||||
expect(successState).toBeTruthy();
|
||||
expect(component.timeoutForTaskList).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,284 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import {
|
||||
EXECUTION_STATUS,
|
||||
TIME_OUT,
|
||||
} from '../../p2p-provider/p2p-provider.service';
|
||||
import { WebhookService } from '../../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { Execution } from '../../../../../../ng-swagger-gen/models/execution';
|
||||
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
|
||||
import {
|
||||
getPageSizeFromLocalStorage,
|
||||
getSortingString,
|
||||
PageSizeMapKeys,
|
||||
setPageSizeToLocalStorage,
|
||||
} from '../../../../shared/units/utils';
|
||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||
import { Task } from 'ng-swagger-gen/models/task';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss'],
|
||||
})
|
||||
export class TasksComponent implements OnInit {
|
||||
projectId: number;
|
||||
policyId: number;
|
||||
tasks: Task[] = [];
|
||||
executionId: number;
|
||||
loading: boolean = true;
|
||||
inProgress: boolean = false;
|
||||
execution: Execution;
|
||||
executionTimeout: any;
|
||||
currentPage: number = 1;
|
||||
pageSize: number = getPageSizeFromLocalStorage(
|
||||
PageSizeMapKeys.WEBHOOK_TASKS_COMPONENT
|
||||
);
|
||||
totalCount: number;
|
||||
state: ClrDatagridStateInterface;
|
||||
timeoutForTaskList: any;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private webhookService: WebhookService,
|
||||
private messageHandlerService: MessageHandlerService
|
||||
) {}
|
||||
ngOnInit(): void {
|
||||
this.projectId = +this.route.snapshot.parent.parent.params['id'];
|
||||
this.policyId = +this.route.snapshot.params['policyId'];
|
||||
this.executionId = +this.route.snapshot.params['executionId'];
|
||||
if (this.executionId) {
|
||||
this.getExecutionDetail(true);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.executionTimeout) {
|
||||
clearTimeout(this.executionTimeout);
|
||||
this.executionTimeout = null;
|
||||
}
|
||||
if (this.timeoutForTaskList) {
|
||||
clearTimeout(this.timeoutForTaskList);
|
||||
this.timeoutForTaskList = null;
|
||||
}
|
||||
}
|
||||
|
||||
getExecutionDetail(withLoading: boolean): void {
|
||||
if (withLoading) {
|
||||
this.inProgress = true;
|
||||
}
|
||||
if (this.executionId) {
|
||||
this.webhookService
|
||||
.ListExecutionsOfWebhookPolicy({
|
||||
webhookPolicyId: this.policyId,
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
q: encodeURIComponent(`id=${this.executionId}`),
|
||||
})
|
||||
.pipe(finalize(() => (this.inProgress = false)))
|
||||
.subscribe({
|
||||
next: res => {
|
||||
if (res?.length) {
|
||||
this.execution = res[0];
|
||||
if (
|
||||
!this.execution ||
|
||||
this.execution.status ===
|
||||
EXECUTION_STATUS.PENDING ||
|
||||
this.execution.status ===
|
||||
EXECUTION_STATUS.RUNNING ||
|
||||
this.execution.status ===
|
||||
EXECUTION_STATUS.SCHEDULED
|
||||
) {
|
||||
if (this.executionTimeout) {
|
||||
clearTimeout(this.executionTimeout);
|
||||
this.executionTimeout = null;
|
||||
}
|
||||
if (!this.executionTimeout) {
|
||||
this.executionTimeout = setTimeout(() => {
|
||||
this.getExecutionDetail(false);
|
||||
}, TIME_OUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.messageHandlerService.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clrLoadTasks(state: ClrDatagridStateInterface, withLoading: boolean) {
|
||||
if (state) {
|
||||
this.state = state;
|
||||
}
|
||||
if (state && state.page) {
|
||||
this.pageSize = state.page.size;
|
||||
setPageSizeToLocalStorage(
|
||||
PageSizeMapKeys.P2P_TASKS_COMPONENT,
|
||||
this.pageSize
|
||||
);
|
||||
}
|
||||
if (withLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
let q: string;
|
||||
if (state && state.filters && state.filters.length) {
|
||||
q = encodeURIComponent(
|
||||
`${state.filters[0].property}=~${state.filters[0].value}`
|
||||
);
|
||||
}
|
||||
let sort: string;
|
||||
if (state && state.sort && state.sort.by) {
|
||||
sort = getSortingString(state);
|
||||
} else {
|
||||
// sort by creation_time desc by default
|
||||
sort = `-creation_time`;
|
||||
}
|
||||
if (withLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this.webhookService
|
||||
.ListTasksOfWebhookExecutionResponse({
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
webhookPolicyId: this.policyId,
|
||||
executionId: +this.executionId,
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
sort: sort,
|
||||
q: q,
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: res => {
|
||||
if (res.headers) {
|
||||
let xHeader: string = res.headers.get('x-total-count');
|
||||
if (xHeader) {
|
||||
this.totalCount = parseInt(xHeader, 0);
|
||||
}
|
||||
}
|
||||
this.tasks = res.body;
|
||||
this.setLoop();
|
||||
},
|
||||
error: error => {
|
||||
this.messageHandlerService.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setLoop() {
|
||||
if (this.timeoutForTaskList) {
|
||||
clearTimeout(this.timeoutForTaskList);
|
||||
this.timeoutForTaskList = null;
|
||||
}
|
||||
if (this.tasks && this.tasks.length) {
|
||||
for (let i = 0; i < this.tasks.length; i++) {
|
||||
if (this.willChangStatus(this.tasks[i].status)) {
|
||||
if (!this.timeoutForTaskList) {
|
||||
this.timeoutForTaskList = setTimeout(() => {
|
||||
this.clrLoadTasks(this.state, false);
|
||||
}, TIME_OUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willChangStatus(status: string): boolean {
|
||||
return (
|
||||
status === EXECUTION_STATUS.PENDING ||
|
||||
status === EXECUTION_STATUS.RUNNING ||
|
||||
status === EXECUTION_STATUS.SCHEDULED
|
||||
);
|
||||
}
|
||||
|
||||
refreshTasks() {
|
||||
this.clrLoadTasks(this.state, true);
|
||||
}
|
||||
viewLog(taskId: number | string): string {
|
||||
return `/api/v2.0/projects/${this.projectId}/webhook/policies/${this.policyId}/executions/${this.executionId}/tasks/${taskId}/log`;
|
||||
}
|
||||
|
||||
isInProgress(): boolean {
|
||||
return this.execution && this.willChangStatus(this.execution.status);
|
||||
}
|
||||
isSuccess(): boolean {
|
||||
return (
|
||||
this.execution && this.execution.status === EXECUTION_STATUS.SUCCESS
|
||||
);
|
||||
}
|
||||
isFailed(): boolean {
|
||||
return (
|
||||
this.execution &&
|
||||
(this.execution.status === EXECUTION_STATUS.ERROR ||
|
||||
this.execution.status === EXECUTION_STATUS.STOPPED)
|
||||
);
|
||||
}
|
||||
|
||||
trigger(): string {
|
||||
return this.execution && this.execution.trigger
|
||||
? this.execution.trigger
|
||||
: '';
|
||||
}
|
||||
|
||||
startTime(): string {
|
||||
return this.execution && this.execution.start_time
|
||||
? this.execution.start_time
|
||||
: null;
|
||||
}
|
||||
|
||||
successNum(): number {
|
||||
if (this.execution && this.execution.metrics) {
|
||||
return this.execution.metrics.success_task_count
|
||||
? this.execution.metrics.success_task_count
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
failedNum(): number {
|
||||
if (this.execution && this.execution.metrics) {
|
||||
return this.execution.metrics.error_task_count
|
||||
? this.execution.metrics.error_task_count
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
progressNum(): number {
|
||||
if (this.execution && this.execution.metrics) {
|
||||
const num: number =
|
||||
(this.execution.metrics.pending_task_count
|
||||
? this.execution.metrics.pending_task_count
|
||||
: 0) +
|
||||
(this.execution.metrics.running_task_count
|
||||
? this.execution.metrics.running_task_count
|
||||
: 0) +
|
||||
(this.execution.metrics.scheduled_task_count
|
||||
? this.execution.metrics.scheduled_task_count
|
||||
: 0);
|
||||
return num ? num : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
stoppedNum(): number {
|
||||
if (this.execution && this.execution.metrics) {
|
||||
return this.execution.metrics.stopped_task_count
|
||||
? this.execution.metrics.stopped_task_count
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
toString(v: any) {
|
||||
if (v) {
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
<clr-datagrid
|
||||
(clrDgRefresh)="getWebhooks($event)"
|
||||
[clrDgLoading]="loadingWebhookList"
|
||||
[(clrDgSelected)]="selectedRow">
|
||||
[(clrDgSingleSelected)]="selectedRow"
|
||||
(clrDgSingleSelectedChange)="refreshExecutions($event)">
|
||||
<clr-dg-action-bar>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-7">
|
||||
|
@ -32,15 +33,12 @@
|
|||
clrDropdownItem
|
||||
(click)="switchWebhookStatus()"
|
||||
[disabled]="
|
||||
!(
|
||||
selectedRow && selectedRow.length === 1
|
||||
) || !hasUpdatePermission
|
||||
!selectedRow || !hasUpdatePermission
|
||||
">
|
||||
<span id="toggle-webhook">
|
||||
<span
|
||||
*ngIf="
|
||||
selectedRow[0] &&
|
||||
!selectedRow[0].enabled
|
||||
selectedRow && !selectedRow.enabled
|
||||
">
|
||||
<clr-icon
|
||||
class="margin-top-2"
|
||||
|
@ -53,8 +51,8 @@
|
|||
<span
|
||||
*ngIf="
|
||||
!(
|
||||
selectedRow[0] &&
|
||||
!selectedRow[0].enabled
|
||||
selectedRow &&
|
||||
!selectedRow.enabled
|
||||
)
|
||||
">
|
||||
<clr-icon
|
||||
|
@ -73,9 +71,7 @@
|
|||
(click)="editWebhook()"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="
|
||||
!(
|
||||
selectedRow && selectedRow.length === 1
|
||||
) || !hasUpdatePermission
|
||||
!selectedRow || !hasUpdatePermission
|
||||
">
|
||||
<clr-icon
|
||||
class="margin-top-0"
|
||||
|
@ -93,8 +89,7 @@
|
|||
(click)="deleteWebhook()"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="
|
||||
!(selectedRow && selectedRow.length >= 1) ||
|
||||
!hasUpdatePermission
|
||||
!selectedRow || !hasUpdatePermission
|
||||
">
|
||||
<clr-icon
|
||||
class="margin-top-0"
|
||||
|
@ -130,6 +125,9 @@
|
|||
'WEBHOOK.ENABLED' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{ 'WEBHOOK.NOTIFY_TYPE' | translate }}</clr-dg-column>
|
||||
<clr-dg-column>{{
|
||||
'WEBHOOK.PAYLOAD_FORMAT' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column class="min-width-340">{{
|
||||
'WEBHOOK.TARGET' | translate
|
||||
}}</clr-dg-column>
|
||||
|
@ -160,6 +158,9 @@
|
|||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{ w?.targets[0].type }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
getI18nKey(w?.targets[0].payload_format) | translate
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ w?.targets[0].address }}</clr-dg-cell>
|
||||
<clr-dg-cell class="event-types">
|
||||
<div class="cell" *ngIf="w?.event_types?.length">
|
||||
|
@ -200,13 +201,6 @@
|
|||
w.creation_time | harborDatetime: 'short'
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ w.description }}</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
<last-trigger
|
||||
class="w-100"
|
||||
[webhookName]="w.name"
|
||||
*clrIfExpanded
|
||||
[inputLastTriggers]="lastTriggers"></last-trigger>
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination
|
||||
|
@ -227,6 +221,11 @@
|
|||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
||||
<app-executions
|
||||
[selectedWebhook]="selectedRow"
|
||||
*ngIf="selectedRow"></app-executions>
|
||||
|
||||
<add-webhook
|
||||
(notify)="success()"
|
||||
[metadata]="metadata"
|
||||
|
|
|
@ -99,3 +99,11 @@
|
|||
.width-120 {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import { WebhookPolicy } from '../../../../../ng-swagger-gen/models/webhook-poli
|
|||
import { WebhookService } from '../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { Registry } from '../../../../../ng-swagger-gen/models/registry';
|
||||
import { ExecutionsComponent } from './excutions/executions.component';
|
||||
import { Execution } from '../../../../../ng-swagger-gen/models/execution';
|
||||
|
||||
describe('WebhookComponent', () => {
|
||||
let component: WebhookComponent;
|
||||
|
@ -48,6 +50,7 @@ describe('WebhookComponent', () => {
|
|||
type: 'http',
|
||||
auth_header: null,
|
||||
skip_cert_verify: true,
|
||||
payload_format: 'cloudevent',
|
||||
},
|
||||
],
|
||||
event_types: ['projectQuota'],
|
||||
|
@ -65,9 +68,6 @@ describe('WebhookComponent', () => {
|
|||
GetSupportedEventTypes() {
|
||||
return of(mockedMetadata).pipe(delay(0));
|
||||
},
|
||||
LastTrigger() {
|
||||
return of([]).pipe(delay(0));
|
||||
},
|
||||
ListWebhookPoliciesOfProjectResponse() {
|
||||
const response: HttpResponse<Array<Registry>> = new HttpResponse<
|
||||
Array<Registry>
|
||||
|
@ -82,6 +82,16 @@ describe('WebhookComponent', () => {
|
|||
UpdateWebhookPolicyOfProject() {
|
||||
return of(true);
|
||||
},
|
||||
ListExecutionsOfWebhookPolicyResponse() {
|
||||
return of(
|
||||
new HttpResponse<Array<Execution>>({
|
||||
headers: new HttpHeaders({
|
||||
'x-total-count': '2',
|
||||
}),
|
||||
body: [],
|
||||
})
|
||||
).pipe(delay(0));
|
||||
},
|
||||
};
|
||||
const mockActivatedRoute = {
|
||||
RouterparamMap: of({ get: key => 'value' }),
|
||||
|
@ -115,6 +125,7 @@ describe('WebhookComponent', () => {
|
|||
AddWebhookFormComponent,
|
||||
InlineAlertComponent,
|
||||
ConfirmationDialogComponent,
|
||||
ExecutionsComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -162,7 +173,7 @@ describe('WebhookComponent', () => {
|
|||
});
|
||||
it('should open edit modal', async () => {
|
||||
component.webhookList[0].name = 'test';
|
||||
component.selectedRow[0] = component.webhookList[0];
|
||||
component.selectedRow = component.webhookList[0];
|
||||
component.editWebhook();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
@ -178,7 +189,7 @@ describe('WebhookComponent', () => {
|
|||
});
|
||||
it('should disable webhook', async () => {
|
||||
await fixture.whenStable();
|
||||
component.selectedRow[0] = component.webhookList[0];
|
||||
component.selectedRow = component.webhookList[0];
|
||||
component.webhookList[0].enabled = true;
|
||||
component.switchWebhookStatus();
|
||||
fixture.detectChanges();
|
||||
|
@ -195,7 +206,7 @@ describe('WebhookComponent', () => {
|
|||
it('should enable webhook', async () => {
|
||||
await fixture.whenStable();
|
||||
component.webhookList[0].enabled = false;
|
||||
component.selectedRow[0] = component.webhookList[0];
|
||||
component.selectedRow = component.webhookList[0];
|
||||
component.switchWebhookStatus();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
@ -207,4 +218,13 @@ describe('WebhookComponent', () => {
|
|||
fixture.nativeElement.querySelector('.modal-body');
|
||||
expect(bodyEnable).toBeFalsy();
|
||||
});
|
||||
it('should show executions', async () => {
|
||||
await fixture.whenStable();
|
||||
component.selectedRow = component.webhookList[0];
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const executions =
|
||||
fixture.nativeElement.querySelector('app-executions');
|
||||
expect(executions).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
// limitations under the License.
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AddWebhookComponent } from './add-webhook/add-webhook.component';
|
||||
import { AddWebhookFormComponent } from './add-webhook-form/add-webhook-form.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
|
||||
import { Project } from '../project';
|
||||
import {
|
||||
|
@ -26,7 +26,7 @@ import {
|
|||
PageSizeMapKeys,
|
||||
setPageSizeToLocalStorage,
|
||||
} from '../../../shared/units/utils';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { forkJoin, Observable, Subscription } from 'rxjs';
|
||||
import {
|
||||
UserPermissionService,
|
||||
USERSTATICPERMISSION,
|
||||
|
@ -40,30 +40,38 @@ import {
|
|||
} from '../../../shared/entities/shared.const';
|
||||
import { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message';
|
||||
import { WebhookService } from '../../../../../ng-swagger-gen/services/webhook.service';
|
||||
import { WebhookLastTrigger } from '../../../../../ng-swagger-gen/models/webhook-last-trigger';
|
||||
import { WebhookPolicy } from '../../../../../ng-swagger-gen/models/webhook-policy';
|
||||
import { ProjectWebhookService } from './webhook.service';
|
||||
|
||||
import {
|
||||
PAYLOAD_FORMATS,
|
||||
PAYLOAD_FORMAT_I18N_MAP,
|
||||
ProjectWebhookService,
|
||||
} from './webhook.service';
|
||||
import { ExecutionsComponent } from './excutions/executions.component';
|
||||
import {
|
||||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../services/event-service/event.service';
|
||||
import { SupportedWebhookEventTypes } from '../../../../../ng-swagger-gen/models/supported-webhook-event-types';
|
||||
// The route path which will display this component
|
||||
const URL_TO_DISPLAY: RegExp = /^\/harbor\/projects\/(\d+)\/webhook$/;
|
||||
@Component({
|
||||
templateUrl: './webhook.component.html',
|
||||
styleUrls: ['./webhook.component.scss'],
|
||||
})
|
||||
export class WebhookComponent implements OnInit {
|
||||
export class WebhookComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(AddWebhookComponent)
|
||||
addWebhookComponent: AddWebhookComponent;
|
||||
@ViewChild(AddWebhookFormComponent)
|
||||
addWebhookFormComponent: AddWebhookFormComponent;
|
||||
@ViewChild('confirmationDialogComponent')
|
||||
confirmationDialogComponent: ConfirmationDialogComponent;
|
||||
lastTriggers: WebhookLastTrigger[] = [];
|
||||
projectId: number;
|
||||
projectName: string;
|
||||
selectedRow: WebhookPolicy[] = [];
|
||||
selectedRow: WebhookPolicy;
|
||||
webhookList: WebhookPolicy[] = [];
|
||||
metadata: any;
|
||||
metadata: SupportedWebhookEventTypes;
|
||||
loadingMetadata: boolean = true;
|
||||
loadingWebhookList: boolean = true;
|
||||
loadingTriggers: boolean = false;
|
||||
hasCreatPermission: boolean = false;
|
||||
hasUpdatePermission: boolean = false;
|
||||
page: number = 1;
|
||||
|
@ -72,16 +80,45 @@ export class WebhookComponent implements OnInit {
|
|||
);
|
||||
total: number = 0;
|
||||
state: ClrDatagridStateInterface;
|
||||
@ViewChild(ExecutionsComponent)
|
||||
executionsComponent: ExecutionsComponent;
|
||||
routerSub: Subscription;
|
||||
scrollSub: Subscription;
|
||||
scrollTop: number;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private translate: TranslateService,
|
||||
private webhookService: WebhookService,
|
||||
private projectWebhookService: ProjectWebhookService,
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private userPermissionService: UserPermissionService
|
||||
private userPermissionService: UserPermissionService,
|
||||
private router: Router,
|
||||
private event: EventService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.scrollSub) {
|
||||
this.scrollSub = this.event.subscribe(HarborEvent.SCROLL, v => {
|
||||
if (v && URL_TO_DISPLAY.test(v.url)) {
|
||||
this.scrollTop = v.scrollTop;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!this.routerSub) {
|
||||
this.routerSub = this.router.events.subscribe(e => {
|
||||
if (e instanceof NavigationEnd) {
|
||||
if (e && URL_TO_DISPLAY.test(e.url)) {
|
||||
// Into view
|
||||
this.event.publish(
|
||||
HarborEvent.SCROLL_TO_POSITION,
|
||||
this.scrollTop
|
||||
);
|
||||
} else {
|
||||
this.event.publish(HarborEvent.SCROLL_TO_POSITION, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this.projectId = +this.route.snapshot.parent.parent.params['id'];
|
||||
let resolverData = this.route.snapshot.parent.parent.data;
|
||||
if (resolverData) {
|
||||
|
@ -91,6 +128,18 @@ export class WebhookComponent implements OnInit {
|
|||
this.getData();
|
||||
this.getPermissions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.routerSub) {
|
||||
this.routerSub.unsubscribe();
|
||||
this.routerSub = null;
|
||||
}
|
||||
if (this.scrollSub) {
|
||||
this.scrollSub.unsubscribe();
|
||||
this.scrollSub = null;
|
||||
}
|
||||
}
|
||||
|
||||
getPermissions() {
|
||||
const permissionsList: Observable<boolean>[] = [];
|
||||
permissionsList.push(
|
||||
|
@ -119,14 +168,13 @@ export class WebhookComponent implements OnInit {
|
|||
refresh() {
|
||||
this.page = 1;
|
||||
this.total = 0;
|
||||
this.selectedRow = [];
|
||||
this.selectedRow = null;
|
||||
this.getWebhooks(this.state);
|
||||
this.getData();
|
||||
}
|
||||
getData() {
|
||||
this.getMetadata();
|
||||
this.getLastTriggers();
|
||||
this.selectedRow = [];
|
||||
this.selectedRow = null;
|
||||
}
|
||||
getMetadata() {
|
||||
this.loadingMetadata = true;
|
||||
|
@ -162,23 +210,6 @@ export class WebhookComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
getLastTriggers() {
|
||||
this.loadingTriggers = true;
|
||||
this.webhookService
|
||||
.LastTrigger({
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
})
|
||||
.pipe(finalize(() => (this.loadingTriggers = false)))
|
||||
.subscribe(
|
||||
response => {
|
||||
this.lastTriggers = response;
|
||||
},
|
||||
error => {
|
||||
this.messageHandlerService.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getWebhooks(state?: ClrDatagridStateInterface) {
|
||||
if (state) {
|
||||
this.state = state;
|
||||
|
@ -235,22 +266,22 @@ export class WebhookComponent implements OnInit {
|
|||
let content = '';
|
||||
this.translate
|
||||
.get(
|
||||
!this.selectedRow[0].enabled
|
||||
!this.selectedRow.enabled
|
||||
? 'WEBHOOK.ENABLED_WEBHOOK_SUMMARY'
|
||||
: 'WEBHOOK.DISABLED_WEBHOOK_SUMMARY',
|
||||
{ name: this.selectedRow[0].name }
|
||||
{ name: this.selectedRow.name }
|
||||
)
|
||||
.subscribe(res => {
|
||||
content = res;
|
||||
let message = new ConfirmationMessage(
|
||||
!this.selectedRow[0].enabled
|
||||
!this.selectedRow.enabled
|
||||
? 'WEBHOOK.ENABLED_WEBHOOK_TITLE'
|
||||
: 'WEBHOOK.DISABLED_WEBHOOK_TITLE',
|
||||
content,
|
||||
'',
|
||||
{},
|
||||
ConfirmationTargets.WEBHOOK,
|
||||
!this.selectedRow[0].enabled
|
||||
!this.selectedRow.enabled
|
||||
? ConfirmationButtons.ENABLE_CANCEL
|
||||
: ConfirmationButtons.DISABLE_CANCEL
|
||||
);
|
||||
|
@ -268,9 +299,9 @@ export class WebhookComponent implements OnInit {
|
|||
this.webhookService
|
||||
.UpdateWebhookPolicyOfProject({
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
webhookPolicyId: this.selectedRow[0].id,
|
||||
policy: Object.assign({}, this.selectedRow[0], {
|
||||
enabled: !this.selectedRow[0].enabled,
|
||||
webhookPolicyId: this.selectedRow.id,
|
||||
policy: Object.assign({}, this.selectedRow, {
|
||||
enabled: !this.selectedRow.enabled,
|
||||
}),
|
||||
})
|
||||
.subscribe(
|
||||
|
@ -282,23 +313,19 @@ export class WebhookComponent implements OnInit {
|
|||
}
|
||||
);
|
||||
} else {
|
||||
const observableLists: Observable<any>[] = [];
|
||||
message.data.forEach(item => {
|
||||
observableLists.push(
|
||||
this.webhookService.DeleteWebhookPolicyOfProject({
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
webhookPolicyId: item.id,
|
||||
})
|
||||
);
|
||||
});
|
||||
forkJoin(...observableLists).subscribe(
|
||||
response => {
|
||||
this.refresh();
|
||||
},
|
||||
error => {
|
||||
this.messageHandlerService.handleError(error);
|
||||
}
|
||||
);
|
||||
this.webhookService
|
||||
.DeleteWebhookPolicyOfProject({
|
||||
projectNameOrId: this.projectId.toString(),
|
||||
webhookPolicyId: message.data.id,
|
||||
})
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.refresh();
|
||||
},
|
||||
error: err => {
|
||||
this.messageHandlerService.handleError(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -309,12 +336,12 @@ export class WebhookComponent implements OnInit {
|
|||
this.addWebhookComponent.isEdit = true;
|
||||
this.addWebhookComponent.addWebhookFormComponent.isModify = true;
|
||||
this.addWebhookComponent.addWebhookFormComponent.webhook = clone(
|
||||
this.selectedRow[0]
|
||||
this.selectedRow
|
||||
);
|
||||
this.addWebhookComponent.addWebhookFormComponent.originValue =
|
||||
clone(this.selectedRow[0]);
|
||||
clone(this.selectedRow);
|
||||
this.addWebhookComponent.addWebhookFormComponent.webhook.event_types =
|
||||
clone(this.selectedRow[0].event_types);
|
||||
clone(this.selectedRow.event_types);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,6 +355,7 @@ export class WebhookComponent implements OnInit {
|
|||
this.addWebhookComponent.addWebhookFormComponent.isModify = false;
|
||||
this.addWebhookComponent.addWebhookFormComponent.currentForm.reset({
|
||||
notifyType: this.metadata.notify_type[0],
|
||||
payloadFormat: PAYLOAD_FORMATS[0],
|
||||
});
|
||||
this.addWebhookComponent.addWebhookFormComponent.webhook = {
|
||||
enabled: true,
|
||||
|
@ -337,6 +365,7 @@ export class WebhookComponent implements OnInit {
|
|||
type: 'http',
|
||||
address: '',
|
||||
skip_cert_verify: true,
|
||||
payload_format: PAYLOAD_FORMATS[0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -350,9 +379,7 @@ export class WebhookComponent implements OnInit {
|
|||
|
||||
deleteWebhook() {
|
||||
const names: string[] = [];
|
||||
this.selectedRow.forEach(item => {
|
||||
names.push(item.name);
|
||||
});
|
||||
names.push(this.selectedRow.name);
|
||||
let content = '';
|
||||
this.translate
|
||||
.get('WEBHOOK.DELETE_WEBHOOK_SUMMARY', { names: names.join(',') })
|
||||
|
@ -370,4 +397,16 @@ export class WebhookComponent implements OnInit {
|
|||
eventTypeToText(eventType: string): string {
|
||||
return this.projectWebhookService.eventTypeToText(eventType);
|
||||
}
|
||||
refreshExecutions(e: WebhookPolicy) {
|
||||
if (e) {
|
||||
this.executionsComponent?.refreshExecutions(true, e.id);
|
||||
}
|
||||
}
|
||||
|
||||
getI18nKey(v: string) {
|
||||
if (v && PAYLOAD_FORMAT_I18N_MAP[v]) {
|
||||
return PAYLOAD_FORMAT_I18N_MAP[v];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,23 +2,37 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { WebhookComponent } from './webhook.component';
|
||||
import { LastTriggerComponent } from './last-trigger/last-trigger.component';
|
||||
import { AddWebhookFormComponent } from './add-webhook-form/add-webhook-form.component';
|
||||
import { AddWebhookComponent } from './add-webhook/add-webhook.component';
|
||||
import { ProjectWebhookService } from './webhook.service';
|
||||
import { ExecutionsComponent } from './excutions/executions.component';
|
||||
import { TasksComponent } from './tasks/tasks.component';
|
||||
import { RouteConfigId } from '../../../route-reuse-strategy/harbor-route-reuse-strategy';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':policyId/executions/:executionId/tasks',
|
||||
component: TasksComponent,
|
||||
data: {
|
||||
routeConfigId: RouteConfigId.WEBHOOK_TASKS_PAGE,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: WebhookComponent,
|
||||
data: {
|
||||
reuse: true,
|
||||
routeConfigId: RouteConfigId.WEBHOOK_POLICIES_PAGE,
|
||||
},
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
declarations: [
|
||||
WebhookComponent,
|
||||
LastTriggerComponent,
|
||||
AddWebhookFormComponent,
|
||||
AddWebhookComponent,
|
||||
ExecutionsComponent,
|
||||
TasksComponent,
|
||||
],
|
||||
imports: [RouterModule.forChild(routes), SharedModule],
|
||||
providers: [ProjectWebhookService],
|
||||
|
|
|
@ -29,6 +29,13 @@ const EVENT_TYPES_TEXT_MAP = {
|
|||
TAG_RETENTION: 'Tag retention finished',
|
||||
};
|
||||
|
||||
export const PAYLOAD_FORMATS: string[] = ['Default', 'CloudEvents'];
|
||||
|
||||
export const PAYLOAD_FORMAT_I18N_MAP = {
|
||||
[PAYLOAD_FORMATS[0]]: 'SCANNER.DEFAULT',
|
||||
[PAYLOAD_FORMATS[1]]: 'WEBHOOK.CLOUD_EVENT',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ProjectWebhookService {
|
||||
constructor() {}
|
||||
|
|
|
@ -17,6 +17,8 @@ export enum RouteConfigId {
|
|||
REPLICATION_TASKS_PAGE = 'ReplicationTasksComponent',
|
||||
P2P_POLICIES_PAGE = 'PolicyComponent',
|
||||
P2P_TASKS_PAGE = 'P2pTaskListComponent',
|
||||
WEBHOOK_POLICIES_PAGE = 'WebhookComponent',
|
||||
WEBHOOK_TASKS_PAGE = 'WebhookTasksComponent',
|
||||
}
|
||||
// should not reuse the routes that meet these RegExps
|
||||
const ShouldNotReuseRouteRegExps: RegExp[] = [
|
||||
|
@ -75,6 +77,15 @@ export class HarborRouteReuseStrategy implements RouteReuseStrategy {
|
|||
) {
|
||||
this.shouldDeleteCache = false;
|
||||
}
|
||||
// action 3: from webhook tasks list page to WebhookComponent page
|
||||
if (
|
||||
curr.routeConfig.data.routeConfigId ===
|
||||
RouteConfigId.WEBHOOK_TASKS_PAGE &&
|
||||
future.routeConfig.data.routeConfigId ===
|
||||
RouteConfigId.WEBHOOK_POLICIES_PAGE
|
||||
) {
|
||||
this.shouldDeleteCache = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -965,6 +965,8 @@ export enum PageSizeMapKeys {
|
|||
TAG_RETENTION_COMPONENT = 'TagRetentionComponent',
|
||||
PROJECT_ROBOT_COMPONENT = 'ProjectRobotAccountComponent',
|
||||
WEBHOOK_COMPONENT = 'WebhookComponent',
|
||||
WEBHOOK_EXECUTIONS_COMPONENT = 'Webhook_Execution_Component',
|
||||
WEBHOOK_TASKS_COMPONENT = 'Webhook_Tasks_Component',
|
||||
PROJECT_AUDIT_LOG_COMPONENT = 'ProjectAuditLogComponent',
|
||||
SYSTEM_RECENT_LOG_COMPONENT = 'SystemRecentLogComponent',
|
||||
SYSTEM_USER_COMPONENT = 'SystemUserComponent',
|
||||
|
|
|
@ -428,7 +428,10 @@
|
|||
"NAME_REQUIRED": "Name ist erforderlich",
|
||||
"NOTIFY_TYPE": "Benachrichtigungstyp",
|
||||
"EVENT_TYPE": "Event Typ",
|
||||
"EVENT_TYPE_REQUIRED": "Mindestens ein Event Typ ist erforderlich"
|
||||
"EVENT_TYPE_REQUIRED": "Mindestens ein Event Typ ist erforderlich",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP": {
|
||||
"GROUP": "Gruppe",
|
||||
|
|
|
@ -428,7 +428,10 @@
|
|||
"NAME_REQUIRED": "Name is required",
|
||||
"NOTIFY_TYPE": "Notify Type",
|
||||
"EVENT_TYPE": "Event Type",
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type"
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP": {
|
||||
"GROUP": "Group",
|
||||
|
|
|
@ -429,7 +429,10 @@
|
|||
"NAME_REQUIRED": "Name is required",
|
||||
"NOTIFY_TYPE": "Notify Type",
|
||||
"EVENT_TYPE": "Event Type",
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type"
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP": {
|
||||
"GROUP": "Group",
|
||||
|
|
|
@ -420,7 +420,10 @@
|
|||
"NAME_REQUIRED": "Le nom est requis",
|
||||
"NOTIFY_TYPE": "Type de notification",
|
||||
"EVENT_TYPE": "Types d'évènement",
|
||||
"EVENT_TYPE_REQUIRED": "Au moins un type d'évènement est nécessaire"
|
||||
"EVENT_TYPE_REQUIRED": "Au moins un type d'évènement est nécessaire",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP": {
|
||||
"Group": "Groupe",
|
||||
|
|
|
@ -460,7 +460,10 @@
|
|||
"NAME_REQUIRED": "Nome é obrigatório",
|
||||
"NOTIFY_TYPE": "Tipo de Notificação",
|
||||
"EVENT_TYPE": "Tipo de Evento",
|
||||
"EVENT_TYPE_REQUIRED": "Pelo menos um tipo de evento é obrigatório"
|
||||
"EVENT_TYPE_REQUIRED": "Pelo menos um tipo de evento é obrigatório",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"AUDIT_LOG": {
|
||||
"USERNAME": "Nome do usuário",
|
||||
|
|
|
@ -428,7 +428,10 @@
|
|||
"NAME_REQUIRED": "Name is required",
|
||||
"NOTIFY_TYPE": "Notify Type",
|
||||
"EVENT_TYPE": "Event Type",
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type"
|
||||
"EVENT_TYPE_REQUIRED": "Require at least one event type",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP": {
|
||||
"GROUP": "Grup",
|
||||
|
|
|
@ -427,7 +427,10 @@
|
|||
"NAME_REQUIRED": "名称为必填项",
|
||||
"NOTIFY_TYPE": "通知类型",
|
||||
"EVENT_TYPE": "事件类型",
|
||||
"EVENT_TYPE_REQUIRED": "请至少选择一种事件类型"
|
||||
"EVENT_TYPE_REQUIRED": "请至少选择一种事件类型",
|
||||
"PAYLOAD_FORMAT": "载荷形式",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "载荷数据"
|
||||
},
|
||||
"GROUP": {
|
||||
"GROUP": "组",
|
||||
|
|
|
@ -425,7 +425,10 @@
|
|||
"NAME_REQUIRED": "名稱為必填項",
|
||||
"NOTIFY_TYPE": "通知類型",
|
||||
"EVENT_TYPE": "事件類型",
|
||||
"EVENT_TYPE_REQUIRED": "請至少選擇一種事件類型"
|
||||
"EVENT_TYPE_REQUIRED": "請至少選擇一種事件類型",
|
||||
"PAYLOAD_FORMAT": "Payload Format",
|
||||
"CLOUD_EVENT": "CloudEvents",
|
||||
"PAYLOAD_DATA": "Payload Data"
|
||||
},
|
||||
"GROUP":{
|
||||
"GROUP":"組",
|
||||
|
|
Loading…
Reference in New Issue