2017-05-15 12:40:13 +02:00
|
|
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
2017-06-19 18:51:08 +02:00
|
|
|
import {
|
|
|
|
Component,
|
2019-10-10 11:42:45 +02:00
|
|
|
ElementRef,
|
2020-02-20 09:12:46 +01:00
|
|
|
Input, OnDestroy,
|
2019-10-10 11:42:45 +02:00
|
|
|
OnInit,
|
2020-02-13 08:39:29 +01:00
|
|
|
ViewChild,
|
|
|
|
|
2018-01-25 08:05:23 +01:00
|
|
|
} from "@angular/core";
|
2020-02-20 09:12:46 +01:00
|
|
|
import { forkJoin, Observable, Subject, of, Subscription } from "rxjs";
|
2019-10-10 11:42:45 +02:00
|
|
|
import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'rxjs/operators';
|
2018-05-15 11:56:33 +02:00
|
|
|
import { TranslateService } from "@ngx-translate/core";
|
2020-02-20 09:12:46 +01:00
|
|
|
import { ClrLoadingState, ClrDatagridStateInterface, ClrDatagridComparatorInterface } from "@clr/angular";
|
2017-05-15 12:40:13 +02:00
|
|
|
|
2020-02-20 09:12:46 +01:00
|
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
2019-10-21 08:17:39 +02:00
|
|
|
import {
|
2021-03-15 03:07:31 +01:00
|
|
|
Comparator, Label, LabelService, ScanningResultService,
|
|
|
|
UserPermissionService, USERSTATICPERMISSION,
|
2021-02-18 02:12:23 +01:00
|
|
|
} from "../../../../../../../shared/services";
|
2017-07-20 03:28:00 +02:00
|
|
|
import {
|
2017-12-08 08:05:52 +01:00
|
|
|
calculatePage,
|
2019-10-10 11:42:45 +02:00
|
|
|
clone,
|
|
|
|
CustomComparator,
|
2021-01-07 10:16:52 +01:00
|
|
|
DEFAULT_PAGE_SIZE,
|
2021-03-15 03:07:31 +01:00
|
|
|
formatSize,
|
|
|
|
VULNERABILITY_SCAN_STATUS,
|
|
|
|
dbEncodeURIComponent,
|
|
|
|
doSorting,
|
|
|
|
DEFAULT_SUPPORTED_MIME_TYPES,
|
|
|
|
getSortingString
|
2021-02-18 02:12:23 +01:00
|
|
|
} from "../../../../../../../shared/units/utils";
|
|
|
|
import { ImageNameInputComponent } from "../../../../../../../shared/components/image-name-input/image-name-input.component";
|
|
|
|
import { CopyInputComponent } from "../../../../../../../shared/components/push-image/copy-input.component";
|
|
|
|
import { ErrorHandler } from "../../../../../../../shared/units/error-handler";
|
|
|
|
import { ArtifactService } from "../../../artifact.service";
|
|
|
|
import { OperationService } from "../../../../../../../shared/components/operation/operation.service";
|
|
|
|
import { ChannelService } from "../../../../../../../shared/services/channel.service";
|
2020-02-20 09:12:46 +01:00
|
|
|
import {
|
|
|
|
ConfirmationButtons,
|
|
|
|
ConfirmationState,
|
|
|
|
ConfirmationTargets
|
2021-02-18 02:12:23 +01:00
|
|
|
} from "../../../../../../../shared/entities/shared.const";
|
|
|
|
import { operateChanges, OperateInfo, OperationState } from "../../../../../../../shared/components/operation/operate";
|
2020-08-11 04:22:02 +02:00
|
|
|
import {
|
|
|
|
ArtifactFront as Artifact,
|
|
|
|
mutipleFilter,
|
|
|
|
artifactPullCommands,
|
2020-11-26 07:39:33 +01:00
|
|
|
artifactDefault, ArtifactFront
|
2021-02-18 02:12:23 +01:00
|
|
|
} from '../../../artifact';
|
|
|
|
import { Project } from "../../../../../project";
|
|
|
|
import { ArtifactService as NewArtifactService } from "../../../../../../../../../ng-swagger-gen/services/artifact.service";
|
|
|
|
import { ADDITIONS } from "../../../artifact-additions/models";
|
|
|
|
import { Platform } from "../../../../../../../../../ng-swagger-gen/models/platform";
|
2020-11-26 07:39:33 +01:00
|
|
|
import { SafeUrl } from '@angular/platform-browser';
|
2021-02-18 02:12:23 +01:00
|
|
|
import { errorHandler } from "../../../../../../../shared/units/shared.utils";
|
|
|
|
import { ConfirmationDialogComponent } from "../../../../../../../shared/components/confirmation-dialog";
|
|
|
|
import { ConfirmationMessage } from "../../../../../../global-confirmation-dialog/confirmation-message";
|
|
|
|
import { ConfirmationAcknowledgement } from "../../../../../../global-confirmation-dialog/confirmation-state-message";
|
2018-04-27 11:42:58 +02:00
|
|
|
export interface LabelState {
|
2018-04-26 04:24:25 +02:00
|
|
|
iconsShow: boolean;
|
|
|
|
label: Label;
|
|
|
|
show: boolean;
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
export const AVAILABLE_TIME = '0001-01-01T00:00:00.000Z';
|
2020-10-12 11:23:22 +02:00
|
|
|
const YES: string = 'yes';
|
2017-05-15 12:40:13 +02:00
|
|
|
@Component({
|
2020-02-13 08:39:29 +01:00
|
|
|
selector: 'artifact-list-tab',
|
|
|
|
templateUrl: './artifact-list-tab.component.html',
|
|
|
|
styleUrls: ['./artifact-list-tab.component.scss']
|
2017-05-15 12:40:13 +02:00
|
|
|
})
|
2020-02-20 09:12:46 +01:00
|
|
|
export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
2017-05-15 12:40:13 +02:00
|
|
|
|
2019-01-09 09:08:56 +01:00
|
|
|
signedCon: { [key: string]: any | string[] } = {};
|
2017-05-15 12:40:13 +02:00
|
|
|
@Input() projectId: number;
|
2020-02-13 08:39:29 +01:00
|
|
|
projectName: string;
|
2018-10-29 10:01:04 +01:00
|
|
|
@Input() memberRoleID: number;
|
2017-05-15 12:40:13 +02:00
|
|
|
@Input() repoName: string;
|
2017-06-13 10:09:46 +02:00
|
|
|
@Input() isEmbedded: boolean;
|
2017-06-07 12:47:18 +02:00
|
|
|
@Input() hasSignedIn: boolean;
|
2018-03-23 02:58:06 +01:00
|
|
|
@Input() isGuest: boolean;
|
2017-06-13 10:09:46 +02:00
|
|
|
@Input() registryUrl: string;
|
|
|
|
@Input() withNotary: boolean;
|
2018-03-27 08:31:18 +02:00
|
|
|
@Input() withAdmiral: boolean;
|
2020-11-26 07:39:33 +01:00
|
|
|
artifactList: ArtifactFront[] = [];
|
2019-11-05 07:05:24 +01:00
|
|
|
availableTime = AVAILABLE_TIME;
|
2017-05-15 12:40:13 +02:00
|
|
|
showTagManifestOpened: boolean;
|
2018-10-10 05:08:26 +02:00
|
|
|
retagDialogOpened: boolean;
|
2017-05-15 12:40:13 +02:00
|
|
|
manifestInfoTitle: string;
|
2017-05-25 13:14:35 +02:00
|
|
|
digestId: string;
|
2018-01-25 08:05:23 +01:00
|
|
|
staticBackdrop = true;
|
|
|
|
closable = false;
|
2017-12-08 08:05:52 +01:00
|
|
|
lastFilteredTagName: string;
|
2018-04-23 13:00:32 +02:00
|
|
|
inprogress: boolean;
|
2018-05-07 08:08:19 +02:00
|
|
|
openLabelFilterPanel: boolean;
|
|
|
|
openLabelFilterPiece: boolean;
|
2018-10-10 05:08:26 +02:00
|
|
|
retagSrcImage: string;
|
2018-12-06 08:35:42 +01:00
|
|
|
showlabel: boolean;
|
2017-05-15 12:40:13 +02:00
|
|
|
|
2020-02-13 08:39:29 +01:00
|
|
|
pullComparator: Comparator<Artifact> = new CustomComparator<Artifact>("pull_time", "date");
|
|
|
|
pushComparator: Comparator<Artifact> = new CustomComparator<Artifact>("push_time", "date");
|
2017-05-25 13:14:35 +02:00
|
|
|
|
2020-02-13 08:39:29 +01:00
|
|
|
loading = true;
|
2018-01-25 08:05:23 +01:00
|
|
|
copyFailed = false;
|
2020-02-13 08:39:29 +01:00
|
|
|
selectedRow: Artifact[] = [];
|
2017-06-22 10:44:34 +02:00
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
imageLabels: LabelState[] = [];
|
|
|
|
imageStickLabels: LabelState[] = [];
|
|
|
|
imageFilterLabels: LabelState[] = [];
|
2018-03-23 02:58:06 +01:00
|
|
|
|
|
|
|
labelListOpen = false;
|
2020-02-13 08:39:29 +01:00
|
|
|
selectedTag: Artifact[];
|
2019-01-09 09:08:56 +01:00
|
|
|
labelNameFilter: Subject<string> = new Subject<string>();
|
|
|
|
stickLabelNameFilter: Subject<string> = new Subject<string>();
|
2018-03-23 02:58:06 +01:00
|
|
|
filterOnGoing: boolean;
|
2018-04-26 04:24:25 +02:00
|
|
|
stickName = '';
|
2018-04-02 08:45:52 +02:00
|
|
|
filterName = '';
|
2018-03-23 02:58:06 +01:00
|
|
|
initFilter = {
|
|
|
|
name: '',
|
|
|
|
description: '',
|
|
|
|
color: '',
|
|
|
|
scope: '',
|
|
|
|
project_id: 0,
|
2018-04-26 04:24:25 +02:00
|
|
|
};
|
2018-03-23 02:58:06 +01:00
|
|
|
filterOneLabel: Label = this.initFilter;
|
|
|
|
|
2020-09-22 11:42:01 +02:00
|
|
|
@ViewChild("confirmationDialog")
|
2017-05-15 12:40:13 +02:00
|
|
|
confirmationDialog: ConfirmationDialogComponent;
|
|
|
|
|
2020-09-22 11:42:01 +02:00
|
|
|
@ViewChild("imageNameInput")
|
2018-10-10 05:08:26 +02:00
|
|
|
imageNameInput: ImageNameInputComponent;
|
|
|
|
|
2020-09-22 11:42:01 +02:00
|
|
|
@ViewChild("digestTarget") textInput: ElementRef;
|
|
|
|
@ViewChild("copyInput") copyInput: CopyInputComponent;
|
2017-09-28 06:59:07 +02:00
|
|
|
|
2017-12-08 08:05:52 +01:00
|
|
|
pageSize: number = DEFAULT_PAGE_SIZE;
|
|
|
|
currentPage = 1;
|
|
|
|
totalCount = 0;
|
2020-03-10 05:07:56 +01:00
|
|
|
currentState: ClrDatagridStateInterface;
|
2017-06-22 10:44:34 +02:00
|
|
|
|
2019-01-09 09:08:56 +01:00
|
|
|
hasAddLabelImagePermission: boolean;
|
|
|
|
hasRetagImagePermission: boolean;
|
|
|
|
hasDeleteImagePermission: boolean;
|
|
|
|
hasScanImagePermission: boolean;
|
2019-10-10 11:42:45 +02:00
|
|
|
hasEnabledScanner: boolean;
|
|
|
|
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
2019-11-12 03:25:03 +01:00
|
|
|
onSendingScanCommand: boolean;
|
2020-02-13 08:39:29 +01:00
|
|
|
|
2020-02-20 09:12:46 +01:00
|
|
|
artifactDigest: string;
|
|
|
|
depth: string;
|
|
|
|
hasInit: boolean = false;
|
|
|
|
triggerSub: Subscription;
|
|
|
|
labelNameFilterSub: Subscription;
|
|
|
|
stickLabelNameFilterSub: Subscription;
|
2020-02-25 09:07:21 +01:00
|
|
|
mutipleFilter = clone(mutipleFilter);
|
|
|
|
filterByType: string = this.mutipleFilter[0].filterBy;
|
|
|
|
openSelectFilterPiece = false;
|
2020-03-10 05:07:56 +01:00
|
|
|
// could Pagination filter
|
|
|
|
filters: string[];
|
2020-03-24 03:56:18 +01:00
|
|
|
|
|
|
|
scanFiinishArtifactLength: number = 0;
|
|
|
|
onScanArtifactsLength: number = 0;
|
2017-05-15 12:40:13 +02:00
|
|
|
constructor(
|
2020-02-20 09:12:46 +01:00
|
|
|
private errorHandlerService: ErrorHandler,
|
2019-01-09 09:08:56 +01:00
|
|
|
private userPermissionService: UserPermissionService,
|
2018-03-23 02:58:06 +01:00
|
|
|
private labelService: LabelService,
|
2020-02-24 02:59:06 +01:00
|
|
|
private artifactService: ArtifactService,
|
|
|
|
private newArtifactService: NewArtifactService,
|
2017-05-15 12:40:13 +02:00
|
|
|
private translateService: TranslateService,
|
2018-05-21 13:05:38 +02:00
|
|
|
private operationService: OperationService,
|
2019-10-10 11:42:45 +02:00
|
|
|
private channel: ChannelService,
|
2020-02-13 08:39:29 +01:00
|
|
|
private activatedRoute: ActivatedRoute,
|
2020-02-20 09:12:46 +01:00
|
|
|
private scanningService: ScanningResultService,
|
|
|
|
private router: Router,
|
|
|
|
) {
|
|
|
|
}
|
2017-05-15 12:40:13 +02:00
|
|
|
ngOnInit() {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.activatedRoute.params.subscribe(params => {
|
|
|
|
this.depth = this.activatedRoute.snapshot.params['depth'];
|
|
|
|
if (this.depth) {
|
|
|
|
const arr: string[] = this.depth.split('-');
|
|
|
|
this.artifactDigest = this.depth.split('-')[arr.length - 1];
|
|
|
|
}
|
|
|
|
if (this.hasInit) {
|
|
|
|
this.currentPage = 1;
|
|
|
|
this.totalCount = 0;
|
|
|
|
const st: ClrDatagridStateInterface = {page: {from: 0, to: this.pageSize - 1, size: this.pageSize}};
|
|
|
|
this.clrLoad(st);
|
|
|
|
}
|
|
|
|
this.init();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
|
|
if (this.triggerSub) {
|
|
|
|
this.triggerSub.unsubscribe();
|
|
|
|
this.triggerSub = null;
|
|
|
|
}
|
|
|
|
if (this.labelNameFilterSub) {
|
|
|
|
this.labelNameFilterSub.unsubscribe();
|
|
|
|
this.labelNameFilterSub = null;
|
|
|
|
}
|
|
|
|
if (this.stickLabelNameFilterSub) {
|
|
|
|
this.stickLabelNameFilterSub.unsubscribe();
|
|
|
|
this.stickLabelNameFilterSub = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
init() {
|
|
|
|
this.hasInit = true;
|
|
|
|
this.depth = this.activatedRoute.snapshot.params['depth'];
|
|
|
|
if (this.depth) {
|
|
|
|
const arr: string[] = this.depth.split('-');
|
|
|
|
this.artifactDigest = this.depth.split('-')[arr.length - 1];
|
|
|
|
}
|
2017-06-13 10:09:46 +02:00
|
|
|
if (!this.projectId) {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error("Project ID cannot be unset.");
|
2017-05-15 12:40:13 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
const resolverData = this.activatedRoute.snapshot.data;
|
|
|
|
if (resolverData) {
|
|
|
|
const pro: Project = <Project>resolverData['projectResolver'];
|
|
|
|
this.projectName = pro.name;
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
|
2019-10-10 11:42:45 +02:00
|
|
|
this.getProjectScanner();
|
2017-06-13 10:09:46 +02:00
|
|
|
if (!this.repoName) {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error("Repo name cannot be unset.");
|
2017-05-15 12:40:13 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
if (!this.triggerSub) {
|
|
|
|
this.triggerSub = this.artifactService.TriggerArtifactChan$.subscribe(res => {
|
|
|
|
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
|
|
|
|
this.clrLoad(st);
|
|
|
|
});
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2018-03-23 02:58:06 +01:00
|
|
|
this.lastFilteredTagName = '';
|
2020-02-20 09:12:46 +01:00
|
|
|
if (!this.labelNameFilterSub) {
|
|
|
|
this.labelNameFilterSub = this.labelNameFilter
|
|
|
|
.pipe(debounceTime(500))
|
|
|
|
.pipe(distinctUntilChanged())
|
|
|
|
.subscribe((name: string) => {
|
|
|
|
if (this.filterName.length) {
|
|
|
|
this.filterOnGoing = true;
|
|
|
|
this.imageFilterLabels.forEach(data => {
|
2020-10-30 07:43:59 +01:00
|
|
|
data.show = data.label.name.indexOf(this.filterName) !== -1;
|
2020-02-20 09:12:46 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!this.stickLabelNameFilterSub) {
|
|
|
|
this.stickLabelNameFilterSub = this.stickLabelNameFilter
|
|
|
|
.pipe(debounceTime(500))
|
|
|
|
.pipe(distinctUntilChanged())
|
|
|
|
.subscribe((name: string) => {
|
|
|
|
if (this.stickName.length) {
|
|
|
|
this.filterOnGoing = true;
|
|
|
|
this.imageStickLabels.forEach(data => {
|
2020-10-30 07:43:59 +01:00
|
|
|
data.show = data.label.name.indexOf(this.stickName) !== -1;
|
2020-02-20 09:12:46 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-04-30 09:32:47 +02:00
|
|
|
this.getImagePermissionRule(this.projectId);
|
2017-12-08 08:05:52 +01:00
|
|
|
}
|
|
|
|
|
2018-03-23 02:58:06 +01:00
|
|
|
public get filterLabelPieceWidth() {
|
|
|
|
let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115;
|
|
|
|
return len > 210 ? 210 : len;
|
2019-01-09 09:08:56 +01:00
|
|
|
}
|
2020-02-25 09:07:21 +01:00
|
|
|
doSearchArtifactByFilter(filterWords) {
|
|
|
|
this.lastFilteredTagName = filterWords;
|
2017-12-08 08:05:52 +01:00
|
|
|
this.currentPage = 1;
|
|
|
|
|
2020-03-10 05:07:56 +01:00
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
2020-02-13 08:39:29 +01:00
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
}
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
this.filters = [];
|
|
|
|
if (this.lastFilteredTagName) {
|
|
|
|
this.filters.push(`${this.filterByType}=~${this.lastFilteredTagName}`);
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
|
|
|
this.clrLoad(st);
|
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
// todo
|
|
|
|
clrDgRefresh(state: ClrDatagridStateInterface) {
|
|
|
|
setTimeout(() => {
|
|
|
|
this.clrLoad(state);
|
|
|
|
});
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
clrLoad(state: ClrDatagridStateInterface): void {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.artifactList = [];
|
|
|
|
this.loading = true;
|
|
|
|
if (!state || !state.page) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-16 11:17:33 +02:00
|
|
|
this.pageSize = state.page.size;
|
2020-02-20 09:12:46 +01:00
|
|
|
this.selectedRow = [];
|
|
|
|
// Keep it for future filtering and sorting
|
|
|
|
|
|
|
|
let pageNumber: number = calculatePage(state);
|
|
|
|
if (pageNumber <= 0) { pageNumber = 1; }
|
|
|
|
let sortBy: any = '';
|
|
|
|
if (state.sort) {
|
|
|
|
sortBy = state.sort.by as string | ClrDatagridComparatorInterface<any>;
|
|
|
|
sortBy = sortBy.fieldName ? sortBy.fieldName : sortBy;
|
|
|
|
sortBy = state.sort.reverse ? `-${sortBy}` : sortBy;
|
|
|
|
}
|
|
|
|
this.currentState = state;
|
|
|
|
|
|
|
|
// Pagination
|
2020-02-25 09:07:21 +01:00
|
|
|
let params: any = {};
|
2020-02-20 09:12:46 +01:00
|
|
|
if (pageNumber && this.pageSize) {
|
2020-02-25 09:07:21 +01:00
|
|
|
params.page = pageNumber;
|
|
|
|
params.pageSize = this.pageSize;
|
2020-02-20 09:12:46 +01:00
|
|
|
}
|
|
|
|
if (sortBy) {
|
2020-02-25 09:07:21 +01:00
|
|
|
params.sort = sortBy;
|
2020-02-20 09:12:46 +01:00
|
|
|
}
|
2020-03-10 05:07:56 +01:00
|
|
|
if (this.filters && this.filters.length) {
|
|
|
|
let q = "";
|
|
|
|
this.filters.forEach(item => {
|
|
|
|
q += item;
|
2020-02-20 09:12:46 +01:00
|
|
|
});
|
2020-03-10 05:07:56 +01:00
|
|
|
params.q = encodeURIComponent(q);
|
2020-02-20 09:12:46 +01:00
|
|
|
}
|
|
|
|
if (this.artifactDigest) {
|
2020-02-25 09:07:21 +01:00
|
|
|
const artifactParam: NewArtifactService.GetArtifactParams = {
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
projectName: this.projectName,
|
|
|
|
reference: this.artifactDigest,
|
|
|
|
withImmutableStatus: true,
|
|
|
|
withLabel: true,
|
|
|
|
withScanOverview: true,
|
2021-01-07 10:16:52 +01:00
|
|
|
withTag: false,
|
|
|
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
|
2020-02-25 09:07:21 +01:00
|
|
|
};
|
|
|
|
this.newArtifactService.getArtifact(artifactParam).subscribe(
|
2020-02-20 09:12:46 +01:00
|
|
|
res => {
|
|
|
|
let observableLists: Observable<Artifact>[] = [];
|
2020-04-09 07:08:39 +02:00
|
|
|
let platFormAttr: { platform: Platform }[] = [];
|
2020-02-20 09:12:46 +01:00
|
|
|
this.totalCount = res.references.length;
|
|
|
|
res.references.forEach((child, index) => {
|
|
|
|
if (index >= (pageNumber - 1) * this.pageSize && index < pageNumber * this.pageSize) {
|
2020-02-25 09:07:21 +01:00
|
|
|
let childParams: NewArtifactService.GetArtifactParams = {
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
projectName: this.projectName,
|
2020-03-05 08:33:28 +01:00
|
|
|
reference: child.child_digest,
|
|
|
|
withImmutableStatus: true,
|
|
|
|
withLabel: true,
|
|
|
|
withScanOverview: true,
|
2021-01-07 10:16:52 +01:00
|
|
|
withTag: false,
|
|
|
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
|
2020-02-25 09:07:21 +01:00
|
|
|
};
|
2020-04-09 07:08:39 +02:00
|
|
|
platFormAttr.push({platform: child.platform});
|
2020-02-25 09:07:21 +01:00
|
|
|
observableLists.push(this.newArtifactService.getArtifact(childParams));
|
2020-02-20 09:12:46 +01:00
|
|
|
}
|
|
|
|
});
|
2020-02-13 08:39:29 +01:00
|
|
|
forkJoin(observableLists).pipe(finalize(() => {
|
|
|
|
this.loading = false;
|
|
|
|
})).subscribe(artifacts => {
|
|
|
|
this.artifactList = artifacts;
|
2021-03-15 03:07:31 +01:00
|
|
|
this.artifactList = doSorting<ArtifactFront>(this.artifactList, state);
|
2020-04-09 07:08:39 +02:00
|
|
|
this.artifactList.forEach((artifact, index) => {
|
|
|
|
artifact.platform = clone(platFormAttr[index].platform);
|
|
|
|
});
|
2020-04-12 19:41:39 +02:00
|
|
|
this.getPullCommand(this.artifactList);
|
2020-11-26 07:39:33 +01:00
|
|
|
this.getArtifactTagsAsync(this.artifactList);
|
2020-08-11 04:22:02 +02:00
|
|
|
this.getIconsFromBackEnd();
|
2020-02-13 08:39:29 +01:00
|
|
|
}, error => {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error(error);
|
2020-02-13 08:39:29 +01:00
|
|
|
});
|
2020-02-20 09:12:46 +01:00
|
|
|
}, error => {
|
|
|
|
this.loading = false;
|
|
|
|
}
|
|
|
|
);
|
2020-02-13 08:39:29 +01:00
|
|
|
} else {
|
2020-02-25 09:07:21 +01:00
|
|
|
let listArtifactParams: NewArtifactService.ListArtifactsParams = {
|
|
|
|
projectName: this.projectName,
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
withLabel: true,
|
|
|
|
withScanOverview: true,
|
2021-01-07 10:16:52 +01:00
|
|
|
withTag: false,
|
2021-03-15 03:07:31 +01:00
|
|
|
sort: getSortingString(state),
|
2021-01-07 10:16:52 +01:00
|
|
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
|
2020-02-25 09:07:21 +01:00
|
|
|
};
|
|
|
|
Object.assign(listArtifactParams, params);
|
|
|
|
this.newArtifactService.listArtifactsResponse(listArtifactParams)
|
2020-02-20 09:12:46 +01:00
|
|
|
.pipe(finalize(() => this.loading = false))
|
|
|
|
.subscribe(res => {
|
|
|
|
if (res.headers) {
|
|
|
|
let xHeader: string = res.headers.get("X-Total-Count");
|
|
|
|
if (xHeader) {
|
|
|
|
this.totalCount = parseInt(xHeader, 0);
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
this.artifactList = res.body;
|
2020-04-21 11:04:02 +02:00
|
|
|
this.artifactList = doSorting<Artifact>(this.artifactList, state);
|
|
|
|
|
2020-04-12 19:41:39 +02:00
|
|
|
this.getPullCommand(this.artifactList);
|
2020-11-26 07:39:33 +01:00
|
|
|
this.getArtifactTagsAsync(this.artifactList);
|
2020-08-11 04:22:02 +02:00
|
|
|
this.getIconsFromBackEnd();
|
2020-02-20 09:12:46 +01:00
|
|
|
}, error => {
|
|
|
|
// error
|
|
|
|
this.errorHandlerService.error(error);
|
|
|
|
});
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2017-12-08 08:05:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
refresh() {
|
2020-03-10 05:07:56 +01:00
|
|
|
this.currentPage = 1;
|
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
|
|
|
}
|
|
|
|
this.clrLoad(st);
|
2017-05-15 12:40:13 +02:00
|
|
|
}
|
2020-04-12 19:41:39 +02:00
|
|
|
|
|
|
|
getPullCommand(artifactList: Artifact[]) {
|
2020-02-25 03:36:24 +01:00
|
|
|
artifactList.forEach(artifact => {
|
2020-04-12 19:41:39 +02:00
|
|
|
artifact.pullCommand = '';
|
|
|
|
artifactPullCommands.forEach(artifactPullCommand => {
|
|
|
|
if (artifactPullCommand.type === artifact.type) {
|
|
|
|
artifact.pullCommand =
|
|
|
|
`${artifactPullCommand.pullCommand} ${this.registryUrl}/${this.projectName}/${this.repoName}@${artifact.digest}`;
|
2020-02-25 03:36:24 +01:00
|
|
|
}
|
2020-04-12 19:41:39 +02:00
|
|
|
});
|
2020-02-25 03:36:24 +01:00
|
|
|
});
|
|
|
|
}
|
2018-03-23 02:58:06 +01:00
|
|
|
getAllLabels(): void {
|
2019-02-01 10:00:01 +01:00
|
|
|
forkJoin(this.labelService.getGLabels(), this.labelService.getPLabels(this.projectId)).subscribe(results => {
|
|
|
|
results.forEach(labels => {
|
|
|
|
labels.forEach(data => {
|
2019-01-09 09:08:56 +01:00
|
|
|
this.imageLabels.push({ 'iconsShow': false, 'label': data, 'show': true });
|
2018-03-23 02:58:06 +01:00
|
|
|
});
|
|
|
|
});
|
2019-02-01 10:00:01 +01:00
|
|
|
this.imageFilterLabels = clone(this.imageLabels);
|
|
|
|
this.imageStickLabels = clone(this.imageLabels);
|
2020-02-20 09:12:46 +01:00
|
|
|
}, error => this.errorHandlerService.error(error));
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-13 08:39:29 +01:00
|
|
|
labelSelectedChange(artifact?: Artifact[]): void {
|
2020-06-02 04:19:02 +02:00
|
|
|
this.imageStickLabels.forEach(data => {
|
|
|
|
data.iconsShow = false;
|
|
|
|
data.show = true;
|
|
|
|
});
|
|
|
|
if (artifact && artifact[0].labels && artifact[0].labels.length) {
|
|
|
|
artifact[0].labels.forEach((labelInfo: Label) => {
|
|
|
|
let findedLabel = this.imageStickLabels.find(data => labelInfo.id === data['label'].id);
|
|
|
|
if (findedLabel) {
|
2018-04-26 04:24:25 +02:00
|
|
|
this.imageStickLabels.splice(this.imageStickLabels.indexOf(findedLabel), 1);
|
|
|
|
this.imageStickLabels.unshift(findedLabel);
|
|
|
|
findedLabel.iconsShow = true;
|
2020-06-02 04:19:02 +02:00
|
|
|
}
|
|
|
|
});
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-12 03:25:03 +01:00
|
|
|
addLabels(): void {
|
2018-03-23 02:58:06 +01:00
|
|
|
this.labelListOpen = true;
|
2019-11-12 03:25:03 +01:00
|
|
|
this.selectedTag = this.selectedRow;
|
2018-04-26 04:24:25 +02:00
|
|
|
this.stickName = '';
|
2019-11-12 03:25:03 +01:00
|
|
|
this.labelSelectedChange(this.selectedRow);
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
2018-04-04 11:09:10 +02:00
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
stickLabel(labelInfo: LabelState): void {
|
2018-03-27 04:12:37 +02:00
|
|
|
if (labelInfo && !labelInfo.iconsShow) {
|
2018-04-04 11:09:10 +02:00
|
|
|
this.selectLabel(labelInfo);
|
|
|
|
}
|
|
|
|
if (labelInfo && labelInfo.iconsShow) {
|
|
|
|
this.unSelectLabel(labelInfo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
selectLabel(labelInfo: LabelState): void {
|
2018-04-23 13:00:32 +02:00
|
|
|
if (!this.inprogress) {
|
|
|
|
this.inprogress = true;
|
2018-03-23 02:58:06 +01:00
|
|
|
this.selectedRow = this.selectedTag;
|
2020-02-25 09:07:21 +01:00
|
|
|
let params: NewArtifactService.AddLabelParams = {
|
|
|
|
projectName: this.projectName,
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
reference: this.selectedRow[0].digest,
|
|
|
|
label: labelInfo.label
|
|
|
|
};
|
|
|
|
this.newArtifactService.addLabel(params).subscribe(res => {
|
2018-03-23 02:58:06 +01:00
|
|
|
this.refresh();
|
2018-04-26 04:24:25 +02:00
|
|
|
// set the selected label in front
|
|
|
|
this.imageStickLabels.splice(this.imageStickLabels.indexOf(labelInfo), 1);
|
2018-05-04 04:27:40 +02:00
|
|
|
this.imageStickLabels.some((data, i) => {
|
|
|
|
if (!data.iconsShow) {
|
|
|
|
this.imageStickLabels.splice(i, 0, labelInfo);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
2018-04-26 04:24:25 +02:00
|
|
|
|
2018-06-15 05:03:47 +02:00
|
|
|
// when is the last one
|
|
|
|
if (this.imageStickLabels.every(data => data.iconsShow === true)) {
|
|
|
|
this.imageStickLabels.push(labelInfo);
|
|
|
|
}
|
|
|
|
|
2018-04-23 13:00:32 +02:00
|
|
|
labelInfo.iconsShow = true;
|
|
|
|
this.inprogress = false;
|
2019-03-12 11:03:47 +01:00
|
|
|
}, err => {
|
2018-04-23 13:00:32 +02:00
|
|
|
this.inprogress = false;
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error(err);
|
2018-03-23 02:58:06 +01:00
|
|
|
});
|
2018-04-23 13:00:32 +02:00
|
|
|
}
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
2017-12-21 04:00:12 +01:00
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
unSelectLabel(labelInfo: LabelState): void {
|
2019-01-09 09:08:56 +01:00
|
|
|
if (!this.inprogress) {
|
|
|
|
this.inprogress = true;
|
|
|
|
let labelId = labelInfo.label.id;
|
|
|
|
this.selectedRow = this.selectedTag;
|
2020-02-25 09:07:21 +01:00
|
|
|
let params: NewArtifactService.RemoveLabelParams = {
|
|
|
|
projectName: this.projectName,
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
reference: this.selectedRow[0].digest,
|
|
|
|
labelId: labelId
|
|
|
|
};
|
|
|
|
this.newArtifactService.removeLabel(params).subscribe(res => {
|
2019-01-09 09:08:56 +01:00
|
|
|
this.refresh();
|
|
|
|
|
|
|
|
// insert the unselected label to groups with the same icons
|
|
|
|
this.sortOperation(this.imageStickLabels, labelInfo);
|
2018-04-23 13:00:32 +02:00
|
|
|
labelInfo.iconsShow = false;
|
|
|
|
this.inprogress = false;
|
2019-03-12 11:03:47 +01:00
|
|
|
}, err => {
|
2018-04-23 13:00:32 +02:00
|
|
|
this.inprogress = false;
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error(err);
|
2018-03-23 02:58:06 +01:00
|
|
|
});
|
2018-04-23 13:00:32 +02:00
|
|
|
}
|
2018-04-04 11:09:10 +02:00
|
|
|
}
|
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
rightFilterLabel(labelInfo: LabelState): void {
|
2018-04-04 11:09:10 +02:00
|
|
|
if (labelInfo) {
|
|
|
|
if (!labelInfo.iconsShow) {
|
|
|
|
this.filterLabel(labelInfo);
|
2018-12-06 08:35:42 +01:00
|
|
|
this.showlabel = true;
|
2018-04-04 11:09:10 +02:00
|
|
|
} else {
|
|
|
|
this.unFilterLabel(labelInfo);
|
2018-12-06 08:35:42 +01:00
|
|
|
this.showlabel = false;
|
2018-04-04 11:09:10 +02:00
|
|
|
}
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
filterLabel(labelInfo: LabelState): void {
|
|
|
|
let labelId = labelInfo.label.id;
|
2018-04-26 04:24:25 +02:00
|
|
|
this.imageFilterLabels.filter(data => {
|
2020-10-30 07:43:59 +01:00
|
|
|
data.iconsShow = data.label.id === labelId;
|
2018-04-26 04:24:25 +02:00
|
|
|
});
|
2019-01-09 09:08:56 +01:00
|
|
|
this.filterOneLabel = labelInfo.label;
|
2018-03-23 02:58:06 +01:00
|
|
|
|
2019-01-09 09:08:56 +01:00
|
|
|
// reload data
|
|
|
|
this.currentPage = 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
2019-01-09 09:08:56 +01:00
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
}
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
|
|
|
|
this.filters = [`${this.filterByType}=(${labelId})`];
|
2019-01-09 09:08:56 +01:00
|
|
|
|
|
|
|
this.clrLoad(st);
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
|
2018-04-27 11:42:58 +02:00
|
|
|
unFilterLabel(labelInfo: LabelState): void {
|
2018-04-26 04:24:25 +02:00
|
|
|
this.filterOneLabel = this.initFilter;
|
|
|
|
labelInfo.iconsShow = false;
|
|
|
|
// reload data
|
|
|
|
this.currentPage = 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
2018-04-26 04:24:25 +02:00
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
}
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
|
|
|
|
this.filters = [];
|
2018-04-26 04:24:25 +02:00
|
|
|
this.clrLoad(st);
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
|
2018-05-07 08:08:19 +02:00
|
|
|
closeFilter(): void {
|
|
|
|
this.openLabelFilterPanel = false;
|
|
|
|
}
|
2020-10-30 07:43:59 +01:00
|
|
|
reSortImageFilterLabels() {
|
|
|
|
if (this.imageFilterLabels && this.imageFilterLabels.length) {
|
|
|
|
for (let i = 0; i < this.imageFilterLabels.length; i++) {
|
|
|
|
if (this.imageFilterLabels[i].iconsShow) {
|
|
|
|
const arr: LabelState[] = this.imageFilterLabels.splice(i, 1);
|
|
|
|
this.imageFilterLabels.unshift(...arr);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
getFilterPlaceholder(): string {
|
|
|
|
return this.showlabel ? "" : 'ARTIFACT.FILTER_FOR_ARTIFACTS';
|
|
|
|
}
|
2018-05-07 08:08:19 +02:00
|
|
|
openFlagEvent(isOpen: boolean): void {
|
|
|
|
if (isOpen) {
|
|
|
|
this.openLabelFilterPanel = true;
|
2020-10-30 07:43:59 +01:00
|
|
|
// every time when filer panel opens, resort imageFilterLabels labels
|
|
|
|
this.reSortImageFilterLabels();
|
2018-05-07 08:08:19 +02:00
|
|
|
this.openLabelFilterPiece = true;
|
2020-02-25 09:07:21 +01:00
|
|
|
this.openSelectFilterPiece = true;
|
2018-05-07 08:08:19 +02:00
|
|
|
this.filterName = '';
|
|
|
|
// redisplay all labels
|
|
|
|
this.imageFilterLabels.forEach(data => {
|
2020-10-30 07:43:59 +01:00
|
|
|
data.show = data.label.name.indexOf(this.filterName) !== -1;
|
2018-05-07 08:08:19 +02:00
|
|
|
});
|
2019-01-09 09:08:56 +01:00
|
|
|
} else {
|
2018-05-07 08:08:19 +02:00
|
|
|
this.openLabelFilterPanel = false;
|
|
|
|
this.openLabelFilterPiece = false;
|
2020-02-25 09:07:21 +01:00
|
|
|
this.openSelectFilterPiece = false;
|
2018-05-07 08:08:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-04-02 08:45:52 +02:00
|
|
|
handleInputFilter() {
|
|
|
|
if (this.filterName.length) {
|
|
|
|
this.labelNameFilter.next(this.filterName);
|
2018-09-04 09:01:50 +02:00
|
|
|
} else {
|
2018-04-26 04:24:25 +02:00
|
|
|
this.imageFilterLabels.every(data => data.show = true);
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-02 08:45:52 +02:00
|
|
|
handleStickInputFilter() {
|
|
|
|
if (this.stickName.length) {
|
|
|
|
this.stickLabelNameFilter.next(this.stickName);
|
2018-09-04 09:01:50 +02:00
|
|
|
} else {
|
2018-04-26 04:24:25 +02:00
|
|
|
this.imageStickLabels.every(data => data.show = true);
|
2018-03-23 02:58:06 +01:00
|
|
|
}
|
|
|
|
}
|
2017-12-21 04:00:12 +01:00
|
|
|
|
2018-04-26 04:24:25 +02:00
|
|
|
// insert the unselected label to groups with the same icons
|
2018-04-27 11:42:58 +02:00
|
|
|
sortOperation(labelList: LabelState[], labelInfo: LabelState): void {
|
2018-04-26 04:24:25 +02:00
|
|
|
labelList.some((data, i) => {
|
|
|
|
if (!data.iconsShow) {
|
|
|
|
if (data.label.scope === labelInfo.label.scope) {
|
|
|
|
labelList.splice(i, 0, labelInfo);
|
|
|
|
labelList.splice(labelList.indexOf(labelInfo, 0), 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (data.label.scope !== labelInfo.label.scope && i === labelList.length - 1) {
|
|
|
|
labelList.push(labelInfo);
|
|
|
|
labelList.splice(labelList.indexOf(labelInfo), 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2017-09-21 08:50:15 +02:00
|
|
|
sizeTransform(tagSize: string): string {
|
2020-02-13 08:39:29 +01:00
|
|
|
return formatSize(tagSize);
|
2017-09-21 08:50:15 +02:00
|
|
|
}
|
|
|
|
|
2019-11-12 03:25:03 +01:00
|
|
|
retag() {
|
2020-03-16 11:59:21 +01:00
|
|
|
if (this.selectedRow && this.selectedRow.length && !this.depth) {
|
2019-01-09 09:08:56 +01:00
|
|
|
this.retagDialogOpened = true;
|
2019-11-12 03:25:03 +01:00
|
|
|
this.retagSrcImage = this.repoName + ":" + this.selectedRow[0].digest;
|
2018-10-11 17:48:56 +02:00
|
|
|
}
|
2018-10-10 05:08:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onRetag() {
|
2020-02-24 02:59:06 +01:00
|
|
|
let params: NewArtifactService.CopyArtifactParams = {
|
|
|
|
projectName: this.imageNameInput.projectName.value,
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.imageNameInput.repoName.value),
|
2020-02-24 02:59:06 +01:00
|
|
|
from: `${this.projectName}/${this.repoName}@${this.selectedRow[0].digest}`,
|
|
|
|
};
|
|
|
|
this.newArtifactService.CopyArtifact(params)
|
2019-01-09 09:08:56 +01:00
|
|
|
.pipe(finalize(() => {
|
2018-12-19 03:02:48 +01:00
|
|
|
this.imageNameInput.form.reset();
|
2020-04-09 07:08:39 +02:00
|
|
|
this.retagDialogOpened = false;
|
2019-01-09 09:08:56 +01:00
|
|
|
}))
|
|
|
|
.subscribe(response => {
|
|
|
|
this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.info(res);
|
2019-01-09 09:08:56 +01:00
|
|
|
});
|
|
|
|
}, error => {
|
2020-02-20 09:12:46 +01:00
|
|
|
this.errorHandlerService.error(error);
|
2019-01-09 09:08:56 +01:00
|
|
|
});
|
2018-10-10 05:08:26 +02:00
|
|
|
}
|
|
|
|
|
2020-02-13 08:39:29 +01:00
|
|
|
deleteArtifact() {
|
2020-03-16 11:59:21 +01:00
|
|
|
if (this.selectedRow && this.selectedRow.length && !this.depth) {
|
2020-02-13 08:39:29 +01:00
|
|
|
let artifactNames: string[] = [];
|
|
|
|
this.selectedRow.forEach(artifact => {
|
|
|
|
artifactNames.push(artifact.digest.slice(0, 15));
|
2017-12-21 04:00:12 +01:00
|
|
|
});
|
|
|
|
|
2017-05-15 12:40:13 +02:00
|
|
|
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
|
2020-03-20 03:04:14 +01:00
|
|
|
titleKey = "REPOSITORY.DELETION_TITLE_ARTIFACT";
|
|
|
|
summaryKey = "REPOSITORY.DELETION_SUMMARY_ARTIFACT";
|
2017-12-21 04:00:12 +01:00
|
|
|
buttons = ConfirmationButtons.DELETE_CANCEL;
|
2020-02-13 08:39:29 +01:00
|
|
|
content = artifactNames.join(" , ");
|
2017-05-15 12:40:13 +02:00
|
|
|
let message = new ConfirmationMessage(
|
|
|
|
titleKey,
|
|
|
|
summaryKey,
|
|
|
|
content,
|
2019-11-12 03:25:03 +01:00
|
|
|
this.selectedRow,
|
2017-05-15 12:40:13 +02:00
|
|
|
ConfirmationTargets.TAG,
|
|
|
|
buttons);
|
|
|
|
this.confirmationDialog.open(message);
|
|
|
|
}
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
deleteArtifactobservableLists: Observable<any>[] = [];
|
2017-12-21 04:00:12 +01:00
|
|
|
confirmDeletion(message: ConfirmationAcknowledgement) {
|
|
|
|
if (message &&
|
2019-01-09 09:08:56 +01:00
|
|
|
message.source === ConfirmationTargets.TAG
|
|
|
|
&& message.state === ConfirmationState.CONFIRMED) {
|
2020-02-13 08:39:29 +01:00
|
|
|
let artifactList = message.data;
|
|
|
|
if (artifactList && artifactList.length) {
|
2020-02-24 02:59:06 +01:00
|
|
|
artifactList.forEach(artifact => {
|
|
|
|
this.deleteArtifactobservableLists.push(this.delOperate(artifact));
|
|
|
|
});
|
2020-02-25 09:07:21 +01:00
|
|
|
this.loading = true;
|
2020-03-16 11:59:21 +01:00
|
|
|
forkJoin(...this.deleteArtifactobservableLists).subscribe((deleteResult) => {
|
|
|
|
let deleteSuccessList = [];
|
|
|
|
let deleteErrorList = [];
|
2020-03-24 03:56:18 +01:00
|
|
|
this.deleteArtifactobservableLists = [];
|
2020-03-16 11:59:21 +01:00
|
|
|
deleteResult.forEach(result => {
|
|
|
|
if (!result) {
|
|
|
|
// delete success
|
|
|
|
deleteSuccessList.push(result);
|
|
|
|
} else {
|
|
|
|
deleteErrorList.push(result);
|
|
|
|
}
|
|
|
|
});
|
2020-03-24 03:56:18 +01:00
|
|
|
this.selectedRow = [];
|
2020-03-16 11:59:21 +01:00
|
|
|
if (deleteSuccessList.length === deleteResult.length) {
|
|
|
|
// all is success
|
|
|
|
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
|
|
|
|
this.clrLoad(st);
|
|
|
|
} else if (deleteErrorList.length === deleteResult.length) {
|
|
|
|
// all is error
|
|
|
|
this.loading = false;
|
2020-07-17 07:25:46 +02:00
|
|
|
this.errorHandlerService.error(deleteResult[deleteResult.length - 1]);
|
2020-03-16 11:59:21 +01:00
|
|
|
} else {
|
|
|
|
// some artifact delete success but it has error delete things
|
2020-07-17 07:25:46 +02:00
|
|
|
this.errorHandlerService.error(deleteErrorList[deleteErrorList.length - 1]);
|
2020-03-16 11:59:21 +01:00
|
|
|
// if delete one success refresh list
|
2020-02-24 02:59:06 +01:00
|
|
|
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
|
|
|
|
this.clrLoad(st);
|
|
|
|
}
|
|
|
|
});
|
2017-12-21 04:00:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-13 08:39:29 +01:00
|
|
|
delOperate(artifact: Artifact): Observable<any> | null {
|
2018-05-21 13:05:38 +02:00
|
|
|
// init operation info
|
|
|
|
let operMessage = new OperateInfo();
|
|
|
|
operMessage.name = 'OPERATION.DELETE_TAG';
|
2020-02-13 08:39:29 +01:00
|
|
|
operMessage.data.id = artifact.id;
|
2018-05-21 13:05:38 +02:00
|
|
|
operMessage.state = OperationState.progressing;
|
2020-02-13 08:39:29 +01:00
|
|
|
operMessage.data.name = artifact.digest;
|
2018-05-21 13:05:38 +02:00
|
|
|
this.operationService.publishInfo(operMessage);
|
2020-02-13 08:39:29 +01:00
|
|
|
// to do signature
|
|
|
|
// if (tag.signature) {
|
|
|
|
// forkJoin(this.translateService.get("BATCH.DELETED_FAILURE"),
|
|
|
|
// this.translateService.get("REPOSITORY.DELETION_SUMMARY_TAG_DENIED")).subscribe(res => {
|
|
|
|
// let wrongInfo: string = res[1] + "notary -s https://" + this.registryUrl +
|
|
|
|
// ":4443 -d ~/.docker/trust remove -p " +
|
|
|
|
// this.registryUrl + "/" + this.repoName +
|
|
|
|
// " " + name;
|
|
|
|
// operateChanges(operMessage, OperationState.failure, wrongInfo);
|
|
|
|
// });
|
|
|
|
// } else {
|
2020-02-25 09:07:21 +01:00
|
|
|
let params: NewArtifactService.DeleteArtifactParams = {
|
|
|
|
projectName: this.projectName,
|
2020-04-07 11:06:26 +02:00
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
2020-02-25 09:07:21 +01:00
|
|
|
reference: artifact.digest
|
|
|
|
};
|
|
|
|
return this.newArtifactService
|
|
|
|
.deleteArtifact(params)
|
2020-02-13 08:39:29 +01:00
|
|
|
.pipe(map(
|
|
|
|
response => {
|
|
|
|
this.translateService.get("BATCH.DELETED_SUCCESS")
|
|
|
|
.subscribe(res => {
|
|
|
|
operateChanges(operMessage, OperationState.success);
|
|
|
|
});
|
|
|
|
}), catchError(error => {
|
2020-02-20 09:12:46 +01:00
|
|
|
const message = errorHandler(error);
|
2020-02-13 08:39:29 +01:00
|
|
|
this.translateService.get(message).subscribe(res =>
|
|
|
|
operateChanges(operMessage, OperationState.failure, res)
|
|
|
|
);
|
|
|
|
return of(error);
|
|
|
|
}));
|
|
|
|
// }
|
2017-12-21 04:00:12 +01:00
|
|
|
}
|
|
|
|
|
2019-11-12 03:25:03 +01:00
|
|
|
showDigestId() {
|
2020-03-16 11:59:21 +01:00
|
|
|
if (this.selectedRow && (this.selectedRow.length === 1) && !this.depth) {
|
2018-01-25 08:05:23 +01:00
|
|
|
this.manifestInfoTitle = "REPOSITORY.COPY_DIGEST_ID";
|
2019-11-12 03:25:03 +01:00
|
|
|
this.digestId = this.selectedRow[0].digest;
|
2017-05-15 12:40:13 +02:00
|
|
|
this.showTagManifestOpened = true;
|
2017-06-22 10:44:34 +02:00
|
|
|
this.copyFailed = false;
|
2017-05-15 12:40:13 +02:00
|
|
|
}
|
|
|
|
}
|
2017-07-20 03:28:00 +02:00
|
|
|
|
2020-02-20 09:12:46 +01:00
|
|
|
goIntoArtifactSummaryPage(artifact: Artifact): void {
|
|
|
|
const relativeRouterLink: string[] = ['artifacts', artifact.digest];
|
2020-10-12 11:23:22 +02:00
|
|
|
if (this.activatedRoute.snapshot.queryParams['publicAndNotLogged'] === YES) {
|
|
|
|
this.router.navigate(relativeRouterLink , { relativeTo: this.activatedRoute, queryParams: {publicAndNotLogged: YES} });
|
|
|
|
} else {
|
|
|
|
this.router.navigate(relativeRouterLink , { relativeTo: this.activatedRoute });
|
|
|
|
}
|
2017-06-13 16:24:38 +02:00
|
|
|
}
|
2017-06-19 18:51:08 +02:00
|
|
|
|
2017-06-22 10:44:34 +02:00
|
|
|
onSuccess($event: any): void {
|
|
|
|
this.copyFailed = false;
|
2017-12-08 08:05:52 +01:00
|
|
|
// Directly close dialog
|
2017-06-22 10:44:34 +02:00
|
|
|
this.showTagManifestOpened = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
onError($event: any): void {
|
2017-12-08 08:05:52 +01:00
|
|
|
// Show error
|
2017-06-22 10:44:34 +02:00
|
|
|
this.copyFailed = true;
|
2017-12-08 08:05:52 +01:00
|
|
|
// Select all text
|
2017-07-20 03:28:00 +02:00
|
|
|
if (this.textInput) {
|
2017-06-22 10:44:34 +02:00
|
|
|
this.textInput.nativeElement.select();
|
|
|
|
}
|
|
|
|
}
|
2017-07-20 03:28:00 +02:00
|
|
|
|
2017-12-08 08:05:52 +01:00
|
|
|
// Get vulnerability scanning status
|
2020-02-13 08:39:29 +01:00
|
|
|
scanStatus(artifact: Artifact): string {
|
|
|
|
if (artifact) {
|
2020-02-20 09:12:46 +01:00
|
|
|
let so = this.handleScanOverview((<any>artifact).scan_overview);
|
2019-10-10 11:42:45 +02:00
|
|
|
if (so && so.scan_status) {
|
|
|
|
return so.scan_status;
|
|
|
|
}
|
2017-07-20 03:28:00 +02:00
|
|
|
}
|
2019-10-10 11:42:45 +02:00
|
|
|
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
|
2017-09-25 08:16:41 +02:00
|
|
|
}
|
2017-12-08 08:05:52 +01:00
|
|
|
// Whether show the 'scan now' menu
|
2019-11-12 03:25:03 +01:00
|
|
|
canScanNow(): boolean {
|
2019-01-09 09:08:56 +01:00
|
|
|
if (!this.hasScanImagePermission) { return false; }
|
2019-11-12 03:25:03 +01:00
|
|
|
if (this.onSendingScanCommand) { return false; }
|
|
|
|
let st: string = this.scanStatus(this.selectedRow[0]);
|
2019-10-18 04:46:24 +02:00
|
|
|
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
|
2017-07-20 03:28:00 +02:00
|
|
|
}
|
2019-01-09 09:08:56 +01:00
|
|
|
getImagePermissionRule(projectId: number): void {
|
2019-10-22 10:04:11 +02:00
|
|
|
const permissions = [
|
2020-03-31 08:23:49 +02:00
|
|
|
{ resource: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.VALUE.CREATE },
|
2020-02-13 08:39:29 +01:00
|
|
|
{ resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL },
|
2020-03-31 08:23:49 +02:00
|
|
|
{ resource: USERSTATICPERMISSION.ARTIFACT.KEY, action: USERSTATICPERMISSION.ARTIFACT.VALUE.DELETE },
|
2020-02-13 08:39:29 +01:00
|
|
|
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE },
|
2019-10-22 10:04:11 +02:00
|
|
|
];
|
|
|
|
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
|
|
|
|
this.hasAddLabelImagePermission = results[0];
|
2020-02-13 08:39:29 +01:00
|
|
|
this.hasRetagImagePermission = results[1];
|
|
|
|
this.hasDeleteImagePermission = results[2];
|
|
|
|
this.hasScanImagePermission = results[3];
|
|
|
|
// only has label permission
|
|
|
|
if (this.hasAddLabelImagePermission) {
|
|
|
|
if (!this.withAdmiral) {
|
|
|
|
this.getAllLabels();
|
2019-10-22 10:04:11 +02:00
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
}, error => this.errorHandlerService.error(error));
|
2019-01-09 09:08:56 +01:00
|
|
|
}
|
2017-12-08 08:05:52 +01:00
|
|
|
// Trigger scan
|
2019-11-12 03:25:03 +01:00
|
|
|
scanNow(): void {
|
2020-03-24 03:56:18 +01:00
|
|
|
if (!this.selectedRow.length) {
|
|
|
|
return;
|
2017-07-20 03:28:00 +02:00
|
|
|
}
|
2020-03-24 03:56:18 +01:00
|
|
|
this.scanFiinishArtifactLength = 0;
|
|
|
|
this.onScanArtifactsLength = this.selectedRow.length;
|
|
|
|
this.onSendingScanCommand = true;
|
|
|
|
this.selectedRow.forEach((data: any) => {
|
|
|
|
let digest = data.digest;
|
|
|
|
this.channel.publishScanEvent(this.repoName + "/" + digest);
|
|
|
|
});
|
2017-07-20 03:28:00 +02:00
|
|
|
}
|
2020-03-05 08:33:28 +01:00
|
|
|
selectedRowHasVul(): boolean {
|
2020-02-28 12:23:09 +01:00
|
|
|
return !!(this.selectedRow
|
|
|
|
&& this.selectedRow[0]
|
|
|
|
&& this.selectedRow[0].addition_links
|
|
|
|
&& this.selectedRow[0].addition_links[ADDITIONS.VULNERABILITIES]);
|
|
|
|
}
|
2020-03-05 08:33:28 +01:00
|
|
|
hasVul(artifact: Artifact): boolean {
|
|
|
|
return !!(artifact && artifact.addition_links && artifact.addition_links[ADDITIONS.VULNERABILITIES]);
|
|
|
|
}
|
2019-11-12 03:25:03 +01:00
|
|
|
submitFinish(e: boolean) {
|
2020-03-24 03:56:18 +01:00
|
|
|
this.scanFiinishArtifactLength += 1;
|
|
|
|
// all selected scan action has start
|
|
|
|
if (this.scanFiinishArtifactLength === this.onScanArtifactsLength) {
|
|
|
|
this.onSendingScanCommand = e;
|
|
|
|
}
|
2019-11-12 03:25:03 +01:00
|
|
|
}
|
2017-12-08 08:05:52 +01:00
|
|
|
// pull command
|
2017-09-28 06:59:07 +02:00
|
|
|
onCpError($event: any): void {
|
2019-01-09 09:08:56 +01:00
|
|
|
this.copyInput.setPullCommendShow();
|
2018-10-29 10:01:04 +01:00
|
|
|
}
|
2019-10-10 11:42:45 +02:00
|
|
|
getProjectScanner(): void {
|
|
|
|
this.hasEnabledScanner = false;
|
|
|
|
this.scanBtnState = ClrLoadingState.LOADING;
|
2019-10-21 08:17:39 +02:00
|
|
|
this.scanningService.getProjectScanner(this.projectId)
|
2020-02-13 08:39:29 +01:00
|
|
|
.subscribe(response => {
|
|
|
|
if (response && "{}" !== JSON.stringify(response) && !response.disabled
|
2019-10-28 04:33:26 +01:00
|
|
|
&& response.health === "healthy") {
|
2020-02-13 08:39:29 +01:00
|
|
|
this.scanBtnState = ClrLoadingState.SUCCESS;
|
|
|
|
this.hasEnabledScanner = true;
|
|
|
|
} else {
|
2019-10-10 11:42:45 +02:00
|
|
|
this.scanBtnState = ClrLoadingState.ERROR;
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
|
|
|
}, error => {
|
|
|
|
this.scanBtnState = ClrLoadingState.ERROR;
|
|
|
|
});
|
2019-10-10 11:42:45 +02:00
|
|
|
}
|
2019-10-28 04:33:26 +01:00
|
|
|
|
2020-04-07 11:13:40 +02:00
|
|
|
handleScanOverview(scanOverview: any): any {
|
2019-10-10 11:42:45 +02:00
|
|
|
if (scanOverview) {
|
2021-01-07 10:16:52 +01:00
|
|
|
return Object.values(scanOverview)[0];
|
2019-10-10 11:42:45 +02:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
goIntoIndexArtifact(artifact: Artifact) {
|
|
|
|
let depth: string = '';
|
|
|
|
if (this.depth) {
|
|
|
|
depth = this.depth + '-' + artifact.digest;
|
|
|
|
} else {
|
|
|
|
depth = artifact.digest;
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2020-02-20 09:12:46 +01:00
|
|
|
const linkUrl = ['harbor', 'projects', this.projectId, 'repositories', this.repoName, 'depth', depth];
|
2020-10-12 11:23:22 +02:00
|
|
|
if (this.activatedRoute.snapshot.queryParams['publicAndNotLogged'] === YES) {
|
|
|
|
this.router.navigate(linkUrl, {queryParams: {publicAndNotLogged: YES}});
|
|
|
|
} else {
|
|
|
|
this.router.navigate(linkUrl);
|
|
|
|
}
|
2020-02-13 08:39:29 +01:00
|
|
|
}
|
2020-02-25 09:07:21 +01:00
|
|
|
selectFilterType() {
|
|
|
|
this.lastFilteredTagName = '';
|
2020-03-10 05:07:56 +01:00
|
|
|
if (this.filterByType === 'labels') {
|
2020-02-25 09:07:21 +01:00
|
|
|
this.openLabelFilterPanel = true;
|
2020-10-30 07:43:59 +01:00
|
|
|
// every time when filer panel opens, resort imageFilterLabels labels
|
|
|
|
this.reSortImageFilterLabels();
|
2020-02-25 09:07:21 +01:00
|
|
|
this.openLabelFilterPiece = true;
|
|
|
|
} else {
|
|
|
|
this.openLabelFilterPiece = false;
|
|
|
|
this.filterOneLabel = this.initFilter;
|
|
|
|
this.showlabel = false;
|
|
|
|
this.imageFilterLabels.forEach(data => {
|
|
|
|
data.iconsShow = false;
|
|
|
|
});
|
|
|
|
}
|
2020-03-10 05:07:56 +01:00
|
|
|
this.currentPage = 1;
|
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
}
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
|
|
|
this.filters = [];
|
|
|
|
this.clrLoad(st);
|
2020-02-25 09:07:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
selectFilter(showItem: string, filterItem: string) {
|
|
|
|
this.lastFilteredTagName = filterItem;
|
|
|
|
this.currentPage = 1;
|
2020-02-20 09:12:46 +01:00
|
|
|
|
2020-03-10 05:07:56 +01:00
|
|
|
let st: ClrDatagridStateInterface = this.currentState;
|
2020-02-25 09:07:21 +01:00
|
|
|
if (!st) {
|
|
|
|
st = { page: {} };
|
|
|
|
}
|
|
|
|
st.page.size = this.pageSize;
|
|
|
|
st.page.from = 0;
|
|
|
|
st.page.to = this.pageSize - 1;
|
2020-03-10 05:07:56 +01:00
|
|
|
this.filters = [];
|
|
|
|
if (filterItem) {
|
|
|
|
this.filters.push(`${this.filterByType}=${filterItem}`);
|
|
|
|
}
|
2020-02-25 09:07:21 +01:00
|
|
|
|
|
|
|
this.clrLoad(st);
|
|
|
|
}
|
|
|
|
get isFilterReadonly() {
|
2020-03-10 05:07:56 +01:00
|
|
|
return this.filterByType === 'labels' ? 'readonly' : null;
|
2020-02-25 09:07:21 +01:00
|
|
|
}
|
2020-06-01 08:51:10 +02:00
|
|
|
// when finished, remove it from selectedRow
|
|
|
|
scanFinished(artifact: Artifact) {
|
|
|
|
if (this.selectedRow && this.selectedRow.length) {
|
|
|
|
for ( let i = 0; i < this.selectedRow.length; i++) {
|
|
|
|
if (artifact.digest === this.selectedRow[i].digest) {
|
|
|
|
this.selectedRow.splice(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-11 04:22:02 +02:00
|
|
|
getIconsFromBackEnd() {
|
|
|
|
if (this.artifactList && this.artifactList.length) {
|
|
|
|
this.artifactService.getIconsFromBackEnd(this.artifactList);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
showDefaultIcon(event: any) {
|
|
|
|
if (event && event.target) {
|
|
|
|
event.target.src = artifactDefault;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
getIcon(icon: string): SafeUrl {
|
|
|
|
return this.artifactService.getIcon(icon);
|
|
|
|
}
|
2020-11-26 07:39:33 +01:00
|
|
|
// get Tags and display less than 9 tags(too many tags will make UI stuck)
|
|
|
|
getArtifactTagsAsync(artifacts: ArtifactFront[]) {
|
|
|
|
if (artifacts && artifacts.length) {
|
|
|
|
artifacts.forEach(item => {
|
|
|
|
const listTagParams: NewArtifactService.ListTagsParams = {
|
|
|
|
projectName: this.projectName,
|
|
|
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
|
|
|
reference: item.digest,
|
|
|
|
withSignature: true,
|
|
|
|
withImmutableStatus: true,
|
|
|
|
page: 1,
|
|
|
|
pageSize: 8
|
|
|
|
};
|
|
|
|
this.newArtifactService.listTagsResponse(listTagParams).subscribe(
|
|
|
|
res => {
|
|
|
|
if (res.headers) {
|
|
|
|
let xHeader: string = res.headers.get("x-total-count");
|
|
|
|
if (xHeader) {
|
|
|
|
item.tagNumber = Number.parseInt(xHeader);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
item.tags = res.body;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2017-07-04 12:03:38 +02:00
|
|
|
}
|