diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.html b/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.html index 7ce863472..9d6ae9196 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.html +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.html @@ -1,53 +1,22 @@ - - - - - -
- -
-
- - {{ i18nMap[item.action] | translate }} - {{ i18nMap[item.resource] | translate }} -
-
-
+ - -
- -
-
- - {{ i18nMap[item.action] | translate }} - {{ i18nMap[item.resource] | translate }} -
-
- - + + + @@ -142,7 +64,7 @@ #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize"> - {{ + {{ 'PAGINATION.PAGE_SIZE' | translate }} { beforeEach(() => { fixture = TestBed.createComponent(ListAllProjectsComponent); component = fixture.componentInstance; - component.defaultAccesses = clone(INITIAL_ACCESSES); - component.cachedAllProjects = [project1, project2, project3]; - component.init(false); fixture.detectChanges(); }); @@ -40,6 +35,7 @@ describe('ListAllProjectsComponent', () => { expect(component).toBeTruthy(); }); it('should render list', async () => { + component.projects = [project1, project2, project3]; fixture.detectChanges(); await fixture.whenStable(); const rows = fixture.nativeElement.querySelectorAll('clr-dg-row'); diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.ts index c487823db..07b1f6f7d 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/list-all-projects/list-all-projects.component.ts @@ -1,86 +1,60 @@ -import { Component, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { Project } from '../../../../../../ng-swagger-gen/models/project'; import { clone, CustomComparator } from '../../../../shared/units/utils'; import { ClrDatagridComparatorInterface } from '@clr/angular'; -import { Router } from '@angular/router'; -import { - ACTION_RESOURCE_I18N_MAP, - FrontAccess, - FrontProjectForAdd, - INITIAL_ACCESSES, - PermissionsKinds, -} from '../system-robot-util'; +import { FrontAccess } from '../system-robot-util'; +import { Access } from '../../../../../../ng-swagger-gen/models/access'; +import { forkJoin, Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; +import { ProjectService } from '../../../../../../ng-swagger-gen/services/project.service'; +import { PermissionSelectPanelModes } from '../../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; import { RobotPermission } from '../../../../../../ng-swagger-gen/models/robot-permission'; +import { Permissions } from '../../../../../../ng-swagger-gen/models/permissions'; + +const FIRST_PROJECTS_PAGE_SIZE: number = 100; @Component({ selector: 'app-list-all-projects', templateUrl: './list-all-projects.component.html', styleUrls: ['./list-all-projects.component.scss'], }) -export class ListAllProjectsComponent { - cachedAllProjects: Project[]; - i18nMap = ACTION_RESOURCE_I18N_MAP; - permissionsForAdd: RobotPermission[] = []; - selectedRow: FrontProjectForAdd[] = []; +export class ListAllProjectsComponent implements OnInit { + selectedRow: Project[] = []; + selectedRowForEdit: Project[] = []; timeComparator: ClrDatagridComparatorInterface = new CustomComparator('creation_time', 'date'); - projects: FrontProjectForAdd[] = []; + projects: Project[] = []; pageSize: number = 5; currentPage: number = 1; - defaultAccesses: FrontAccess[] = []; - @Input() - coverAll: boolean = false; showSelectAll: boolean = true; myNameFilterValue: string; - constructor(private router: Router) {} - init(isEdit: boolean) { - this.pageSize = 5; - this.currentPage = 1; - this.showSelectAll = true; - this.myNameFilterValue = null; - if (isEdit) { - this.defaultAccesses = clone(INITIAL_ACCESSES); - this.defaultAccesses.forEach(item => (item.checked = false)); - } else { - this.defaultAccesses = clone(INITIAL_ACCESSES); - } - if (this.cachedAllProjects && this.cachedAllProjects.length) { - this.projects = clone(this.cachedAllProjects); - this.resetAccess(this.defaultAccesses); - } else { - this.projects = []; - } + @Input() + robotMetadata: Permissions; + + initialAccess: Access[] = []; + selectedProjectPermissionMap: { [key: string]: Access[] } = {}; + selectedProjectPermissionMapForEdit: { [key: string]: Access[] } = {}; + loadingData: boolean = true; + @Input() + initDataForEdit: RobotPermission[]; + + constructor( + private projectService: ProjectService, + private cdf: ChangeDetectorRef + ) {} + + ngOnInit() { + this.loadDataFromBackend(); } resetAccess(accesses: FrontAccess[]) { if (this.projects && this.projects.length) { this.projects.forEach(item => { - item.permissions = [ - { - kind: PermissionsKinds.PROJECT, - namespace: item.name, - access: clone(accesses), - }, - ]; + this.selectedProjectPermissionMap[item.name] = clone(accesses); }); } } - chooseAccess(access: FrontAccess) { - access.checked = !access.checked; - } - chooseDefaultAccess(access: FrontAccess) { - access.checked = !access.checked; - this.resetAccess(this.defaultAccesses); - } - getPermissions(access: FrontAccess[]): number { - let count: number = 0; - access.forEach(item => { - if (item.checked) { - count++; - } - }); - return count; - } + getLink(proId: number): string { return `/harbor/projects/${proId}`; } @@ -108,26 +82,87 @@ export class ListAllProjectsComponent { } this.showSelectAll = !this.showSelectAll; } - isSelectAll(permissions: FrontAccess[]): boolean { - if (permissions?.length) { - return ( - permissions.filter(item => item.checked).length < - permissions.length / 2 - ); - } - return false; + + loadDataFromBackend() { + this.loadingData = true; + this.projectService + .listProjectsResponse({ + withDetail: false, + page: 1, + pageSize: FIRST_PROJECTS_PAGE_SIZE, + }) + .subscribe({ + next: result => { + // Get total count + if (result.headers) { + const xHeader: string = + result.headers.get('X-Total-Count'); + const totalCount = parseInt(xHeader, 0); + let arr = result.body || []; + if (totalCount <= FIRST_PROJECTS_PAGE_SIZE) { + // already gotten all projects + this.projects = result.body; + this.initDataForEditMode(); + this.loadingData = false; + this.cdf.detectChanges(); + } else { + // get all the projects in specified times + const times: number = Math.ceil( + totalCount / FIRST_PROJECTS_PAGE_SIZE + ); + const observableList: Observable[] = []; + for (let i = 2; i <= times; i++) { + observableList.push( + this.projectService.listProjects({ + withDetail: false, + page: i, + pageSize: FIRST_PROJECTS_PAGE_SIZE, + }) + ); + } + forkJoin(observableList) + .pipe( + finalize(() => { + this.loadingData = false; + }) + ) + .subscribe(res => { + if (res && res.length) { + res.forEach(item => { + arr = arr.concat(item); + }); + this.projects = arr; + this.initDataForEditMode(); + this.cdf.detectChanges(); + } + }); + } + } + }, + error: error => { + this.loadingData = false; + }, + }); } - selectAllPermissionOrUnselectAll(permissions: FrontAccess[]) { - if (permissions?.length) { - if (this.isSelectAll(permissions)) { - permissions.forEach(item => { - item.checked = true; + initDataForEditMode() { + if (this.initDataForEdit?.length) { + this.selectedRow = []; + this.projects.forEach((pro, index) => { + this.initDataForEdit.forEach(item => { + if (pro.name === item.namespace) { + item.access.forEach(acc => { + this.selectedProjectPermissionMap[pro.name] = + item.access; + }); + this.selectedRow.push(pro); + } }); - } else { - permissions.forEach(item => { - item.checked = false; - }); - } + this.selectedProjectPermissionMapForEdit = clone( + this.selectedProjectPermissionMap + ); + this.selectedRowForEdit = clone(this.selectedRow); + }); } } + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.html b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.html index 6d3ea10a8..b9b65df29 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.html +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.html @@ -1,25 +1,42 @@ - - - - - -
+ + + + +
- - - - - -
-
- -
+ + diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.scss b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.scss index 5ef284190..f5fd8155f 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.scss +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.scss @@ -14,11 +14,6 @@ margin: 0; } -.permission{ - padding-top: 0.1rem; - color: #000; -} - .padding-left-120{ padding-left: 126px; } @@ -35,10 +30,6 @@ width: 265px; } -.mt-description { - margin-top: 2.5rem; -} - .width-table { width: 388px; } @@ -74,33 +65,16 @@ width: 194px; } -.check { - margin-right: 5px; - color: green; -} -.dropdown-per { - margin-top: -1px; -} - -.mt-8px { - margin-top: 8px !important; -} /* stylelint-disable */ .showWarning { color: #b3a000; } -.dropdown-item { - min-height: 20px; - display: flex; - align-items: center; -} - -.dropdown-menu { - overflow-y: auto; -} - .ml-20px { margin-left: 20px; } + +:host::ng-deep.modal-dialog { + width: 48rem; +} diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.spec.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.spec.ts index 3b1f43ad9..4f131dd27 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.spec.ts @@ -5,12 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ClarityModule } from '@clr/angular'; import { TranslateModule } from '@ngx-translate/core'; import { Robot } from '../../../../../../ng-swagger-gen/models/robot'; -import { - Action, - INITIAL_ACCESSES, - PermissionsKinds, - Resource, -} from '../system-robot-util'; +import { Action, PermissionsKinds, Resource } from '../system-robot-util'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { OperationService } from '../../../../shared/components/operation/operation.service'; import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.service'; @@ -19,7 +14,6 @@ import { delay } from 'rxjs/operators'; import { ConfigurationService } from '../../../../services/config.service'; import { Configuration } from '../../config/config'; import { FormsModule } from '@angular/forms'; -import { clone } from '../../../../shared/units/utils'; describe('NewRobotComponent', () => { let component: NewRobotComponent; @@ -103,7 +97,6 @@ describe('NewRobotComponent', () => { fixture.autoDetectChanges(); component.isEditMode = false; component.addRobotOpened = true; - component.defaultAccesses = clone(INITIAL_ACCESSES); await fixture.whenStable(); const nameInput = fixture.nativeElement.querySelector('#name'); nameInput.value = ''; @@ -117,28 +110,9 @@ describe('NewRobotComponent', () => { fixture.autoDetectChanges(); component.isEditMode = true; component.addRobotOpened = true; - component.defaultAccesses = clone(INITIAL_ACCESSES); component.systemRobot = robot1; await fixture.whenStable(); const nameInput = fixture.nativeElement.querySelector('#name'); expect(nameInput.value).toEqual('robot1'); }); - it('should be valid', async () => { - fixture.autoDetectChanges(); - component.isEditMode = false; - component.addRobotOpened = true; - component.defaultAccesses = clone(INITIAL_ACCESSES); - await fixture.whenStable(); - const nameInput = fixture.nativeElement.querySelector('#name'); - nameInput.value = 'test'; - nameInput.dispatchEvent(new Event('input')); - const expiration = fixture.nativeElement.querySelector( - '#robotTokenExpiration' - ); - expiration.value = 10; - expiration.dispatchEvent(new Event('input')); - component.coverAll = true; - await fixture.whenStable(); - expect(component.disabled()).toBeFalsy(); - }); }); diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts index 73562dc62..2de5190ac 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, + Input, OnDestroy, OnInit, Output, @@ -17,19 +18,22 @@ import { finalize, switchMap, } from 'rxjs/operators'; -import { Access } from '../../../../../../ng-swagger-gen/models/access'; import { - ACTION_RESOURCE_I18N_MAP, ExpirationType, - FrontAccess, - INITIAL_ACCESSES, + getSystemAccess, NAMESPACE_ALL_PROJECTS, + NAMESPACE_SYSTEM, + NEW_EMPTY_ROBOT, onlyHasPushPermission, PermissionsKinds, } from '../system-robot-util'; -import { clone } from '../../../../shared/units/utils'; +import { + clone, + isSameArrayValue, + isSameObject, +} from '../../../../shared/units/utils'; import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.service'; -import { ClrLoadingState } from '@clr/angular'; +import { ClrLoadingState, ClrWizard } from '@clr/angular'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { Subject, Subscription } from 'rxjs'; import { @@ -40,6 +44,9 @@ import { import { OperationService } from '../../../../shared/components/operation/operation.service'; import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component'; import { errorHandler } from '../../../../shared/units/shared.utils'; +import { RobotPermission } from '../../../../../../ng-swagger-gen/models/robot-permission'; +import { PermissionSelectPanelModes } from '../../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; +import { Permissions } from '../../../../../../ng-swagger-gen/models/permissions'; const MINI_SECONDS_ONE_DAY: number = 60 * 24 * 60 * 1000; @@ -49,13 +56,12 @@ const MINI_SECONDS_ONE_DAY: number = 60 * 24 * 60 * 1000; styleUrls: ['./new-robot.component.scss'], }) export class NewRobotComponent implements OnInit, OnDestroy { - i18nMap = ACTION_RESOURCE_I18N_MAP; isEditMode: boolean = false; originalRobotForEdit: Robot; @Output() addSuccess: EventEmitter = new EventEmitter(); addRobotOpened: boolean = false; - systemRobot: Robot = {}; + systemRobot: Robot = clone(NEW_EMPTY_ROBOT); expirationType: string = ExpirationType.DAYS; systemExpirationDays: number; coverAll: boolean = false; @@ -65,8 +71,6 @@ export class NewRobotComponent implements OnInit, OnDestroy { loading: boolean = false; checkNameOnGoing: boolean = false; loadingSystemConfig: boolean = false; - defaultAccesses: FrontAccess[] = []; - defaultAccessesForEdit: FrontAccess[] = []; @ViewChild(ListAllProjectsComponent) listAllProjectsComponent: ListAllProjectsComponent; @ViewChild(InlineAlertComponent) @@ -75,6 +79,27 @@ export class NewRobotComponent implements OnInit, OnDestroy { saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; private _nameSubject: Subject = new Subject(); private _nameSubscription: Subscription; + + @Input() + robotMetadata: Permissions; + + permissionForCoverAll: RobotPermission = { + access: [], + kind: PermissionsKinds.PROJECT, + namespace: NAMESPACE_ALL_PROJECTS, + }; + + permissionForCoverAllForEdit: RobotPermission; + + permissionForSystem: RobotPermission = { + access: [], + kind: PermissionsKinds.SYSTEM, + namespace: NAMESPACE_SYSTEM, + }; + + permissionForSystemForEdit: RobotPermission; + showPage3: boolean = false; + @ViewChild('wizard') wizard: ClrWizard; constructor( private configService: ConfigurationService, private robotService: RobotService, @@ -166,36 +191,42 @@ export class NewRobotComponent implements OnInit, OnDestroy { this._nameSubject.next(this.systemRobot.name); } cancel() { + this.wizard.reset(); + this.reset(); this.addRobotOpened = false; } - getPermissions(): number { - let count: number = 0; - this.defaultAccesses.forEach(item => { - if (item.checked) { - count++; - } - }); - return count; - } - chooseAccess(access: FrontAccess) { - access.checked = !access.checked; - } + reset() { this.open(false); - this.defaultAccesses = clone(INITIAL_ACCESSES); - this.listAllProjectsComponent.init(false); - this.listAllProjectsComponent.selectedRow = []; - this.systemRobot = {}; + this.systemRobot = clone(NEW_EMPTY_ROBOT); + this.permissionForCoverAll = { + access: [], + kind: PermissionsKinds.PROJECT, + namespace: NAMESPACE_ALL_PROJECTS, + }; + this.permissionForSystem = { + access: [], + kind: PermissionsKinds.SYSTEM, + namespace: NAMESPACE_SYSTEM, + }; + this.coverAll = false; + this.showPage3 = false; this.robotForm.reset(); this.expirationType = ExpirationType.DAYS; this.getSystemRobotExpiration(); } resetForEdit(robot: Robot) { this.open(true); - this.defaultAccesses = clone(INITIAL_ACCESSES); - this.defaultAccesses.forEach(item => (item.checked = false)); this.originalRobotForEdit = clone(robot); - this.systemRobot = robot; + this.systemRobot = clone(robot); + this.permissionForSystem = { + access: getSystemAccess(robot), + kind: PermissionsKinds.SYSTEM, + namespace: NAMESPACE_SYSTEM, + }; + + this.permissionForSystemForEdit = clone(this.permissionForSystem); + this.expirationType = robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS; if (robot && robot.permissions && robot.permissions.length) { @@ -206,67 +237,17 @@ export class NewRobotComponent implements OnInit, OnDestroy { item.namespace === NAMESPACE_ALL_PROJECTS ) { this.coverAll = true; - if (item && item.access) { - item.access.forEach(item2 => { - this.defaultAccesses.forEach(item3 => { - if ( - item3.resource === item2.resource && - item3.action === item2.action - ) { - item3.checked = true; - } - }); - }); - this.defaultAccessesForEdit = clone( - this.defaultAccesses - ); - } + this.permissionForCoverAll = clone(item); + this.permissionForCoverAllForEdit = clone(item); } }); } - if (!this.coverAll) { - this.defaultAccesses.forEach(item => (item.checked = true)); - } this.robotForm.reset({ name: this.systemRobot.name, expiration: this.systemRobot.duration, description: this.systemRobot.description, - coverAll: this.coverAll, }); this.coverAllForEdit = this.coverAll; - this.listAllProjectsComponent.init(true); - this.listAllProjectsComponent.selectedRow = []; - const map = {}; - this.listAllProjectsComponent.projects.forEach((pro, index) => { - if (this.systemRobot && this.systemRobot.permissions) { - this.systemRobot.permissions.forEach(item => { - if (pro.name === item.namespace) { - item.access.forEach(acc => { - pro.permissions[0].access.forEach(item3 => { - if ( - item3.resource === acc.resource && - item3.action === acc.action - ) { - item3.checked = true; - } - }); - }); - map[index] = true; - this.listAllProjectsComponent.selectedRow.push(pro); - } - }); - } - }); - this.listAllProjectsComponent.defaultAccesses.forEach( - item => (item.checked = true) - ); - this.listAllProjectsComponent.projects.forEach((pro, index) => { - if (!map[index]) { - pro.permissions[0].access.forEach(item => { - item.checked = true; - }); - } - }); } open(isEditMode: boolean) { this.isNameExisting = false; @@ -286,46 +267,32 @@ export class NewRobotComponent implements OnInit, OnDestroy { return false; } if (this.coverAll) { - let flag = false; - this.defaultAccesses.forEach(item => { - if (item.checked) { - flag = true; - } - }); - if (flag) { - return true; - } - } - if ( - !this.listAllProjectsComponent || - !this.listAllProjectsComponent.selectedRow || - !this.listAllProjectsComponent.selectedRow.length - ) { - return false; - } - for ( - let i = 0; - i < this.listAllProjectsComponent.selectedRow.length; - i++ - ) { - let flag = false; - for ( - let j = 0; - j < - this.listAllProjectsComponent.selectedRow[i].permissions[0] - .access.length; - j++ - ) { - if ( - this.listAllProjectsComponent.selectedRow[i].permissions[0] - .access[j].checked - ) { - flag = true; - } - } - if (!flag) { + if (!this.permissionForCoverAll.access?.length) { return false; } + } else { + if ( + !this.permissionForSystem?.access?.length && + !this.listAllProjectsComponent?.selectedRow?.length + ) { + return false; + } + if (this.listAllProjectsComponent?.selectedRow?.length) { + for ( + let i = 0; + i < this.listAllProjectsComponent?.selectedRow?.length; + i++ + ) { + if ( + !this.listAllProjectsComponent + ?.selectedProjectPermissionMap[ + this.listAllProjectsComponent?.selectedRow[i].name + ]?.length + ) { + return false; + } + } + } } return true; } @@ -344,79 +311,61 @@ export class NewRobotComponent implements OnInit, OnDestroy { ) { return true; } - if (this.coverAll !== this.coverAllForEdit) { - if (this.coverAll) { - let flag = false; - this.defaultAccesses.forEach(item => { - if (item.checked) { - flag = true; - } - }); - if (!flag) { - return false; - } - } - return true; - } - if (this.coverAll === this.coverAllForEdit) { - if (this.coverAll) { - let flag = true; - this.defaultAccessesForEdit.forEach(item => { - this.defaultAccesses.forEach(item2 => { - if ( - item.resource === item2.resource && - item.action === item2.action && - item.checked !== item2.checked - ) { - flag = false; - } - }); - }); - return !flag; - } - } if ( - this.systemRobot.permissions.length !== - this.listAllProjectsComponent.selectedRow.length + !isSameObject( + this.permissionForSystem, + this.permissionForSystemForEdit + ) ) { return true; } - const map = {}; - let accessFlag = true; - this.listAllProjectsComponent.selectedRow.forEach(item => { - this.systemRobot.permissions.forEach(item2 => { - if (item.name === item2.namespace) { - map[item.name] = true; - if ( - item2.access.length !== - this.getAccessNum(item.permissions[0].access) - ) { - accessFlag = false; - } - item2.access.forEach(arr => { - item.permissions[0].access.forEach(arr2 => { - if ( - arr.resource === arr2.resource && - arr.action === arr2.action && - !arr2.checked - ) { - accessFlag = false; - } - }); - }); - } - }); - }); - if (!accessFlag) { + if (this.coverAll !== this.coverAllForEdit) { return true; } - let flag1 = true; - this.systemRobot.permissions.forEach(item => { - if (!map[item.namespace]) { - flag1 = false; + if (this.coverAll) { + if ( + !isSameObject( + this.permissionForCoverAll, + this.permissionForCoverAllForEdit + ) + ) { + return true; } - }); - return !flag1; + } + if (this.listAllProjectsComponent) { + if ( + !isSameArrayValue( + this.listAllProjectsComponent.selectedRow, + this.listAllProjectsComponent.selectedRowForEdit + ) + ) { + return true; + } else { + for ( + let i = 0; + i < this.listAllProjectsComponent.selectedRow.length; + i++ + ) { + if ( + !isSameArrayValue( + this.listAllProjectsComponent + .selectedProjectPermissionMap[ + this.listAllProjectsComponent.selectedRow[i] + .name + ], + this.listAllProjectsComponent + .selectedProjectPermissionMapForEdit[ + this.listAllProjectsComponent.selectedRow[i] + .name + ] + ) + ) { + return true; + } + } + } + } + return false; } save() { const robot: Robot = clone(this.systemRobot); @@ -424,37 +373,27 @@ export class NewRobotComponent implements OnInit, OnDestroy { robot.level = PermissionsKinds.SYSTEM; robot.duration = +this.systemRobot.duration; robot.permissions = []; + if (this.permissionForSystem?.access?.length) { + robot.permissions.push(this.permissionForSystem); + } if (this.coverAll) { - const access: Access[] = []; - this.defaultAccesses.forEach(item => { - if (item.checked) { - access.push({ - resource: item.resource, - action: item.action, - }); - } - }); - robot.permissions.push({ - kind: PermissionsKinds.PROJECT, - namespace: NAMESPACE_ALL_PROJECTS, - access: access, - }); + if (this.permissionForCoverAll?.access?.length) { + robot.permissions.push(this.permissionForCoverAll); + } } else { this.listAllProjectsComponent.selectedRow.forEach(item => { - const access: Access[] = []; - item.permissions[0].access.forEach(item2 => { - if (item2.checked) { - access.push({ - resource: item2.resource, - action: item2.action, - }); - } - }); - robot.permissions.push({ - kind: PermissionsKinds.PROJECT, - namespace: item.name, - access: access, - }); + if ( + this.listAllProjectsComponent.selectedProjectPermissionMap[ + item.name + ]?.length + ) { + robot.permissions.push({ + kind: PermissionsKinds.PROJECT, + namespace: item.name, + access: this.listAllProjectsComponent + .selectedProjectPermissionMap[item.name], + }); + } }); } // Push permission must work with pull permission @@ -486,7 +425,7 @@ export class NewRobotComponent implements OnInit, OnDestroy { res => { this.saveBtnState = ClrLoadingState.SUCCESS; this.addSuccess.emit(null); - this.addRobotOpened = false; + this.cancel(); operateChanges(opeMessage, OperationState.success); this.msgHandler.showSuccess( 'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY' @@ -517,7 +456,7 @@ export class NewRobotComponent implements OnInit, OnDestroy { res => { this.saveBtnState = ClrLoadingState.SUCCESS; this.addSuccess.emit(res); - this.addRobotOpened = false; + this.cancel(); operateChanges(opeMessage, OperationState.success); }, error => { @@ -533,15 +472,6 @@ export class NewRobotComponent implements OnInit, OnDestroy { } } - getAccessNum(access: FrontAccess[]): number { - let count: number = 0; - access.forEach(item => { - if (item.checked) { - count++; - } - }); - return count; - } calculateExpiresAt(): Date { if ( this.systemRobot && @@ -559,26 +489,9 @@ export class NewRobotComponent implements OnInit, OnDestroy { return new Date() >= this.calculateExpiresAt(); } - isSelectAll(permissions: FrontAccess[]): boolean { - if (permissions?.length) { - return ( - permissions.filter(item => item.checked).length < - permissions.length / 2 - ); - } - return false; - } - selectAllOrUnselectAll(permissions: FrontAccess[]) { - if (permissions?.length) { - if (this.isSelectAll(permissions)) { - permissions.forEach(item => { - item.checked = true; - }); - } else { - permissions.forEach(item => { - item.checked = false; - }); - } - } + clrWizardPageOnLoad() { + this.showPage3 = true; } + + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.html b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.html index 4f9cca068..25b02fbe5 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.html +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.html @@ -12,7 +12,9 @@

{{ 'SYSTEM_ROBOT.PROJECTS_MODAL_SUMMARY' | translate }}

- + {{ 'PROJECT.NAME' | translate }} {{ 'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate @@ -29,36 +31,26 @@ > -
- - - -
- {{ i18nMap[item.action] | translate }} - {{ - i18nMap[item.resource] | translate - }} -
-
-
-
+ + +
{{ getProject(p)?.creation_time | harborDatetime : 'short' }} - + {{ 'PAGINATION.PAGE_SIZE' | translate }} diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.scss b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.scss index c4e5bc5b1..057b795d7 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.scss +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.scss @@ -14,13 +14,3 @@ font-size: 16px; opacity: 0.9; } - -.permissions { - height: 16px; - display: flex; - align-items: center; -} - -.datagrid-host { - position: inherit; -} \ No newline at end of file diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.ts index c09dd392e..9e71fe407 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/projects-modal/projects-modal.component.ts @@ -1,8 +1,12 @@ import { Component } from '@angular/core'; import { Project } from '../../../../../../ng-swagger-gen/models/project'; import { Router } from '@angular/router'; -import { ACTION_RESOURCE_I18N_MAP } from '../system-robot-util'; +import { PermissionsKinds } from '../system-robot-util'; import { RobotPermission } from '../../../../../../ng-swagger-gen/models/robot-permission'; +import { PermissionSelectPanelModes } from '../../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; +import { ProjectService } from '../../../../../../ng-swagger-gen/services/project.service'; +import { ClrDatagridStateInterface } from '@clr/angular'; +import { finalize } from 'rxjs/operators'; @Component({ selector: 'app-projects-modal', @@ -14,12 +18,52 @@ export class ProjectsModalComponent { robotName: string; cachedAllProjects: Project[]; permissions: RobotPermission[] = []; - i18nMap = ACTION_RESOURCE_I18N_MAP; - constructor(private router: Router) {} + pageSize: number = 10; + loading: boolean = false; + constructor( + private router: Router, + private projectService: ProjectService + ) {} close() { this.projectsModalOpened = false; } + clrDgRefresh(state?: ClrDatagridStateInterface) { + if (this.permissions.length) { + if (state) { + this.pageSize = state.page.size; + this.getProjectFromBackend( + this.permissions.slice(state.page.from, state.page.to + 1) + ); + } else { + this.getProjectFromBackend( + this.permissions.slice(0, this.pageSize) + ); + } + } + } + getProjectFromBackend(permissions: RobotPermission[]) { + const projectNames: string[] = []; + permissions?.forEach(item => { + if (item?.kind === PermissionsKinds.PROJECT) { + projectNames.push(item?.namespace); + } + }); + this.loading = true; + this.projectService + .listProjects({ + withDetail: false, + page: 1, + pageSize: permissions?.length, + q: encodeURIComponent(`name={${projectNames.join(' ')}}`), + }) + .pipe(finalize(() => (this.loading = false))) + .subscribe(res => { + if (res?.length) { + this.cachedAllProjects = res; + } + }); + } getProject(p: RobotPermission): Project { if (this.cachedAllProjects && this.cachedAllProjects.length) { for (let i = 0; i < this.cachedAllProjects.length; i++) { @@ -33,4 +77,6 @@ export class ProjectsModalComponent { goToLink(proId: number): void { this.router.navigate(['harbor', 'projects', proId]); } + + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.html b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.html index 106941050..2f6ba80ce 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.html +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.html @@ -19,7 +19,7 @@
+ +
{{ 'SYSTEM_ROBOT.ALL_PROJECTS' | translate }} - - - -
- {{ i18nMap[item.action] | translate }} - {{ - i18nMap[item.resource] | translate - }} -
-
-
+
- 0 {{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }} + {{ 'SCHEDULE.NONE' | translate }}
@@ -243,6 +254,8 @@
- + diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.scss b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.scss index f1540490d..cb09f7136 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.scss +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.scss @@ -33,3 +33,9 @@ align-items: center; height: 16px; } + + +.icon { + margin-top: 3px; +} + diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.ts index 005015945..e71cae04f 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-accounts.component.ts @@ -21,15 +21,15 @@ import { } from 'rxjs/operators'; import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { - ACTION_RESOURCE_I18N_MAP, FrontRobot, + getSystemAccess, NAMESPACE_ALL_PROJECTS, + NEW_EMPTY_ROBOT, PermissionsKinds, } from './system-robot-util'; import { ProjectsModalComponent } from './projects-modal/projects-modal.component'; import { forkJoin, Observable, of, Subscription } from 'rxjs'; import { FilterComponent } from '../../../shared/components/filter/filter.component'; -import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service'; import { HttpErrorResponse } from '@angular/common/http'; import { operateChanges, @@ -37,7 +37,6 @@ import { OperationState, } from '../../../shared/components/operation/operate'; import { OperationService } from '../../../shared/components/operation/operation.service'; -import { Project } from '../../../../../ng-swagger-gen/models/project'; import { DomSanitizer } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { ConfirmationDialogService } from '../../global-confirmation-dialog/confirmation-dialog.service'; @@ -50,8 +49,10 @@ import { errorHandler } from '../../../shared/units/shared.utils'; import { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message'; import { RobotPermission } from '../../../../../ng-swagger-gen/models/robot-permission'; import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service'; - -const FIRST_PROJECTS_PAGE_SIZE: number = 100; +import { Access } from '../../../../../ng-swagger-gen/models/access'; +import { PermissionSelectPanelModes } from '../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; +import { PermissionsService } from '../../../../../ng-swagger-gen/services/permissions.service'; +import { Permissions } from '../../../../../ng-swagger-gen/models/permissions'; @Component({ selector: 'system-robot-accounts', @@ -59,7 +60,6 @@ const FIRST_PROJECTS_PAGE_SIZE: number = 100; styleUrls: ['./system-robot-accounts.component.scss'], }) export class SystemRobotAccountsComponent implements OnInit, OnDestroy { - i18nMap = ACTION_RESOURCE_I18N_MAP; pageSize: number = getPageSizeFromLocalStorage( PageSizeMapKeys.SYSTEM_ROBOT_COMPONENT ); @@ -82,19 +82,22 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { searchKey: string; subscription: Subscription; deltaTime: number; // the different between server time and local time + + robotMetadata: Permissions; + loadingMetadata: boolean = false; constructor( private robotService: RobotService, - private projectService: ProjectService, private msgHandler: MessageHandlerService, private operateDialogService: ConfirmationDialogService, private operationService: OperationService, private sanitizer: DomSanitizer, private translate: TranslateService, - private systemInfoService: SysteminfoService + private systemInfoService: SysteminfoService, + private permissionService: PermissionsService ) {} ngOnInit() { + this.getRobotPermissions(); this.getCurrenTime(); - this.loadDataFromBackend(); if (!this.searchSub) { this.searchSub = this.filterComponent.filterTerms .pipe( @@ -169,6 +172,17 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { this.subscription = null; } } + + getRobotPermissions() { + this.loadingData = true; + this.permissionService + .getPermissions() + .pipe(finalize(() => (this.loadingData = false))) + .subscribe(res => { + this.robotMetadata = res; + }); + } + getCurrenTime() { this.systemInfoService.getSystemInfo().subscribe(res => { if (res?.current_time) { @@ -178,89 +192,7 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { } }); } - loadDataFromBackend() { - this.loadingData = true; - this.addBtnState = ClrLoadingState.LOADING; - this.projectService - .listProjectsResponse({ - withDetail: false, - page: 1, - pageSize: FIRST_PROJECTS_PAGE_SIZE, - }) - .subscribe( - result => { - // Get total count - if (result.headers) { - const xHeader: string = - result.headers.get('X-Total-Count'); - const totalCount = parseInt(xHeader, 0); - let arr = result.body || []; - if (totalCount <= FIRST_PROJECTS_PAGE_SIZE) { - // already gotten all projects - if ( - this.newRobotComponent && - this.newRobotComponent.listAllProjectsComponent - ) { - this.newRobotComponent.listAllProjectsComponent.cachedAllProjects = - result.body; - } - if (this.projectsModalComponent) { - this.projectsModalComponent.cachedAllProjects = - result.body; - } - this.loadingData = false; - this.addBtnState = ClrLoadingState.ERROR; - } else { - // get all the projects in specified times - const times: number = Math.ceil( - totalCount / FIRST_PROJECTS_PAGE_SIZE - ); - const observableList: Observable[] = []; - for (let i = 2; i <= times; i++) { - observableList.push( - this.projectService.listProjects({ - withDetail: false, - page: i, - pageSize: FIRST_PROJECTS_PAGE_SIZE, - }) - ); - } - forkJoin(observableList) - .pipe( - finalize(() => { - this.loadingData = false; - this.addBtnState = - ClrLoadingState.ERROR; - }) - ) - .subscribe(res => { - if (res && res.length) { - res.forEach(item => { - arr = arr.concat(item); - }); - if ( - this.newRobotComponent && - this.newRobotComponent - .listAllProjectsComponent - ) { - this.newRobotComponent.listAllProjectsComponent.cachedAllProjects = - arr; - } - if (this.projectsModalComponent) { - this.projectsModalComponent.cachedAllProjects = - arr; - } - } - }); - } - } - }, - error => { - this.loadingData = false; - this.addBtnState = ClrLoadingState.ERROR; - } - ); - } + clrLoad(state?: ClrDatagridStateInterface) { if (state && state.page && state.page.size) { this.pageSize = state.page.size; @@ -337,7 +269,7 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { } } } - getProjects(r: FrontRobot): RobotPermission[] { + getProjects(r: Robot): RobotPermission[] { const arr = []; if (r && r.permissions && r.permissions.length) { for (let i = 0; i < r.permissions.length; i++) { @@ -352,6 +284,7 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { this.projectsModalComponent.projectsModalOpened = true; this.projectsModalComponent.robotName = robotName; this.projectsModalComponent.permissions = permissions; + this.projectsModalComponent.clrDgRefresh(); } refresh() { this.currentPage = 1; @@ -492,4 +425,11 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy { } this.refresh(); } + + getSystemAccess(r: Robot): Access[] { + return getSystemAccess(r); + } + + protected readonly NEW_EMPTY_ROBOT = NEW_EMPTY_ROBOT; + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts index 674f63215..49a88b2c0 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts @@ -1,6 +1,7 @@ import { Robot } from '../../../../../ng-swagger-gen/models/robot'; import { Access } from '../../../../../ng-swagger-gen/models/access'; -import { Project } from '../../../../../ng-swagger-gen/models/project'; +import { RobotPermission } from '../../../../../ng-swagger-gen/models/robot-permission'; +import { Permission } from '../../../../../ng-swagger-gen/models/permission'; export interface FrontRobot extends Robot { permissionScope?: { @@ -9,14 +10,6 @@ export interface FrontRobot extends Robot { }; } -export interface FrontProjectForAdd extends Project { - permissions?: Array<{ - kind?: string; - namespace?: string; - access?: Array; - }>; -} - export interface FrontAccess extends Access { checked?: boolean; } @@ -43,78 +36,7 @@ export enum Action { export const NAMESPACE_ALL_PROJECTS: string = '*'; -export const INITIAL_ACCESSES: FrontAccess[] = [ - { - resource: 'repository', - action: 'list', - checked: true, - }, - { - resource: 'repository', - action: 'pull', - checked: true, - }, - { - resource: 'repository', - action: 'push', - checked: true, - }, - { - resource: 'repository', - action: 'delete', - checked: true, - }, - { - resource: 'artifact', - action: 'read', - checked: true, - }, - { - resource: 'artifact', - action: 'list', - checked: true, - }, - { - resource: 'artifact', - action: 'delete', - checked: true, - }, - { - resource: 'artifact-label', - action: 'create', - checked: true, - }, - { - resource: 'artifact-label', - action: 'delete', - checked: true, - }, - { - resource: 'tag', - action: 'create', - checked: true, - }, - { - resource: 'tag', - action: 'delete', - checked: true, - }, - { - resource: 'tag', - action: 'list', - checked: true, - }, - { - resource: 'scan', - action: 'create', - checked: true, - }, - { - resource: 'scan', - action: 'stop', - checked: true, - }, -]; +export const NAMESPACE_SYSTEM: string = '/'; export const ACTION_RESOURCE_I18N_MAP = { push: 'SYSTEM_ROBOT.PUSH_AND_PULL', // push permission contains pull permission @@ -122,16 +44,45 @@ export const ACTION_RESOURCE_I18N_MAP = { read: 'SYSTEM_ROBOT.READ', create: 'SYSTEM_ROBOT.CREATE', delete: 'SYSTEM_ROBOT.DELETE', - repository: 'SYSTEM_ROBOT.REPOSITORY', - artifact: 'SYSTEM_ROBOT.ARTIFACT', - tag: 'REPLICATION.TAG', - 'artifact-label': 'SYSTEM_ROBOT.ARTIFACT_LABEL', scan: 'SYSTEM_ROBOT.SCAN', - 'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL', stop: 'SYSTEM_ROBOT.STOP', list: 'SYSTEM_ROBOT.LIST', + update: 'ROBOT_ACCOUNT.UPDATE', + 'audit-log': 'ROBOT_ACCOUNT.AUDIT_LOG', + 'preheat-instance': 'ROBOT_ACCOUNT.PREHEAT_INSTANCE', + project: 'ROBOT_ACCOUNT.PROJECT', + 'replication-policy': 'ROBOT_ACCOUNT.REPLICATION_POLICY', + replication: 'ROBOT_ACCOUNT.REPLICATION', + 'replication-adapter': 'ROBOT_ACCOUNT.REPLICATION_ADAPTER', + registry: 'ROBOT_ACCOUNT.REGISTRY', + 'scan-all': 'ROBOT_ACCOUNT.SCAN_ALL', + 'system-volumes': 'ROBOT_ACCOUNT.SYSTEM_VOLUMES', + 'garbage-collection': 'ROBOT_ACCOUNT.GARBAGE_COLLECTION', + 'purge-audit': 'ROBOT_ACCOUNT.PURGE_AUDIT', + 'jobservice-monitor': 'ROBOT_ACCOUNT.JOBSERVICE_MONITOR', + 'tag-retention': 'ROBOT_ACCOUNT.TAG_RETENTION', + scanner: 'ROBOT_ACCOUNT.SCANNER', + label: 'ROBOT_ACCOUNT.LABEL', + 'export-cve': 'ROBOT_ACCOUNT.EXPORT_CVE', + 'security-hub': 'ROBOT_ACCOUNT.SECURITY_HUB', + catalog: 'ROBOT_ACCOUNT.CATALOG', + metadata: 'ROBOT_ACCOUNT.METADATA', + repository: 'ROBOT_ACCOUNT.REPOSITORY', + artifact: 'ROBOT_ACCOUNT.ARTIFACT', + tag: 'ROBOT_ACCOUNT.TAG', + accessory: 'ROBOT_ACCOUNT.ACCESSORY', + 'artifact-addition': 'ROBOT_ACCOUNT.ARTIFACT_ADDITION', + 'artifact-label': 'ROBOT_ACCOUNT.ARTIFACT_LABEL', + 'preheat-policy': 'ROBOT_ACCOUNT.PREHEAT_POLICY', + 'immutable-tag': 'ROBOT_ACCOUNT.IMMUTABLE_TAG', + log: 'ROBOT_ACCOUNT.LOG', + 'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY', }; +export function convertKey(key: string) { + return ACTION_RESOURCE_I18N_MAP[key] ? ACTION_RESOURCE_I18N_MAP[key] : key; +} + export enum ExpirationType { DEFAULT = 'default', DAYS = 'days', @@ -168,3 +119,66 @@ export enum RobotTimeRemainColor { WARNING = 'yellow', EXPIRED = 'red', } + +export function isCandidate( + candidatePermissions: Permission[], + permission: Access +): boolean { + if (candidatePermissions?.length) { + for (let i = 0; i < candidatePermissions.length; i++) { + if ( + candidatePermissions[i].resource === permission.resource && + candidatePermissions[i].action === permission.action + ) { + return true; + } + } + } + return false; +} + +export function hasPermission( + permissions: Access[], + permission: Access +): boolean { + if (permissions?.length) { + for (let i = 0; i < permissions.length; i++) { + if ( + permissions[i].resource === permission.resource && + permissions[i].action === permission.action + ) { + return true; + } + } + } + return false; +} + +export const NEW_EMPTY_ROBOT: Robot = { + permissions: [ + { + access: [], + }, + ], +}; + +export function getSystemAccess(r: Robot): Access[] { + let systemPermissions: RobotPermission[] = []; + if (r?.permissions?.length) { + systemPermissions = r.permissions.filter( + item => item.kind === PermissionsKinds.SYSTEM + ); + } + if (systemPermissions?.length) { + const map = {}; + systemPermissions.forEach(p => { + if (p?.access?.length) { + p.access.forEach(item => { + map[`${item.resource}@${item.action}`] = item; + }); + } + }); + return Object.values(map); + } + return []; +} diff --git a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.html b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.html index 07fe37c98..b03048fce 100644 --- a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.html +++ b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.html @@ -1,25 +1,45 @@ - - - - - - + + + + {{ + 'ROBOT_ACCOUNT.SELECT_PERMISSIONS' | translate + }} + +
+
+ + +
+
+
+ diff --git a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.scss b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.scss index 32e5a8f81..ce53431ce 100644 --- a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.scss +++ b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.scss @@ -34,44 +34,26 @@ } .input-width { - width: 232px; + width: 16rem; } -.expiration-width { - width: 80px; +.expiration { + margin-left: 1rem; } -.check { - margin-right: 5px; - color: green; -} - -.dropdown-per { - margin-left: -12px; -} - -.mt-description { - width: 238px; -} - -.mt-8px { - margin-top: 8px !important; -} /* stylelint-disable */ .showWarning { color: #b3a000; } -.dropdown-item { - min-height: 20px; - display: flex; +.icon { + margin-top: 3px; +} + +.align-center { align-items: center; } -.overflow-y-scroll { - overflow-y: auto; -} - -.ml-20px { - margin-left: 20px; +:host::ng-deep.modal-dialog { + width: 46.2rem; } diff --git a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts index 6dea00c0c..29f239c35 100644 --- a/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts +++ b/src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts @@ -16,30 +16,30 @@ import { } from 'rxjs/operators'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { - ACTION_RESOURCE_I18N_MAP, ExpirationType, - FrontAccess, - INITIAL_ACCESSES, + NEW_EMPTY_ROBOT, onlyHasPushPermission, PermissionsKinds, } from '../../../left-side-nav/system-robot-accounts/system-robot-util'; import { Robot } from '../../../../../../ng-swagger-gen/models/robot'; import { NgForm } from '@angular/forms'; -import { ClrLoadingState } from '@clr/angular'; +import { ClrLoadingState, ClrWizard } from '@clr/angular'; import { Subject, Subscription } from 'rxjs'; import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.service'; import { OperationService } from '../../../../shared/components/operation/operation.service'; -import { clone } from '../../../../shared/units/utils'; +import { clone, isSameArrayValue } from '../../../../shared/units/utils'; import { operateChanges, OperateInfo, OperationState, } from '../../../../shared/components/operation/operate'; -import { Access } from '../../../../../../ng-swagger-gen/models/access'; import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component'; import { errorHandler } from '../../../../shared/units/shared.utils'; +import { PermissionSelectPanelModes } from '../../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; +import { Permissions } from '../../../../../../ng-swagger-gen/models/permissions'; const MINI_SECONDS_ONE_DAY: number = 60 * 24 * 60 * 1000; + @Component({ selector: 'add-robot', templateUrl: './add-robot.component.html', @@ -48,25 +48,27 @@ const MINI_SECONDS_ONE_DAY: number = 60 * 24 * 60 * 1000; export class AddRobotComponent implements OnInit, OnDestroy { @Input() projectId: number; @Input() projectName: string; - i18nMap = ACTION_RESOURCE_I18N_MAP; isEditMode: boolean = false; originalRobotForEdit: Robot; @Output() addSuccess: EventEmitter = new EventEmitter(); addRobotOpened: boolean = false; - systemRobot: Robot = {}; + robot: Robot = clone(NEW_EMPTY_ROBOT); expirationType: string = ExpirationType.DAYS; isNameExisting: boolean = false; loading: boolean = false; checkNameOnGoing: boolean = false; - defaultAccesses: FrontAccess[] = []; - defaultAccessesForEdit: FrontAccess[] = []; @ViewChild(InlineAlertComponent) inlineAlertComponent: InlineAlertComponent; - @ViewChild('robotForm', { static: true }) robotForm: NgForm; + @ViewChild('robotBasicForm', { static: true }) robotBasicForm: NgForm; saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; private _nameSubject: Subject = new Subject(); private _nameSubscription: Subscription; + + @Input() + robotMetadata: Permissions; + + @ViewChild('wizard') wizard: ClrWizard; constructor( private robotService: RobotService, private msgHandler: MessageHandlerService, @@ -119,10 +121,10 @@ export class AddRobotComponent implements OnInit, OnDestroy { } } isExpirationInvalid(): boolean { - return this.systemRobot.duration < -1; + return this.robot.duration < -1; } inputExpiration() { - if (+this.systemRobot.duration === -1) { + if (+this.robot.duration === -1) { this.expirationType = ExpirationType.NEVER; } else { this.expirationType = ExpirationType.DAYS; @@ -130,60 +132,38 @@ export class AddRobotComponent implements OnInit, OnDestroy { } changeExpirationType() { if (this.expirationType === ExpirationType.DAYS) { - this.systemRobot.duration = null; + this.robot.duration = null; } if (this.expirationType === ExpirationType.NEVER) { - this.systemRobot.duration = -1; + this.robot.duration = -1; } } inputName() { - this._nameSubject.next(this.systemRobot.name); + this._nameSubject.next(this.robot.name); } + cancel() { + this.wizard.reset(); + this.reset(); this.addRobotOpened = false; } - getPermissions(): number { - let count: number = 0; - this.defaultAccesses.forEach(item => { - if (item.checked) { - count++; - } - }); - return count; - } - chooseAccess(access: FrontAccess) { - access.checked = !access.checked; - } + reset() { this.open(false); - this.defaultAccesses = clone(INITIAL_ACCESSES); - this.systemRobot = {}; - this.robotForm.reset(); + this.robot = clone(NEW_EMPTY_ROBOT); + this.robotBasicForm.reset(); this.expirationType = ExpirationType.DAYS; } resetForEdit(robot: Robot) { this.open(true); - this.defaultAccesses = clone(INITIAL_ACCESSES); - this.defaultAccesses.forEach(item => (item.checked = false)); this.originalRobotForEdit = clone(robot); - this.systemRobot = robot; + this.robot = clone(robot); this.expirationType = robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS; - this.defaultAccesses.forEach(item => { - this.systemRobot.permissions[0].access.forEach(item2 => { - if ( - item.resource === item2.resource && - item.action === item2.action - ) { - item.checked = true; - } - }); - }); - this.defaultAccessesForEdit = clone(this.defaultAccesses); - this.robotForm.reset({ - name: this.systemRobot.name, - expiration: this.systemRobot.duration, - description: this.systemRobot.description, + this.robotBasicForm.reset({ + name: this.robot.name, + expiration: this.robot.duration, + description: this.robot.description, }); } open(isEditMode: boolean) { @@ -200,75 +180,37 @@ export class AddRobotComponent implements OnInit, OnDestroy { return !this.canEdit(); } canAdd(): boolean { - let flag = false; - this.defaultAccesses.forEach(item => { - if (item.checked) { - flag = true; - } - }); - if (!flag) { - return false; - } - return !this.robotForm.invalid; + return ( + this.robot?.permissions[0]?.access?.length > 0 && + !this.robotBasicForm.invalid + ); } canEdit() { if (!this.canAdd()) { return false; } // eslint-disable-next-line eqeqeq - if (this.systemRobot.duration != this.originalRobotForEdit.duration) { + if (this.robot.duration != this.originalRobotForEdit.duration) { return true; } // eslint-disable-next-line eqeqeq - if ( - this.systemRobot.description != - this.originalRobotForEdit.description - ) { + if (this.robot.description != this.originalRobotForEdit.description) { return true; } - if ( - this.getAccessNum(this.defaultAccesses) !== - this.getAccessNum(this.defaultAccessesForEdit) - ) { - return true; - } - let flag = true; - this.defaultAccessesForEdit.forEach(item => { - this.defaultAccesses.forEach(item2 => { - if ( - item.resource === item2.resource && - item.action === item2.action && - item.checked !== item2.checked - ) { - flag = false; - } - }); - }); - return !flag; + return !isSameArrayValue( + this.robot.permissions[0].access, + this.originalRobotForEdit.permissions[0].access + ); } save() { - const robot: Robot = clone(this.systemRobot); + const robot: Robot = clone(this.robot); robot.disable = false; robot.level = PermissionsKinds.PROJECT; - robot.duration = +this.systemRobot.duration; - const access: Access[] = []; - this.defaultAccesses.forEach(item => { - if (item.checked) { - access.push({ - resource: item.resource, - action: item.action, - }); - } - }); - robot.permissions = [ - { - namespace: this.projectName, - kind: PermissionsKinds.PROJECT, - access: access, - }, - ]; + robot.duration = +this.robot.duration; + robot.permissions[0].kind = PermissionsKinds.PROJECT; + robot.permissions[0].namespace = this.projectName; // Push permission must work with pull permission - if (onlyHasPushPermission(access)) { + if (onlyHasPushPermission(robot.permissions[0].access)) { this.inlineAlertComponent.showInlineError( 'SYSTEM_ROBOT.PUSH_PERMISSION_TOOLTIP' ); @@ -276,7 +218,7 @@ export class AddRobotComponent implements OnInit, OnDestroy { } this.saveBtnState = ClrLoadingState.LOADING; if (this.isEditMode) { - robot.disable = this.systemRobot.disable; + robot.disable = this.robot.disable; const opeMessage = new OperateInfo(); opeMessage.name = 'SYSTEM_ROBOT.UPDATE_ROBOT'; opeMessage.data.id = robot.id; @@ -292,7 +234,7 @@ export class AddRobotComponent implements OnInit, OnDestroy { res => { this.saveBtnState = ClrLoadingState.SUCCESS; this.addSuccess.emit(null); - this.addRobotOpened = false; + this.cancel(); operateChanges(opeMessage, OperationState.success); this.msgHandler.showSuccess( 'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY' @@ -324,7 +266,7 @@ export class AddRobotComponent implements OnInit, OnDestroy { this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS; this.addSuccess.emit(res); - this.addRobotOpened = false; + this.cancel(); operateChanges(opeMessage, OperationState.success); }, error => { @@ -339,24 +281,12 @@ export class AddRobotComponent implements OnInit, OnDestroy { ); } } - getAccessNum(access: FrontAccess[]): number { - let count: number = 0; - access.forEach(item => { - if (item.checked) { - count++; - } - }); - return count; - } + calculateExpiresAt(): Date { - if ( - this.systemRobot && - this.systemRobot.creation_time && - this.systemRobot.duration > 0 - ) { + if (this.robot && this.robot.creation_time && this.robot.duration > 0) { return new Date( - new Date(this.systemRobot.creation_time).getTime() + - this.systemRobot.duration * MINI_SECONDS_ONE_DAY + new Date(this.robot.creation_time).getTime() + + this.robot.duration * MINI_SECONDS_ONE_DAY ); } return null; @@ -364,26 +294,6 @@ export class AddRobotComponent implements OnInit, OnDestroy { shouldShowWarning(): boolean { return new Date() >= this.calculateExpiresAt(); } - isSelectAll(permissions: FrontAccess[]): boolean { - if (permissions?.length) { - return ( - permissions.filter(item => item.checked).length < - permissions.length / 2 - ); - } - return false; - } - selectAllOrUnselectAll(permissions: FrontAccess[]) { - if (permissions?.length) { - if (this.isSelectAll(permissions)) { - permissions.forEach(item => { - item.checked = true; - }); - } else { - permissions.forEach(item => { - item.checked = false; - }); - } - } - } + + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/base/project/robot-account/robot-account.component.html b/src/portal/src/app/base/project/robot-account/robot-account.component.html index 4b007c078..d2bcec725 100644 --- a/src/portal/src/app/base/project/robot-account/robot-account.component.html +++ b/src/portal/src/app/base/project/robot-account/robot-account.component.html @@ -18,7 +18,7 @@
- -
- {{ i18nMap[item.action] | translate }} - {{ - i18nMap[item.resource] | translate - }} -
-
- -
+ + + {{ r.creation_time | harborDatetime : 'short' @@ -238,6 +224,7 @@ diff --git a/src/portal/src/app/base/project/robot-account/robot-account.component.scss b/src/portal/src/app/base/project/robot-account/robot-account.component.scss index dd2c6b1dc..55e5765d2 100644 --- a/src/portal/src/app/base/project/robot-account/robot-account.component.scss +++ b/src/portal/src/app/base/project/robot-account/robot-account.component.scss @@ -27,12 +27,7 @@ } } -.permissions { - height: 16px; - display: flex; - align-items: center; -} -.datagrid-host { - position: inherit; +.icon { + margin-top: 3px; } diff --git a/src/portal/src/app/base/project/robot-account/robot-account.component.spec.ts b/src/portal/src/app/base/project/robot-account/robot-account.component.spec.ts index fc0150119..19ba9bea0 100644 --- a/src/portal/src/app/base/project/robot-account/robot-account.component.spec.ts +++ b/src/portal/src/app/base/project/robot-account/robot-account.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { of, Subscription } from 'rxjs'; -import { ActivatedRoute, RouterModule } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { RobotAccountComponent } from './robot-account.component'; import { UserPermissionService } from '../../../shared/services'; diff --git a/src/portal/src/app/base/project/robot-account/robot-account.component.ts b/src/portal/src/app/base/project/robot-account/robot-account.component.ts index d1aed59c7..477b0c9df 100644 --- a/src/portal/src/app/base/project/robot-account/robot-account.component.ts +++ b/src/portal/src/app/base/project/robot-account/robot-account.component.ts @@ -13,10 +13,7 @@ import { UserPermissionService, USERSTATICPERMISSION, } from '../../../shared/services'; -import { - ACTION_RESOURCE_I18N_MAP, - PermissionsKinds, -} from '../../left-side-nav/system-robot-accounts/system-robot-util'; +import { PermissionsKinds } from '../../left-side-nav/system-robot-accounts/system-robot-util'; import { clone, getPageSizeFromLocalStorage, @@ -50,6 +47,9 @@ import { import { errorHandler } from '../../../shared/units/shared.utils'; import { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message'; import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service'; +import { PermissionSelectPanelModes } from '../../../shared/components/robot-permissions-panel/robot-permissions-panel.component'; +import { PermissionsService } from '../../../../../ng-swagger-gen/services/permissions.service'; +import { Permissions } from '../../../../../ng-swagger-gen/models/permissions'; @Component({ selector: 'app-robot-account', @@ -57,7 +57,6 @@ import { SysteminfoService } from '../../../../../ng-swagger-gen/services/system styleUrls: ['./robot-account.component.scss'], }) export class RobotAccountComponent implements OnInit, OnDestroy { - i18nMap = ACTION_RESOURCE_I18N_MAP; pageSize: number = getPageSizeFromLocalStorage( PageSizeMapKeys.PROJECT_ROBOT_COMPONENT ); @@ -83,6 +82,9 @@ export class RobotAccountComponent implements OnInit, OnDestroy { projectId: number; projectName: string; deltaTime: number; // the different between server time and local time + + loadingMetadata: boolean = false; + robotMetadata: Permissions; constructor( private robotService: RobotService, private msgHandler: MessageHandlerService, @@ -92,7 +94,8 @@ export class RobotAccountComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private translate: TranslateService, private sanitizer: DomSanitizer, - private systemInfoService: SysteminfoService + private systemInfoService: SysteminfoService, + private permissionService: PermissionsService ) {} ngOnInit() { this.getCurrenTime(); @@ -171,6 +174,17 @@ export class RobotAccountComponent implements OnInit, OnDestroy { ); } } + + getRobotPermissions() { + this.loadingMetadata = true; + this.permissionService + .getPermissions() + .pipe(finalize(() => (this.loadingMetadata = false))) + .subscribe(res => { + this.robotMetadata = res; + }); + } + getCurrenTime() { this.systemInfoService.getSystemInfo().subscribe(res => { if (res?.current_time) { @@ -214,6 +228,9 @@ export class RobotAccountComponent implements OnInit, OnDestroy { forkJoin(...permissionsList).subscribe( Rules => { this.hasRobotCreatePermission = Rules[0] as boolean; + if (this.hasRobotCreatePermission) { + this.getRobotPermissions(); + } this.hasRobotUpdatePermission = Rules[1] as boolean; this.hasRobotDeletePermission = Rules[2] as boolean; this.hasRobotReadPermission = Rules[3] as boolean; @@ -418,4 +435,5 @@ export class RobotAccountComponent implements OnInit, OnDestroy { } this.refresh(); } + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; } diff --git a/src/portal/src/app/shared/components/remaining-time/remaining-time.component.spec.ts b/src/portal/src/app/shared/components/remaining-time/remaining-time.component.spec.ts index b461c18fa..98d19e606 100644 --- a/src/portal/src/app/shared/components/remaining-time/remaining-time.component.spec.ts +++ b/src/portal/src/app/shared/components/remaining-time/remaining-time.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RemainingTimeComponent } from './remaining-time.component'; import { Component, ViewChild } from '@angular/core'; -import { Project } from '../../../../../ng-swagger-gen/models/project'; import { RobotTimeRemainColor } from '../../../base/left-side-nav/system-robot-accounts/system-robot-util'; import { SharedTestingModule } from '../../shared.module'; diff --git a/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.html b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.html new file mode 100644 index 000000000..537a75ce3 --- /dev/null +++ b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.html @@ -0,0 +1,136 @@ + +
+ +
+ + + +
+ + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ {{ 'AUDIT_LOG.RESOURCE' | translate }} + + {{ convertKey(item) | translate }} +
+ {{ convertKey(resource) | translate }} + + +
+
+
+
+ + +
+ +
+ + + + + + + + + + + + + +
{{ 'AUDIT_LOG.RESOURCE' | translate }} + {{ convertKey(item) | translate }} +
{{ convertKey(resource) | translate }} + +
+
diff --git a/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.scss b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.scss new file mode 100644 index 000000000..94e91503f --- /dev/null +++ b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.scss @@ -0,0 +1,19 @@ +.td { + height: 24px; + line-height: 24px; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.dropdown-menu { + max-width: unset; +} + +:host::ng-deep.trigger clr-icon { + padding: 0; + position: unset !important; +} + +.select-all-for-dropdown { + margin-bottom: 0.25rem; +} diff --git a/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.spec.ts b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.spec.ts new file mode 100644 index 000000000..34b0d51d2 --- /dev/null +++ b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.spec.ts @@ -0,0 +1,71 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, ViewChild } from '@angular/core'; +import { SharedTestingModule } from '../../shared.module'; +import { + PermissionSelectPanelModes, + RobotPermissionsPanelComponent, +} from './robot-permissions-panel.component'; + +describe('RobotPermissionsPanelComponent', () => { + let component: TestHostComponent; + let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharedTestingModule], + declarations: [TestHostComponent, RobotPermissionsPanelComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render right mode', async () => { + component.robotPermissionsPanelComponent.modalOpen = true; + fixture.detectChanges(); + await fixture.whenStable(); + const table = fixture.nativeElement.querySelector('table'); + expect(table).toBeTruthy(); + component.mode = PermissionSelectPanelModes.DROPDOWN; + fixture.detectChanges(); + await fixture.whenStable(); + const clrDropdown = fixture.nativeElement.querySelector('clr-dropdown'); + expect(clrDropdown).toBeTruthy(); + component.mode = PermissionSelectPanelModes.MODAL; + fixture.detectChanges(); + await fixture.whenStable(); + const modal = fixture.nativeElement.querySelector('clr-modal'); + expect(modal).toBeTruthy(); + }); +}); + +// mock a TestHostComponent for RobotPermissionsPanelComponent +@Component({ + template: ` + + +
modal
+
+
+ + +
dropDown
+
+
+ + + + `, +}) +class TestHostComponent { + @ViewChild(RobotPermissionsPanelComponent) + robotPermissionsPanelComponent: RobotPermissionsPanelComponent; + mode = PermissionSelectPanelModes.NORMAL; + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; +} diff --git a/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.ts b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.ts new file mode 100644 index 000000000..59ef454c2 --- /dev/null +++ b/src/portal/src/app/shared/components/robot-permissions-panel/robot-permissions-panel.component.ts @@ -0,0 +1,156 @@ +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import { + convertKey, + hasPermission, + isCandidate, +} from '../../../base/left-side-nav/system-robot-accounts/system-robot-util'; +import { Access } from '../../../../../ng-swagger-gen/models/access'; +import { Permission } from '../../../../../ng-swagger-gen/models/permission'; + +enum Position { + UP = 'left-bottom', + DOWN = 'left-top', +} + +@Component({ + selector: 'robot-permissions-panel', + templateUrl: './robot-permissions-panel.component.html', + styleUrls: ['./robot-permissions-panel.component.scss'], +}) +export class RobotPermissionsPanelComponent + implements AfterViewInit, OnChanges +{ + modalOpen: boolean = false; + + @Input() + mode: PermissionSelectPanelModes = PermissionSelectPanelModes.NORMAL; + + @Input() + dropdownPosition: string = 'bottom-left'; + + @Input() + usedInDatagrid: boolean = false; + + @Input() + candidatePermissions: Permission[] = []; + + candidateActions: string[] = []; + candidateResources: string[] = []; + + @Input() + permissionsModel!: Access[]; + @Output() + permissionsModelChange = new EventEmitter(); + + @ViewChild('dropdown') + clrDropdown: ElementRef; + + ngAfterViewInit() { + setTimeout(() => { + if (this.clrDropdown && this.usedInDatagrid) { + if ( + this.clrDropdown.nativeElement.getBoundingClientRect().y < + 488 + ) { + this.dropdownPosition = Position.DOWN; + } else { + this.dropdownPosition = Position.UP; + } + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes && changes['candidatePermissions']) { + this.initCandidates(); + } + } + + initCandidates() { + this.candidateActions = []; + this.candidateResources = []; + this.candidatePermissions?.forEach(item => { + if (this.candidateResources.indexOf(item?.resource) === -1) { + this.candidateResources.push(item?.resource); + } + if (this.candidateActions.indexOf(item?.action) === -1) { + this.candidateActions.push(item?.action); + } + }); + } + + isCandidate(resource: string, action: string): boolean { + return isCandidate(this.candidatePermissions, { resource, action }); + } + + getCheckBoxValue(resource: string, action: string): boolean { + return hasPermission(this.permissionsModel, { resource, action }); + } + + setCheckBoxValue(resource: string, action: string, value: boolean) { + if (value) { + if (!this.permissionsModel) { + this.permissionsModel = []; + } + this.permissionsModel.push({ resource, action }); + } else { + this.permissionsModel = this.permissionsModel.filter(item => { + return item.resource !== resource || item.action !== action; + }); + } + this.permissionsModelChange.emit(this.permissionsModel); + } + + isAllSelected(): boolean { + let flag: boolean = true; + this.candidateActions.forEach(action => { + this.candidateResources.forEach(resource => { + if ( + this.isCandidate(resource, action) && + !hasPermission(this.permissionsModel, { resource, action }) + ) { + flag = false; + } + }); + }); + return flag; + } + + selectAllOrUnselectAll() { + if (this.isAllSelected()) { + this.permissionsModel = []; + } else { + this.permissionsModel = []; + this.candidateActions.forEach(action => { + this.candidateResources.forEach(resource => { + if (this.isCandidate(resource, action)) { + this.permissionsModel.push({ resource, action }); + } + }); + }); + } + this.permissionsModelChange.emit(this.permissionsModel); + } + + convertKey(key: string): string { + return convertKey(key); + } + protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes; +} + +export enum PermissionSelectPanelModes { + DROPDOWN, + MODAL, + NORMAL, +} diff --git a/src/portal/src/app/shared/shared.module.ts b/src/portal/src/app/shared/shared.module.ts index 8418b0c76..5409a12e5 100644 --- a/src/portal/src/app/shared/shared.module.ts +++ b/src/portal/src/app/shared/shared.module.ts @@ -91,6 +91,7 @@ import { } from 'echarts/components'; import { LabelLayout, UniversalTransition } from 'echarts/features'; import { CanvasRenderer } from 'echarts/renderers'; +import { RobotPermissionsPanelComponent } from './components/robot-permissions-panel/robot-permissions-panel.component'; // register necessary components echarts.use([ @@ -175,6 +176,7 @@ ClarityIcons.add({ RemainingTimeComponent, LabelSelectorComponent, AppLevelAlertsComponent, + RobotPermissionsPanelComponent, ], exports: [ TranslateModule, @@ -217,6 +219,7 @@ ClarityIcons.add({ RemainingTimeComponent, LabelSelectorComponent, AppLevelAlertsComponent, + RobotPermissionsPanelComponent, ], providers: [ { provide: EndpointService, useClass: EndpointDefaultService }, diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index a925d04ad..83a02b344 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -378,7 +378,45 @@ "INVALID_VALUE": "Der Wert der Ablaufzeit ist ungültig", "NEVER_EXPIRED": "Läuft nie ab", "NAME_PREFIX": "Prefix für den Namen der Robot-Zugänge", - "NAME_PREFIX_REQUIRED": "Es ist ein Prefix für den Robot-Zugang erforderlich" + "NAME_PREFIX_REQUIRED": "Es ist ein Prefix für den Robot-Zugang erforderlich", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index f3b0424fb..bece1ab76 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -378,7 +378,45 @@ "INVALID_VALUE": "The value of the expiration time is invalid", "NEVER_EXPIRED": "Never Expires", "NAME_PREFIX": "Robot Name Prefix", - "NAME_PREFIX_REQUIRED": "Robot name prefix is required" + "NAME_PREFIX_REQUIRED": "Robot name prefix is required", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 999cab9d8..161268d57 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -379,7 +379,45 @@ "INVALID_VALUE": "The value of the expiration time is invalid", "NEVER_EXPIRED": "Never Expired", "NAME_PREFIX": "Robot Name Prefix", - "NAME_PREFIX_REQUIRED": "Robot name prefix is required" + "NAME_PREFIX_REQUIRED": "Robot name prefix is required", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 948ca843c..c217779aa 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -370,7 +370,45 @@ "INVALID_VALUE": "La valeur de la date d'expiration est invalide", "NEVER_EXPIRED": "Ne jamais expirer", "NAME_PREFIX": "Préfixe du nom du compte robot", - "NAME_PREFIX_REQUIRED": "Le préfixe du nom du compte robot est obligatoire" + "NAME_PREFIX_REQUIRED": "Le préfixe du nom du compte robot est obligatoire", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "Éditer", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 6690f50b2..98bc450a9 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -376,7 +376,45 @@ "INVALID_VALUE": "Valor do tempo de expiração inválido.", "NEVER_EXPIRED": "Não expirar nunca", "NAME_PREFIX": "Prefixo para contas de automação", - "NAME_PREFIX_REQUIRED": "Prefixo é obrigatório." + "NAME_PREFIX_REQUIRED": "Prefixo é obrigatório.", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "GROUP": { "GROUP": "Grupo", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 204de7e5f..29cbba0ee 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -378,7 +378,45 @@ "INVALID_VALUE": "The value of the expiration time is invalid", "NEVER_EXPIRED": "Never Expired", "NAME_PREFIX": "Robot Name Prefix", - "NAME_PREFIX_REQUIRED": "Robot name prefix is required" + "NAME_PREFIX_REQUIRED": "Robot name prefix is required", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "DÜZENLE", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 6485f9382..b64ab4926 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -377,7 +377,45 @@ "INVALID_VALUE": "无效的过期日期", "NEVER_EXPIRED": "永不过期", "NAME_PREFIX": "机器人账户名称前缀", - "NAME_PREFIX_REQUIRED": "机器人账户名称前缀为必填项" + "NAME_PREFIX_REQUIRED": "机器人账户名称前缀为必填项", + "UPDATE": "更新", + "AUDIT_LOG": "审核日志", + "PREHEAT_INSTANCE": "预热实例", + "PROJECT": "项目", + "REPLICATION_POLICY": "镜像复制策略", + "REPLICATION": "镜像复制", + "REPLICATION_ADAPTER": "镜像复制适配器", + "REGISTRY": "镜像库", + "SCAN_ALL": "扫描全部", + "SYSTEM_VOLUMES": "系统卷", + "GARBAGE_COLLECTION": "垃圾回收", + "PURGE_AUDIT": "日志清除", + "JOBSERVICE_MONITOR": "任务监视器", + "TAG_RETENTION": "Tag 保留", + "SCANNER": "扫描器", + "LABEL": "标签", + "EXPORT_CVE": "导出 CVE", + "SECURITY_HUB": "安全中心", + "CATALOG": "镜像目录", + "METADATA": "项目元数据", + "REPOSITORY": "仓库", + "ARTIFACT": "Artifact", + "SCAN": "扫描", + "TAG": "Tag", + "ACCESSORY": "附件", + "ARTIFACT_ADDITION": "Artifact 额外信息", + "ARTIFACT_LABEL": "Artifact 标签", + "PREHEAT_POLICY": "预热策略", + "IMMUTABLE_TAG": "不可变 Tag", + "LOG": "日志", + "NOTIFICATION_POLICY": "Webhook 策略", + "BACK": "上一步", + "NEXT": "下一步", + "FINISH": "完成", + "BASIC_INFO": "基本信息", + "SELECT_PERMISSIONS": "选择权限", + "SELECT_SYSTEM_PERMISSIONS": "选择系统权限", + "SELECT_PROJECT_PERMISSIONS": "选择项目权限" }, "WEBHOOK": { "EDIT_BUTTON": "编辑", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 455f308da..8a8bef052 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -377,7 +377,45 @@ "INVALID_VALUE": "無效的過期日期", "NEVER_EXPIRED": "永不過期", "NAME_PREFIX": "機器人名稱前綴", - "NAME_PREFIX_REQUIRED": "機器人名稱前綴為必填項目" + "NAME_PREFIX_REQUIRED": "機器人名稱前綴為必填項目", + "UPDATE": "Update", + "AUDIT_LOG": "Audit Log", + "PREHEAT_INSTANCE": "Preheat Instance", + "PROJECT": "Project", + "REPLICATION_POLICY": "Replication Policy", + "REPLICATION": "Replication", + "REPLICATION_ADAPTER": "Replication Adapter", + "REGISTRY": "Registry", + "SCAN_ALL": "Scan All", + "SYSTEM_VOLUMES": "System Volumes", + "GARBAGE_COLLECTION": "Garbage Collection", + "PURGE_AUDIT": "Purge Audit", + "JOBSERVICE_MONITOR": "Job Service Monitor", + "TAG_RETENTION": "Tag Retention", + "SCANNER": "Scanner", + "LABEL": "Label", + "EXPORT_CVE": "Export CVE", + "SECURITY_HUB": "Security Hub", + "CATALOG": "Catalog", + "METADATA": "Project Metadata", + "REPOSITORY": "Repository", + "ARTIFACT": "Artifact", + "SCAN": "Scan", + "TAG": "Tag", + "ACCESSORY": "Accessory", + "ARTIFACT_ADDITION": "Artifact Addition", + "ARTIFACT_LABEL": "Artifact Label", + "PREHEAT_POLICY": "Preheat Policy", + "IMMUTABLE_TAG": "Immutable Tag", + "LOG": "Log", + "NOTIFICATION_POLICY": "Notification Policy", + "BACK": "Back", + "NEXT": "Next", + "FINISH": "Finish", + "BASIC_INFO": "Basic Information", + "SELECT_PERMISSIONS": "Select Permissions", + "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", + "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions" }, "WEBHOOK": { "EDIT_BUTTON": "編輯",