harbor/src/portal/src/app/base/project/repository/artifact/artifact-tag/artifact-tag.component.ts

354 lines
13 KiB
TypeScript
Raw Normal View History

import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, finalize, map, switchMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { NgForm } from '@angular/forms';
import { AVAILABLE_TIME } from "../artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component";
import { OperationService } from "../../../../../shared/components/operation/operation.service";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../../../shared/entities/shared.const";
import { operateChanges, OperateInfo, OperationState } from "../../../../../shared/components/operation/operate";
import { ArtifactFront as Artifact, artifactImages, artifactPullCommands } from '../artifact';
import { ArtifactService } from '../../../../../../../ng-swagger-gen/services/artifact.service';
import { Tag } from '../../../../../../../ng-swagger-gen/models/tag';
import { SystemInfo, SystemInfoService, UserPermissionService, USERSTATICPERMISSION } from "../../../../../shared/services";
import { ClrDatagridStateInterface } from '@clr/angular';
import {
calculatePage,
dbEncodeURIComponent,
DEFAULT_PAGE_SIZE,
doFiltering,
doSorting,
getSortingString
} from '../../../../../shared/units/utils';
import { AppConfigService } from "../../../../../services/app-config.service";
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";
class InitTag {
name = "";
}
const DeleteTagWithNotoryCommand1 = 'notary -s https://';
const DeleteTagWithNotoryCommand2 = ':4443 -d ~/.docker/trust remove -p ';
@Component({
selector: 'artifact-tag',
templateUrl: './artifact-tag.component.html',
styleUrls: ['./artifact-tag.component.scss']
})
export class ArtifactTagComponent implements OnInit, OnDestroy {
@Input() artifactDetails: Artifact;
@Input() projectName: string;
@Input() isProxyCacheProject: boolean = false;
@Input() projectId: number;
@Input() repositoryName: string;
newTagName = new InitTag();
newTagForm: NgForm;
@ViewChild("newTagForm", { static: true }) currentForm: NgForm;
selectedRow: Tag[] = [];
isTagNameExist = false;
newTagformShow = false;
loading = true;
openTag = false;
availableTime = AVAILABLE_TIME;
@ViewChild("confirmationDialog")
confirmationDialog: ConfirmationDialogComponent;
hasDeleteTagPermission: boolean;
hasCreateTagPermission: boolean;
totalCount: number = 0;
allTags: Tag[] = [];
currentTags: Tag[] = [];
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage = 1;
tagNameChecker: Subject<string> = new Subject<string>();
tagNameCheckSub: Subscription;
tagNameCheckOnGoing = false;
systemInfo: SystemInfo;
constructor(
private operationService: OperationService,
private artifactService: ArtifactService,
private translateService: TranslateService,
private userPermissionService: UserPermissionService,
private systemInfoService: SystemInfoService,
private appConfigService: AppConfigService,
private errorHandlerService: ErrorHandler
) { }
ngOnInit() {
this.getImagePermissionRule(this.projectId);
this.invalidCreateTag();
this.systemInfoService.getSystemInfo()
.subscribe(systemInfo => this.systemInfo = systemInfo, error => this.errorHandlerService.error(error));
}
checkTagName(name) {
const listTagParams: ArtifactService.ListTagsParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repositoryName),
reference: this.artifactDetails.digest,
withSignature: true,
withImmutableStatus: true,
q: encodeURIComponent(`name=${name}`)
};
return this.artifactService.listTags(listTagParams)
.pipe(finalize(() => this.tagNameCheckOnGoing = false));
}
invalidCreateTag() {
if (!this.tagNameCheckSub) {
this.tagNameCheckSub = this.tagNameChecker
.pipe(debounceTime(500))
.pipe(distinctUntilChanged())
.pipe(switchMap(name => {
this.tagNameCheckOnGoing = true;
this.isTagNameExist = false;
return this.checkTagName(name);
}))
.subscribe(response => {
// tag existing
if (response && response.length) {
this.isTagNameExist = true;
}
}, error => {
this.errorHandlerService.error(error);
});
}
}
getCurrentArtifactTags(state: ClrDatagridStateInterface) {
if (!state || !state.page) {
return ;
}
this.pageSize = state.page.size;
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
let params: ArtifactService.ListTagsParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repositoryName),
reference: this.artifactDetails.digest,
page: pageNumber,
withSignature: true,
withImmutableStatus: true,
pageSize: this.pageSize,
sort: getSortingString(state)
};
this.loading = true;
this.artifactService.listTagsResponse(params).pipe(finalize(() => {
this.loading = false;
})).subscribe(res => {
if (res.headers) {
let xHeader: string = res.headers.get("x-total-count");
if (xHeader) {
this.totalCount = Number.parseInt(xHeader);
}
}
this.currentTags = res.body;
// Do customising filtering and sorting
this.currentTags = doFiltering<Tag>(this.currentTags, state);
this.currentTags = doSorting<Tag>(this.currentTags, state);
}, error => {
this.errorHandlerService.error(error);
});
}
getImagePermissionRule(projectId: number): void {
const permissions = [
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.CREATE },
];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
this.hasDeleteTagPermission = results[0];
this.hasCreateTagPermission = results[1];
}, error => this.errorHandlerService.error(error));
}
addTag() {
this.newTagformShow = true;
}
cancelAddTag() {
this.newTagformShow = false;
this.newTagName = new InitTag();
}
saveAddTag() {
// const tag: NewTag = {name: this.newTagName};
const createTagParams: ArtifactService.CreateTagParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repositoryName),
reference: this.artifactDetails.digest,
tag: this.newTagName
};
this.loading = true;
this.artifactService.createTag(createTagParams).subscribe(res => {
this.newTagformShow = false;
this.newTagName = new InitTag();
this.refresh();
}, error => {
this.loading = false;
this.errorHandlerService.error(error);
});
}
removeTag() {
if (this.selectedRow && this.selectedRow.length) {
let tagNames: string[] = [];
this.selectedRow.forEach(artifact => {
tagNames.push(artifact.name);
});
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
titleKey = "REPOSITORY.DELETION_TITLE_TAG";
summaryKey = "REPOSITORY.DELETION_SUMMARY_TAG";
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tagNames.join(" , ");
let message = new ConfirmationMessage(
titleKey,
summaryKey,
content,
this.selectedRow,
ConfirmationTargets.TAG,
buttons);
this.confirmationDialog.open(message);
}
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tagList: Tag[] = message.data;
if (tagList && tagList.length) {
let observableLists: any[] = [];
tagList.forEach(tag => {
observableLists.push(this.delOperate(tag));
});
this.loading = true;
forkJoin(...observableLists).subscribe((deleteResult) => {
// if delete one success refresh list
let deleteSuccessList = [];
let deleteErrorList = [];
deleteResult.forEach(result => {
if (!result) {
// delete success
deleteSuccessList.push(result);
} else {
deleteErrorList.push(result);
}
});
this.selectedRow = [];
if (deleteSuccessList.length === deleteResult.length) {
// all is success
this.refresh();
} else if (deleteErrorList.length === deleteResult.length) {
// all is error
this.loading = false;
this.errorHandlerService.error(deleteResult[deleteResult.length - 1]);
} else {
// some artifact delete success but it has error delete things
this.errorHandlerService.error(deleteErrorList[deleteErrorList.length - 1]);
// if delete one success refresh list
this.currentPage = 1;
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.getCurrentArtifactTags(st);
}
});
}
}
}
deletePort(url): string {
if (url && url.indexOf(':') !== -1) {
return url.split(':')[0];
}
return url;
}
delOperate(tag: Tag): Observable<any> | null {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_TAG';
operMessage.state = OperationState.progressing;
operMessage.data.name = tag.name;
this.operationService.publishInfo(operMessage);
if (tag.signed) {
forkJoin(this.translateService.get("BATCH.DELETED_FAILURE"),
this.translateService.get("REPOSITORY.DELETION_SUMMARY_TAG_DENIED")).subscribe(res => {
const wrongInfo: string = res[1] + DeleteTagWithNotoryCommand1 + this.deletePort(this.registryUrl) +
DeleteTagWithNotoryCommand2 +
this.registryUrl + "/" + this.repositoryName +
" " + tag.name;
operateChanges(operMessage, OperationState.failure, wrongInfo);
});
return of(null);
} else {
const deleteTagParams: ArtifactService.DeleteTagParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repositoryName),
reference: this.artifactDetails.digest,
tagName: tag.name
};
return this.artifactService.deleteTag(deleteTagParams)
.pipe(map(
response => {
this.translateService.get("BATCH.DELETED_SUCCESS")
.subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
}), catchError(error => {
const message = errorHandler(error);
this.translateService.get(message).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return of(error);
}));
}
}
existValid(name) {
if (name) {
this.tagNameChecker.next(name);
} else {
this.isTagNameExist = false;
}
}
toggleTagListOpenOrClose() {
this.openTag = !this.openTag;
this.newTagformShow = false;
}
hasImmutableOnTag(): boolean {
return this.selectedRow.some((artifact) => artifact.immutable);
}
refresh() {
this.loading = true;
this.currentPage = 1;
this.selectedRow = [];
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.getCurrentArtifactTags(st);
}
ngOnDestroy(): void {
this.tagNameCheckSub.unsubscribe();
}
get withNotary(): boolean {
return this.appConfigService.getConfig().with_notary;
}
public get registryUrl(): string {
if (this.systemInfo && this.systemInfo.registry_url) {
return this.systemInfo.registry_url;
}
return location.hostname;
}
hasPullCommand(): boolean {
return this.artifactDetails
&& (this.artifactDetails.type === artifactImages[0]
|| this.artifactDetails.type === artifactImages[1]
|| this.artifactDetails.type === artifactImages[2]);
}
getPullCommand(tag: Tag): string {
let pullCommand: string = '';
if (tag && tag.name && this.artifactDetails ) {
artifactPullCommands.forEach(artifactPullCommand => {
if (artifactPullCommand.type === this.artifactDetails.type) {
pullCommand = `${artifactPullCommand.pullCommand} ${this.registryUrl}/${this.projectName}/${this.repositoryName}:${tag.name}`;
}
});
}
return pullCommand;
}
}