Add full permissions for the robot account (#19507)

1.Fixes #19353

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2023-11-09 11:18:07 +08:00 committed by GitHub
parent 5c02fd807e
commit b7116fff0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1528 additions and 1218 deletions

View File

@ -1,53 +1,22 @@
<clr-datagrid [clrDgPreserveSelection]="true" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button
[disabled]="coverAll"
class="btn btn-secondary btn-sm"
clrDropdownTrigger>
<clr-datagrid
[clrDgPreserveSelection]="true"
[(clrDgSelected)]="selectedRow"
[clrDgLoading]="loadingData"
class="datagrid-compact">
<clr-dg-action-bar class="action-bar">
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.DROPDOWN"
[(permissionsModel)]="initialAccess"
(permissionsModelChange)="resetAccess(initialAccess)"
[candidatePermissions]="robotMetadata?.project">
<button class="btn btn-secondary btn-sm m-0">
{{ 'SYSTEM_ROBOT.RESET_PERMISSION' | translate }}
<clr-icon shape="caret down"></clr-icon>
<clr-icon size="12" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
[style.height.px]="230"
clrPosition="bottom-left"
*clrIfOpen>
<div>
<button
class="btn btn-link btn-sm select-all-for-dropdown ml-20px"
(click)="
selectAllPermissionOrUnselectAll(defaultAccesses);
resetAccess(defaultAccesses)
">
<span *ngIf="isSelectAll(defaultAccesses)">{{
'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span>
<span *ngIf="!isSelectAll(defaultAccesses)">{{
'SYSTEM_ROBOT.UNSELECT_ALL' | translate
}}</span>
</button>
</div>
<div
clrDropdownItem
*ngFor="let item of defaultAccesses"
(click)="chooseDefaultAccess(item)">
<clr-icon
class="check"
shape="check"
[style.visibility]="
item.checked ? 'visible' : 'hidden'
"></clr-icon>
<span
>{{ i18nMap[item.action] | translate }}
{{ i18nMap[item.resource] | translate }}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</robot-permissions-panel>
<button
(click)="selectAllOrUnselectAll()"
[disabled]="coverAll"
class="btn btn-secondary btn-sm ml-1">
class="btn btn-secondary btn-sm m-0 ml-1">
<span *ngIf="showSelectAll">{{
'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span>
@ -67,9 +36,7 @@
<clr-dg-column>{{
'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate
}}</clr-dg-column>
<clr-dg-row
*clrDgItems="let p of projects; let projectIndex = index"
[clrDgItem]="p">
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
<clr-dg-cell>
<a href="javascript:void(0)" [routerLink]="getLink(p.project_id)">{{
p.name
@ -79,62 +46,17 @@
p.creation_time | harborDatetime : 'short'
}}</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button
[disabled]="coverAll"
class="btn btn-link"
clrDropdownTrigger>
{{ getPermissions(p.permissions[0].access) }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
[style.height.px]="140"
clrPosition="bottom-left"
*clrIfOpen>
<div>
<button
class="btn btn-link btn-sm select-all-for-dropdown ml-20px"
(click)="
selectAllPermissionOrUnselectAll(
p.permissions[0].access
)
">
<span
*ngIf="isSelectAll(p.permissions[0].access)"
>{{
'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span
>
<span
*ngIf="
!isSelectAll(p.permissions[0].access)
"
>{{
'SYSTEM_ROBOT.UNSELECT_ALL' | translate
}}</span
>
</button>
</div>
<div
clrDropdownItem
*ngFor="let item of p.permissions[0].access"
(click)="chooseAccess(item)">
<clr-icon
class="check"
shape="check"
[style.visibility]="
item.checked ? 'visible' : 'hidden'
"></clr-icon>
<span
>{{ i18nMap[item.action] | translate }}
{{ i18nMap[item.resource] | translate }}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<robot-permissions-panel
[usedInDatagrid]="true"
[mode]="PermissionSelectPanelModes.DROPDOWN"
[(permissionsModel)]="selectedProjectPermissionMap[p.name]"
[candidatePermissions]="robotMetadata?.project">
<button class="btn btn-link btn-sm m-0 p-0">
{{ selectedProjectPermissionMap[p.name]?.length || 0 }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
</robot-permissions-panel>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
@ -142,7 +64,7 @@
#pagination
[(clrDgPage)]="currentPage"
[clrDgPageSize]="pageSize">
<clr-dg-page-size [clrPageSizeOptions]="[5, 15, 25]">{{
<clr-dg-page-size [clrPageSizeOptions]="[5, 10]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span

View File

@ -1,10 +1,9 @@
.check {
margin-right: 5px;
color: green;
.ml-20px {
margin-left: 20px;
}
.permissions {
height: 16px;
.action-bar {
margin-top: 0;
display: flex;
align-items: center;
}
@ -13,16 +12,12 @@
position: inherit;
}
.dropdown-menu {
overflow-y: auto;
:host::ng-deep.datagrid-spinner {
top: 5rem !important;
width: 62% !important;
height: 32% !important;
}
.dropdown-item {
min-height: 20px;
display: flex;
align-items: center;
}
.ml-20px {
margin-left: 20px;
:host::ng-deep.spinner {
left: 14rem !important;
}

View File

@ -1,7 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ListAllProjectsComponent } from './list-all-projects.component';
import { clone } from '../../../../shared/units/utils';
import { INITIAL_ACCESSES } from '../system-robot-util';
import { Project } from '../../../../../../ng-swagger-gen/models/project';
import { SharedTestingModule } from '../../../../shared/shared.module';
@ -30,9 +28,6 @@ describe('ListAllProjectsComponent', () => {
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');

View File

@ -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<Project> =
new CustomComparator<Project>('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<Project[]>[] = [];
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;
}

View File

@ -1,25 +1,42 @@
<clr-modal
clrModalSize="lg"
[(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true"
[clrModalClosable]="true">
<h3 *ngIf="!isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.CREATE_ROBOT' | translate }}
</h3>
<h3 *ngIf="isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.EDIT_ROBOT' | translate }}
</h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<clr-wizard
#wizard
[(clrWizardOpen)]="addRobotOpened"
(clrWizardOnCancel)="cancel()">
<clr-wizard-title>
<h3 *ngIf="!isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.CREATE_ROBOT' | translate }}
</h3>
<h3 *ngIf="isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.EDIT_ROBOT' | translate }}
</h3>
<p *ngIf="!isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.CREATE_ROBOT_SUMMARY' | translate }}
</p>
<p *ngIf="isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.EDIT_ROBOT_SUMMARY' | translate }}
</p>
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1">
</p></clr-wizard-title
>
<clr-wizard-button [type]="'cancel'">{{
'BUTTON.CANCEL' | translate
}}</clr-wizard-button>
<clr-wizard-button [type]="'previous'">{{
'ROBOT_ACCOUNT.BACK' | translate
}}</clr-wizard-button>
<clr-wizard-button [type]="'next'">{{
'ROBOT_ACCOUNT.NEXT' | translate
}}</clr-wizard-button>
<clr-wizard-button [clrLoading]="saveBtnState" [type]="'finish'">{{
'ROBOT_ACCOUNT.FINISH' | translate
}}</clr-wizard-button>
<clr-wizard-page
[clrWizardPageNextDisabled]="
!robotForm.valid || checkNameOnGoing || isNameExisting
">
<ng-template clrPageTitle>{{
'ROBOT_ACCOUNT.BASIC_INFO' | translate
}}</ng-template>
<form #robotForm="ngForm" class="clr-form clr-form-horizontal">
<section class="form-block">
<!-- name -->
<div class="clr-form-control">
<label for="name" class="clr-control-label required"
>{{ 'P2P_PROVIDER.NAME' | translate }}
@ -95,8 +112,17 @@
</clr-control-error>
</div>
</div>
<!-- expiration -->
<div class="clr-form-control">
<clr-textarea-container class="mt-2">
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea
class="input-width"
clrTextarea
type="text"
id="description"
name="description"
[(ngModel)]="systemRobot.description"></textarea>
</clr-textarea-container>
<div class="clr-form-control mt-2">
<label class="clr-control-label required"
>{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }}
<clr-tooltip>
@ -216,22 +242,41 @@
</clr-control-helper>
</div>
</div>
<!-- 3. description -->
<clr-textarea-container class="mt-description">
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea
class="input-width"
clrTextarea
type="text"
id="description"
name="description"
[(ngModel)]="systemRobot.description"></textarea>
</clr-textarea-container>
<div class="clr-form-control">
<label class="clr-control-label mt-8px">{{
</section>
</form>
</clr-wizard-page>
<clr-wizard-page class="pb-0">
<ng-template clrPageTitle>{{
'ROBOT_ACCOUNT.SELECT_SYSTEM_PERMISSIONS' | translate
}}</ng-template>
<form class="clr-form clr-form-horizontal">
<section class="form-block">
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.NORMAL"
[(permissionsModel)]="permissionForSystem.access"
[candidatePermissions]="robotMetadata?.system">
</robot-permissions-panel>
</section>
</form>
</clr-wizard-page>
<clr-wizard-page
class="pb-0"
(clrWizardPageOnLoad)="clrWizardPageOnLoad()"
(clrWizardPageOnCommit)="save()"
[clrWizardPagePreventDefaultNext]="true"
[clrWizardPageNextDisabled]="disabled()">
<ng-template clrPageTitle>{{
'ROBOT_ACCOUNT.SELECT_PROJECT_PERMISSIONS' | translate
}}</ng-template>
<inline-alert class="modal-title"></inline-alert>
<form class="clr-form clr-form-horizontal pb-0 pt-0">
<section class="form-block">
<div class="clr-form-control mt-1">
<label class="clr-control-label">{{
'SYSTEM_ROBOT.COVER_ALL' | translate
}}</label>
<div class="clr-control-container padding-top-3 flex">
<div class="clr-control-container">
<clr-checkbox-wrapper>
<input
clrCheckbox
@ -247,7 +292,7 @@
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrPosition="bottom-right"
clrSize="lg"
*clrIfOpen>
<span>{{
@ -258,102 +303,26 @@
</clr-tooltip>
</label>
</clr-checkbox-wrapper>
<clr-dropdown
[style.visibility]="coverAll ? 'visible' : 'hidden'"
class="dropdown-per"
[clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{ getPermissions() }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
class="dropdown-menu"
[style.height.px]="230"
clrPosition="bottom-left"
*clrIfOpen>
<div>
<button
class="btn btn-link btn-sm select-all-for-dropdown ml-20px"
(click)="
selectAllOrUnselectAll(
defaultAccesses
)
">
<span
*ngIf="isSelectAll(defaultAccesses)"
>{{
'SYSTEM_ROBOT.SELECT_ALL'
| translate
}}</span
>
<span
*ngIf="
!isSelectAll(defaultAccesses)
"
>{{
'SYSTEM_ROBOT.UNSELECT_ALL'
| translate
}}</span
>
</button>
</div>
<div
clrDropdownItem
*ngFor="let item of defaultAccesses"
(click)="chooseAccess(item)">
<clr-icon
class="check"
shape="check"
[style.visibility]="
item.checked ? 'visible' : 'hidden'
"></clr-icon>
<span
>{{ i18nMap[item.action] | translate }}
{{
i18nMap[item.resource] | translate
}}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</div>
<div class="clr-form-control mt-0" *ngIf="coverAll">
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.NORMAL"
[(permissionsModel)]="permissionForCoverAll.access"
[candidatePermissions]="robotMetadata?.project">
</robot-permissions-panel>
</div>
<div
class="clr-form-control mt-1"
*ngIf="showPage3 && !coverAll">
<app-list-all-projects
[initDataForEdit]="
isEditMode ? systemRobot.permissions : null
"
[robotMetadata]="robotMetadata"
class="all-projects"></app-list-all-projects>
</div>
</section>
<div
class="clr-form-control"
[class.clr-form-control-disabled]="coverAll">
<app-list-all-projects
[coverAll]="coverAll"
[class.disabled]="coverAll"
class="all-projects"></app-list-all-projects>
</div>
</form>
</div>
<div class="modal-footer">
<span
class="message"
[style.visibility]="coverAll ? 'visible' : 'hidden'"
>{{ 'SYSTEM_ROBOT.COVER_ALL_SUMMARY' | translate }}</span
>
<span>
<button
(click)="cancel()"
id="system-robot-cancel"
type="button"
class="btn btn-outline">
{{ 'BUTTON.CANCEL' | translate }}
</button>
<button
[disabled]="disabled() || checkNameOnGoing || isNameExisting"
[clrLoading]="saveBtnState"
(click)="save()"
id="system-robot-save"
type="button"
class="btn btn-primary">
<span *ngIf="isEditMode">{{ 'BUTTON.SAVE' | translate }}</span>
<span *ngIf="!isEditMode">{{ 'BUTTON.ADD' | translate }}</span>
</button>
</span>
</div>
</clr-modal>
</clr-wizard-page>
</clr-wizard>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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<Robot> = new EventEmitter<Robot>();
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<string> = new Subject<string>();
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;
}

View File

@ -12,7 +12,9 @@
<p class="mt-0">
{{ 'SYSTEM_ROBOT.PROJECTS_MODAL_SUMMARY' | translate }}
</p>
<clr-datagrid>
<clr-datagrid
(clrDgRefresh)="clrDgRefresh($event)"
[clrDgLoading]="loading">
<clr-dg-column>{{ 'PROJECT.NAME' | translate }}</clr-dg-column>
<clr-dg-column>{{
'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate
@ -29,36 +31,26 @@
>
</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{ p.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
clrPosition="bottom-left"
*clrIfOpen>
<div
clrDropdownItem
*ngFor="let item of p.access">
<span
>{{ i18nMap[item.action] | translate }}
{{
i18nMap[item.resource] | translate
}}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.MODAL"
[permissionsModel]="p.access"
[candidatePermissions]="p.access">
<button class="btn btn-link btn-sm m-0" modal>
{{ p.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon
class="icon"
size="12"
shape="caret down"></clr-icon>
</button>
</robot-permissions-panel>
</clr-dg-cell>
<clr-dg-cell>{{
getProject(p)?.creation_time | harborDatetime : 'short'
}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="10">
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize">
<clr-dg-page-size [clrPageSizeOptions]="[10, 20, 30]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>

View File

@ -14,13 +14,3 @@
font-size: 16px;
opacity: 0.9;
}
.permissions {
height: 16px;
display: flex;
align-items: center;
}
.datagrid-host {
position: inherit;
}

View File

@ -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;
}

View File

@ -19,7 +19,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar>
<button
[disabled]="loadingData"
[disabled]="loadingData || loadingMetadata"
[clrLoading]="addBtnState"
class="btn btn-secondary"
(click)="openNewRobotModal(false)">
@ -136,6 +136,7 @@
<clr-dg-column>{{
'ROBOT_ACCOUNT.ENABLED_STATE' | translate
}}</clr-dg-column>
<clr-dg-column>System Permissions</clr-dg-column>
<clr-dg-column>{{
'SYSTEM_ROBOT.PROJECTS' | translate
}}</clr-dg-column>
@ -165,6 +166,25 @@
size="16"
class="color-red red-position"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="!getSystemAccess(r)?.length">
{{ 'SCHEDULE.NONE' | translate }}
</span>
<robot-permissions-panel
*ngIf="getSystemAccess(r)?.length"
[mode]="PermissionSelectPanelModes.MODAL"
[permissionsModel]="getSystemAccess(r)"
[candidatePermissions]="getSystemAccess(r)">
<button class="btn btn-link btn-sm m-0 p-0" modal>
{{ getSystemAccess(r)?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon
class="icon"
size="12"
shape="caret down"></clr-icon>
</button>
</robot-permissions-panel>
</clr-dg-cell>
<clr-dg-cell>
<div
class="all-projects"
@ -172,29 +192,20 @@
<span>{{
'SYSTEM_ROBOT.ALL_PROJECTS' | translate
}}</span>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.MODAL"
[permissionsModel]="r.permissionScope?.access"
[candidatePermissions]="r.permissionScope?.access">
<button class="btn btn-link btn-sm m-0" modal>
{{ r.permissionScope?.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
<clr-icon
class="icon"
size="12"
shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
clrPosition="bottom-left"
*clrIfOpen>
<div
clrDropdownItem
*ngFor="
let item of r.permissionScope?.access
">
<span
>{{ i18nMap[item.action] | translate }}
{{
i18nMap[item.resource] | translate
}}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</robot-permissions-panel>
</div>
<span
*ngIf="
@ -208,7 +219,7 @@
{{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }}
</a>
<span *ngIf="!getProjects(r)?.length">
0 {{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }}
{{ 'SCHEDULE.NONE' | translate }}
</span>
</span>
</clr-dg-cell>
@ -243,6 +254,8 @@
</clr-datagrid>
</div>
</div>
<new-robot (addSuccess)="addSuccess($event)"></new-robot>
<new-robot
(addSuccess)="addSuccess($event)"
[robotMetadata]="robotMetadata"></new-robot>
<view-token (refreshSuccess)="refresh()"></view-token>
<app-projects-modal></app-projects-modal>

View File

@ -33,3 +33,9 @@
align-items: center;
height: 16px;
}
.icon {
margin-top: 3px;
}

View File

@ -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<Project[]>[] = [];
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;
}

View File

@ -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<FrontAccess>;
}>;
}
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 [];
}

View File

@ -1,25 +1,45 @@
<clr-modal
clrModalSize="md"
[(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true"
[clrModalClosable]="true">
<h3 *ngIf="!isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT' | translate }}
</h3>
<h3 *ngIf="isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT' | translate }}
</h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<clr-wizard
#wizard
[(clrWizardOpen)]="addRobotOpened"
[clrWizardSize]="'lg'"
(clrWizardOnCancel)="cancel()">
<clr-wizard-title
><h3 *ngIf="!isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT' | translate }}
</h3>
<h3 *ngIf="isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT' | translate }}
</h3>
<p *ngIf="!isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT_SUMMARY' | translate }}
</p>
<p *ngIf="isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT_SUMMARY' | translate }}
</p>
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1">
</p></clr-wizard-title
>
<clr-wizard-button [type]="'cancel'">{{
'BUTTON.CANCEL' | translate
}}</clr-wizard-button>
<clr-wizard-button [type]="'previous'">{{
'ROBOT_ACCOUNT.BACK' | translate
}}</clr-wizard-button>
<clr-wizard-button [type]="'next'">{{
'ROBOT_ACCOUNT.NEXT' | translate
}}</clr-wizard-button>
<clr-wizard-button [clrLoading]="saveBtnState" [type]="'finish'">{{
'ROBOT_ACCOUNT.FINISH' | translate
}}</clr-wizard-button>
<clr-wizard-page
[clrWizardPageNextDisabled]="
!robotBasicForm.valid || checkNameOnGoing || isNameExisting
">
<ng-template clrPageTitle>{{
'ROBOT_ACCOUNT.BASIC_INFO' | translate
}}</ng-template>
<form
#robotBasicForm="ngForm"
class="clr-form clr-form-horizontal mt-1">
<section class="form-block">
<!-- name -->
<div class="clr-form-control">
<label for="name" class="clr-control-label required"
>{{ 'P2P_PROVIDER.NAME' | translate }}
@ -47,11 +67,11 @@
">
<div class="clr-input-wrapper">
<input
class="clr-input"
class="clr-input input-width"
[disabled]="loading || isEditMode"
type="text"
id="name"
[(ngModel)]="systemRobot.name"
[(ngModel)]="robot.name"
required
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
maxLengthExt="255"
@ -95,8 +115,17 @@
</clr-control-error>
</div>
</div>
<!-- expiration -->
<div class="clr-form-control">
<clr-textarea-container class="mt-2">
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea
class="input-width"
clrTextarea
type="text"
id="description"
name="description"
[(ngModel)]="robot.description"></textarea>
</clr-textarea-container>
<div class="clr-form-control mt-2">
<label class="clr-control-label required"
>{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }}
<clr-tooltip>
@ -144,15 +173,15 @@
</option>
</select>
</div>
<div class="clr-input-wrapper">
<div class="clr-input-wrapper expiration">
<input
(input)="inputExpiration()"
class="clr-input expiration-width"
class="clr-input"
name="expiration"
type="text"
#expiration="ngModel"
autocomplete="off"
[(ngModel)]="systemRobot.duration"
[(ngModel)]="robot.duration"
required
pattern="^[\-1-9]{1}[0-9]*$"
id="robotTokenExpiration"
@ -165,7 +194,7 @@
<clr-control-helper
*ngIf="
(isEditMode &&
systemRobot?.duration > 0 &&
robot?.duration > 0 &&
!(
(expiration.dirty ||
expiration.touched) &&
@ -201,104 +230,26 @@
</clr-control-error>
</div>
</div>
<!-- 3. description -->
<clr-textarea-container>
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea
class="mt-description"
clrTextarea
type="text"
id="description"
name="description"
[(ngModel)]="systemRobot.description"></textarea>
</clr-textarea-container>
<div class="clr-form-control">
<label class="clr-control-label mt-8px">{{
'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate
}}</label>
<div class="clr-control-container">
<clr-dropdown
class="dropdown-per"
[clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{ getPermissions() }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
class="overflow-y-scroll"
[style.height.px]="230"
clrPosition="bottom-left"
*clrIfOpen>
<div>
<button
class="btn btn-link btn-sm select-all-for-dropdown ml-20px"
(click)="
selectAllOrUnselectAll(
defaultAccesses
)
">
<span
*ngIf="isSelectAll(defaultAccesses)"
>{{
'SYSTEM_ROBOT.SELECT_ALL'
| translate
}}</span
>
<span
*ngIf="
!isSelectAll(defaultAccesses)
"
>{{
'SYSTEM_ROBOT.UNSELECT_ALL'
| translate
}}</span
>
</button>
</div>
<div
clrDropdownItem
*ngFor="let item of defaultAccesses"
(click)="chooseAccess(item)">
<clr-icon
class="check"
shape="check"
[style.visibility]="
item.checked ? 'visible' : 'hidden'
"></clr-icon>
<span
>{{ i18nMap[item.action] | translate }}
{{
i18nMap[item.resource] | translate
}}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<span>
<button
(click)="cancel()"
id="system-robot-cancel"
type="button"
class="btn btn-outline">
{{ 'BUTTON.CANCEL' | translate }}
</button>
<button
[disabled]="disabled() || checkNameOnGoing || isNameExisting"
[clrLoading]="saveBtnState"
(click)="save()"
id="system-robot-save"
type="button"
class="btn btn-primary">
<span *ngIf="isEditMode">{{ 'BUTTON.SAVE' | translate }}</span>
<span *ngIf="!isEditMode">{{ 'BUTTON.ADD' | translate }}</span>
</button>
</span>
</div>
</clr-modal>
</clr-wizard-page>
<clr-wizard-page
(clrWizardPageOnCommit)="save()"
[clrWizardPagePreventDefaultNext]="true"
[clrWizardPageNextDisabled]="disabled()">
<ng-template clrPageTitle>{{
'ROBOT_ACCOUNT.SELECT_PERMISSIONS' | translate
}}</ng-template>
<inline-alert class="modal-title"></inline-alert>
<form class="clr-form clr-form-horizontal mt-1">
<section class="form-block">
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.NORMAL"
[(permissionsModel)]="robot.permissions[0].access"
[candidatePermissions]="robotMetadata?.project">
</robot-permissions-panel>
</section>
</form>
</clr-wizard-page>
</clr-wizard>

View File

@ -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;
}

View File

@ -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<Robot> = new EventEmitter<Robot>();
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<string> = new Subject<string>();
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;
}

View File

@ -18,7 +18,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar>
<button
[disabled]="!hasRobotCreatePermission"
[disabled]="!hasRobotCreatePermission || loadingMetadata"
[clrLoading]="addBtnState"
class="btn btn-secondary"
(click)="openNewRobotModal(false)">
@ -178,33 +178,19 @@
class="color-red red-position"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown
[clrCloseMenuOnItemClick]="false"
*ngIf="r.permissions[0]?.access?.length">
<button class="btn btn-link" clrDropdownTrigger>
{{ r.permissions[0]?.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu
clrPosition="bottom-left"
*clrIfOpen>
<div
clrDropdownItem
*ngFor="
let item of r.permissions[0]?.access
">
<span
>{{ i18nMap[item.action] | translate }}
{{
i18nMap[item.resource] | translate
}}</span
>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<robot-permissions-panel
[mode]="PermissionSelectPanelModes.MODAL"
[permissionsModel]="r.permissions[0].access"
[candidatePermissions]="r.permissions[0].access">
<button class="btn btn-link btn-sm m-0 p-0" modal>
{{ r.permissions[0]?.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon
class="icon"
size="12"
shape="caret down"></clr-icon>
</button>
</robot-permissions-panel>
</clr-dg-cell>
<clr-dg-cell>{{
r.creation_time | harborDatetime : 'short'
@ -238,6 +224,7 @@
</div>
</div>
<add-robot
[robotMetadata]="robotMetadata"
[projectId]="projectId"
[projectName]="projectName"
(addSuccess)="addSuccess($event)"></add-robot>

View File

@ -27,12 +27,7 @@
}
}
.permissions {
height: 16px;
display: flex;
align-items: center;
}
.datagrid-host {
position: inherit;
.icon {
margin-top: 3px;
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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';

View File

@ -0,0 +1,136 @@
<ng-container *ngIf="mode === PermissionSelectPanelModes.MODAL">
<div (click)="modalOpen = true">
<ng-content select="[modal]"></ng-content>
</div>
<clr-modal
[clrModalSize]="'lg'"
[(clrModalOpen)]="modalOpen"
[clrModalStaticBackdrop]="false">
<div class="modal-body">
<table class="table table-compact mt-0">
<thead>
<tr>
<th class="left">
{{ 'AUDIT_LOG.RESOURCE' | translate }}
</th>
<th class="left" *ngFor="let item of candidateActions">
{{ convertKey(item) | translate }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let resource of candidateResources">
<td class="left td">
{{ convertKey(resource) | translate }}
</td>
<td class="td" *ngFor="let action of candidateActions">
<input
*ngIf="isCandidate(resource, action)"
type="checkbox"
clrCheckbox
[disabled]="true"
[ngModel]="getCheckBoxValue(resource, action)"
(ngModelChange)="
setCheckBoxValue(resource, action, $event)
" />
</td>
</tr>
</tbody>
</table>
</div>
</clr-modal>
</ng-container>
<ng-container *ngIf="mode === PermissionSelectPanelModes.DROPDOWN">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<span #dropdown class="trigger" clrDropdownTrigger>
<ng-content></ng-content>
</span>
<clr-dropdown-menu
class="dropdown-menu p-1"
clrPosition="{{ dropdownPosition }}"
*clrIfOpen>
<div>
<button
class="btn btn-link btn-sm select-all-for-dropdown p-0"
(click)="selectAllOrUnselectAll()">
<span *ngIf="!isAllSelected()">{{
'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span>
<span *ngIf="isAllSelected()">{{
'SYSTEM_ROBOT.UNSELECT_ALL' | translate
}}</span>
</button>
</div>
<table class="table table-compact mt-0">
<thead>
<tr>
<th class="left">
{{ 'AUDIT_LOG.RESOURCE' | translate }}
</th>
<th class="left" *ngFor="let item of candidateActions">
{{ convertKey(item) | translate }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let resource of candidateResources">
<td class="left td">
{{ convertKey(resource) | translate }}
</td>
<td class="td" *ngFor="let action of candidateActions">
<input
*ngIf="isCandidate(resource, action)"
type="checkbox"
clrCheckbox
[ngModel]="getCheckBoxValue(resource, action)"
(ngModelChange)="
setCheckBoxValue(resource, action, $event)
" />
</td>
</tr>
</tbody>
</table>
</clr-dropdown-menu>
</clr-dropdown>
</ng-container>
<ng-container *ngIf="mode === PermissionSelectPanelModes.NORMAL">
<div>
<button
class="btn btn-outline btn-sm"
(click)="selectAllOrUnselectAll()">
<span *ngIf="!isAllSelected()">{{
'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span>
<span *ngIf="isAllSelected()">{{
'SYSTEM_ROBOT.UNSELECT_ALL' | translate
}}</span>
</button>
</div>
<table class="table table-compact mt-0">
<thead>
<tr>
<th class="left">{{ 'AUDIT_LOG.RESOURCE' | translate }}</th>
<th class="left" *ngFor="let item of candidateActions">
{{ convertKey(item) | translate }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let resource of candidateResources">
<td class="left td">{{ convertKey(resource) | translate }}</td>
<td class="td" *ngFor="let action of candidateActions">
<input
*ngIf="isCandidate(resource, action)"
type="checkbox"
clrCheckbox
[ngModel]="getCheckBoxValue(resource, action)"
(ngModelChange)="
setCheckBoxValue(resource, action, $event)
" />
</td>
</tr>
</tbody>
</table>
</ng-container>

View File

@ -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;
}

View File

@ -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<TestHostComponent>;
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: `
<ng-container *ngIf="mode === PermissionSelectPanelModes.MODAL">
<robot-permissions-panel [mode]="mode">
<div>modal</div>
</robot-permissions-panel>
</ng-container>
<ng-container *ngIf="mode === PermissionSelectPanelModes.DROPDOWN">
<robot-permissions-panel [mode]="mode">
<div>dropDown</div>
</robot-permissions-panel>
</ng-container>
<ng-container *ngIf="mode === PermissionSelectPanelModes.NORMAL">
<robot-permissions-panel [mode]="mode"> </robot-permissions-panel>
</ng-container>
`,
})
class TestHostComponent {
@ViewChild(RobotPermissionsPanelComponent)
robotPermissionsPanelComponent: RobotPermissionsPanelComponent;
mode = PermissionSelectPanelModes.NORMAL;
protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes;
}

View File

@ -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<Access[]>();
@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,
}

View File

@ -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 },

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "编辑",

View File

@ -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": "編輯",