Merge pull request #11362 from jwangyangls/refact-artifact-tag

[OCI] Refact artifact tag
This commit is contained in:
jwangyangls 2020-03-31 15:46:45 +08:00 committed by GitHub
commit ceded08507
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 187 additions and 52 deletions

View File

@ -95,7 +95,7 @@
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}
</button>
<clr-dropdown class="btn btn-link">
<clr-dropdown class="btn btn-link" *ngIf="!depth">
<span clrDropdownTrigger id="artifact-list-action" class="btn pl-0">
{{'BUTTON.ACTIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>

View File

@ -248,9 +248,9 @@ describe("ArtifactListTabComponent (inline template)", () => {
error: () => { }
};
const permissions = [
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.ARTIFACT.KEY, action: USERSTATICPERMISSION.ARTIFACT.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE },
];
const mockRouter = {

View File

@ -848,9 +848,9 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
}
getImagePermissionRule(projectId: number): void {
const permissions = [
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.ARTIFACT.KEY, action: USERSTATICPERMISSION.ARTIFACT.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE },
];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {

View File

@ -21,7 +21,7 @@
<ng-container *ngIf="!loading">
<!-- tags -->
<artifact-tag [artifactDetails]="artifact" [projectName]="projectName" [projectId]="projectId" [repositoryName]="repositoryName"
(refreshArtifact)="refreshArtifact()"></artifact-tag>
></artifact-tag>
<!-- Overview -->
<artifact-common-properties [artifactDetails]="artifact"></artifact-common-properties>

View File

@ -91,9 +91,7 @@ export class ArtifactSummaryComponent implements OnInit {
reference: this.artifactDigest,
projectName: this.projectName,
withLabel: true,
withScanOverview: true,
withSignature: true,
withImmutableStatus: true
withScanOverview: true
}).pipe(finalize(() => this.loading = false))
.subscribe(response => {
this.artifact = response;
@ -105,8 +103,4 @@ export class ArtifactSummaryComponent implements OnInit {
onBack(): void {
this.backEvt.emit(this.repositoryName);
}
refreshArtifact() {
this.getArtifactDetails();
}
}

View File

@ -1,12 +1,16 @@
<h4>{{'REPOSITORY.TAGS_COUNT' | translate}}</h4>
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" [clrDgRowSelection]="true">
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getCurrentArtifactTags($event)" [(clrDgSelected)]="selectedRow" [clrDgRowSelection]="true">
<clr-dg-action-bar>
<button type="button" class="btn btn-secondary" (click)="addTag()">
<button type="button" [disabled]="!hasCreateTagPermission" class="btn btn-secondary" (click)="addTag()">
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'TAG.ADD_TAG' | translate}}
</button>
<button type="button" class="btn btn-secondary" [disabled]="!(selectedRow.length>=1&& !hasImmutableOnTag() && hasDeleteTagPermission)" (click)="removeTag()">
<clr-icon shape="trash" size="16"></clr-icon>&nbsp;{{'TAG.REMOVE_TAG' | translate}}
</button>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
<form #tagForm="ngForm" [hidden]="!newTagformShow" class="label-form stack-block-label">
<section>
<label>
@ -39,7 +43,7 @@
<clr-dg-column>{{'TAG.PULL_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG.PUSH_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let tag of artifactDetails?.tags" [clrDgItem]="tag">
<clr-dg-row *ngFor="let tag of currentTags" [clrDgItem]="tag">
<clr-dg-cell>
<div class="cell white-normal" [class.immutable]="tag.immutable">
<span href="javascript:void(0)" class="max-width-100" title="{{tag.name}}">{{tag.name}}</span>
@ -61,9 +65,9 @@
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{'TAG.OF' | translate}} {{pagination.totalItems}} {{'TAG.ITEMS' | translate}}</span>
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
<span *ngIf="totalCount">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{'TAG.OF' | translate}} {{totalCount}} {{'TAG.ITEMS' | translate}}</span>
<clr-dg-pagination #pagination [clrDgTotalItems]="totalCount" [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -25,3 +25,9 @@
.position-ab {
position: absolute;
}
.refresh-btn {
float: right;
margin-top: 15px;
margin-right: 20px;
cursor: pointer;
}

View File

@ -11,6 +11,8 @@ import { ArtifactService } from '../../../../../../ng-swagger-gen/services/artif
import { OperationService } from "../../../../../lib/components/operation/operation.service";
import { CURRENT_BASE_HREF } from "../../../../../lib/utils/utils";
import { USERSTATICPERMISSION, UserPermissionService, UserPermissionDefaultService } from '../../../../../lib/services';
import { TagService } from '../../../../../../ng-swagger-gen/services/tag.service';
import { delay } from 'rxjs/operators';
describe('ArtifactTagComponent', () => {
@ -19,6 +21,10 @@ describe('ArtifactTagComponent', () => {
const mockErrorHandler = {
error: () => {}
};
const mockTagService = {
listTagsResponse: () => of({headers: null, body: []}).pipe(delay(0)),
listTags: () => of([]),
};
const mockArtifactService = {
createTag: () => of([]),
deleteTag: () => of(null),
@ -29,6 +35,7 @@ describe('ArtifactTagComponent', () => {
let userPermissionService;
const permissions = [
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.CREATE },
];
let mockHasDeleteImagePermission: boolean = true;
beforeEach(async(() => {
@ -47,6 +54,7 @@ describe('ArtifactTagComponent', () => {
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: mockErrorHandler, useValue: ErrorHandler },
{ provide: ArtifactService, useValue: mockArtifactService },
{ provide: TagService, useValue: mockTagService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: OperationService },
]
@ -62,6 +70,7 @@ describe('ArtifactTagComponent', () => {
.withArgs(component.projectId, permissions)
.and.returnValue(of([
mockHasDeleteImagePermission]));
component.artifactDetails = {id: 1};
fixture.detectChanges();
});

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core';
import { Observable, of, forkJoin } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { map, catchError, finalize } 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";
@ -16,11 +16,14 @@ import { operateChanges, OperateInfo, OperationState } from "../../../../../lib/
import { errorHandler } from "../../../../../lib/utils/shared/shared.utils";
import { ArtifactFront as Artifact } from "../artifact";
import { ArtifactService } from '../../../../../../ng-swagger-gen/services/artifact.service';
import { TagService } from '../../../../../../ng-swagger-gen/services/tag.service';
import { Tag } from '../../../../../../ng-swagger-gen/models/tag';
import {
UserPermissionService, USERSTATICPERMISSION
} from "../../../../../lib/services";
import { ClrDatagridStateInterface } from '@clr/angular';
import { DEFAULT_PAGE_SIZE, calculatePage } from '../../../../../lib/utils/utils';
class InitTag {
name = "";
}
@ -35,22 +38,29 @@ export class ArtifactTagComponent implements OnInit {
@Input() projectName: string;
@Input() projectId: number;
@Input() repositoryName: string;
@Output() refreshArtifact = new EventEmitter();
newTagName = new InitTag();
newTagForm: NgForm;
@ViewChild("newTagForm", { static: true }) currentForm: NgForm;
selectedRow: Tag[] = [];
isTagNameExist = false;
newTagformShow = false;
loading = false;
loading = true;
openTag = false;
availableTime = AVAILABLE_TIME;
@ViewChild("confirmationDialog", { static: false })
confirmationDialog: ConfirmationDialogComponent;
hasDeleteTagPermission: boolean;
hasCreateTagPermission: boolean;
totalCount: number = 0;
allTags: Tag[] = [];
currentTags: Tag[] = [];
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage = 1;
constructor(
private operationService: OperationService,
private artifactService: ArtifactService,
private tagService: TagService,
private translateService: TranslateService,
private userPermissionService: UserPermissionService,
private errorHandlerService: ErrorHandler
@ -58,13 +68,57 @@ export class ArtifactTagComponent implements OnInit {
) { }
ngOnInit() {
this.getImagePermissionRule(this.projectId);
this.getAllTags();
}
getCurrentArtifactTags(state: ClrDatagridStateInterface) {
if (!state || !state.page) {
return ;
}
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
let params: TagService.ListTagsParams = {
projectName: this.projectName,
repositoryName: this.repositoryName,
page: pageNumber,
withSignature: true,
withImmutableStatus: true,
pageSize: this.pageSize,
q: encodeURIComponent(`artifact_id=${this.artifactDetails.id}`)
};
this.tagService.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;
}, error => {
this.errorHandlerService.error(error);
});
}
getAllTags() {
let params: TagService.ListTagsParams = {
projectName: this.projectName,
repositoryName: this.repositoryName
};
this.tagService.listTags(params).subscribe(res => {
this.allTags = res;
}, 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.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));
}
@ -84,11 +138,16 @@ export class ArtifactTagComponent implements OnInit {
reference: this.artifactDetails.digest,
tag: this.newTagName
};
this.loading = true;
this.artifactService.createTag(createTagParams).subscribe(res => {
this.newTagformShow = false;
this.newTagName = new InitTag();
this.refreshArtifact.emit();
this.getAllTags();
this.currentPage = 1;
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.getCurrentArtifactTags(st);
}, error => {
this.loading = false;
this.errorHandlerService.error(error);
});
}
@ -124,12 +183,36 @@ export class ArtifactTagComponent implements OnInit {
tagList.forEach(tag => {
observableLists.push(this.delOperate(tag));
});
forkJoin(...observableLists).subscribe((items) => {
this.loading = true;
forkJoin(...observableLists).subscribe((deleteResult) => {
// if delete one success refresh list
if (items.some(item => !item)) {
this.selectedRow = [];
this.refreshArtifact.emit();
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.currentPage = 1;
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.getCurrentArtifactTags(st);
} else if (deleteErrorList.length === deleteResult.length) {
// all is error
this.loading = false;
this.errorHandlerService.error(deleteResult[deleteResult.length - 1].error);
} else {
// some artifact delete success but it has error delete things
this.errorHandlerService.error(deleteErrorList[deleteErrorList.length - 1].error);
// 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);
}
});
}
@ -167,8 +250,8 @@ export class ArtifactTagComponent implements OnInit {
existValid(name) {
this.isTagNameExist = false;
if (this.artifactDetails.tags) {
this.artifactDetails.tags.forEach(tag => {
if (this.allTags) {
this.allTags.forEach(tag => {
if (tag.name === name) {
this.isTagNameExist = true;
}
@ -183,5 +266,10 @@ export class ArtifactTagComponent implements OnInit {
hasImmutableOnTag(): boolean {
return this.selectedRow.some((artifact) => artifact.immutable);
}
refresh() {
this.loading = true;
this.currentPage = 1;
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.getCurrentArtifactTags(st);
}
}

View File

@ -116,6 +116,12 @@ body {
button:focus {
outline: none;
}
clr-dropdown {
div:focus {
outline: none;
}
}
.w-100 {
width: 100% !important;

View File

@ -13,6 +13,7 @@ export const USERSTATICPERMISSION = {
"CREATE": "create",
"UPDATE": "update",
"DELETE": "delete",
"READ": "read",
"LIST": "list"
}
},
@ -22,6 +23,7 @@ export const USERSTATICPERMISSION = {
"LIST": "list"
}
},
// to do remove
"REPLICATION": {
'KEY': 'replication',
'VALUE': {
@ -31,6 +33,7 @@ export const USERSTATICPERMISSION = {
"LIST": "list",
}
},
// to do remove
"REPLICATION_JOB": {
'KEY': 'replication-job',
'VALUE': {
@ -43,6 +46,7 @@ export const USERSTATICPERMISSION = {
"CREATE": "create",
"UPDATE": "update",
"DELETE": "delete",
"READ": "read",
"LIST": "list",
}
},
@ -67,43 +71,47 @@ export const USERSTATICPERMISSION = {
"DELETE": "delete",
"LIST": "list",
"PUSH": "push",
"READ": "read",
"PULL": "pull",
}
},
"ARTIFACT": {
'KEY': 'artifact',
'VALUE': {
"CREATE": "create",
"DELETE": "delete",
"LIST": "list",
"READ": "read",
}
},
"ARTIFACT_ADDITION": {
'KEY': 'artifact-addition',
'VALUE': {
"READ": "read",
}
},
"REPOSITORY_TAG": {
'KEY': 'repository-tag',
'KEY': 'tag',
'VALUE': {
"DELETE": "delete",
"LIST": "list",
"CREATE": "create"
}
},
"REPOSITORY_TAG_SCAN_JOB": {
'KEY': 'repository-tag-scan-job',
'KEY': 'scan',
'VALUE': {
"CREATE": "create",
"READ": "read",
"LIST": "list",
}
},
"REPOSITORY_TAG_VULNERABILITY": {
'KEY': 'repository-tag-vulnerability',
'VALUE': {
"LIST": "list",
}
},
"REPOSITORY_TAG_LABEL": {
'KEY': 'repository-tag-label',
"REPOSITORY_ARTIFACT_LABEL": {
'KEY': 'repository-artifact-label',
'VALUE': {
"CREATE": "create",
"DELETE": "delete",
}
},
"REPOSITORY_TAG_MANIFEST": {
'KEY': 'repository-tag-manifest',
'VALUE': {
"READ": "read",
}
},
"HELM_CHART": {
'KEY': 'helm-chart',
'VALUE': {
@ -118,6 +126,7 @@ export const USERSTATICPERMISSION = {
'VALUE': {
"DELETE": "delete",
"LIST": "list",
"CREATE": "create",
"READ": "read",
}
},
@ -146,8 +155,16 @@ export const USERSTATICPERMISSION = {
"DELETE": "delete",
"LIST": "list",
"READ": "read",
"PULL": "pull",
"PUSH": "push"
"OPERATE": "operate"
}
},
"IMMUTABLE_TAG": {
'KEY': "immutable-tag",
'VALUE': {
"CREATE": "create",
"UPDATE": "update",
"DELETE": "delete",
"LIST": "list",
}
},
"WEBHOOK": {
@ -155,6 +172,8 @@ export const USERSTATICPERMISSION = {
"VALUE": {
"LIST": "list",
"READ": "read",
"CREATE": "create",
"UPDATE": "update",
}
},
"SCANNER": {
@ -163,6 +182,15 @@ export const USERSTATICPERMISSION = {
"READ": "read",
"CREATE": "create"
}
},
"METADATA": {
"KEY": "metadata",
"VALUE": {
"READ": "read",
"CREATE": "create",
"UPDATE": "update",
"DELETE": "delete",
}
}
};