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-datagrid
<clr-dg-action-bar> [clrDgPreserveSelection]="true"
<clr-dropdown [clrCloseMenuOnItemClick]="false"> [(clrDgSelected)]="selectedRow"
<button [clrDgLoading]="loadingData"
[disabled]="coverAll" class="datagrid-compact">
class="btn btn-secondary btn-sm" <clr-dg-action-bar class="action-bar">
clrDropdownTrigger> <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 }} {{ 'SYSTEM_ROBOT.RESET_PERMISSION' | translate }}
<clr-icon shape="caret down"></clr-icon> <clr-icon size="12" shape="caret down"></clr-icon>
</button> </button>
<clr-dropdown-menu </robot-permissions-panel>
[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>
<button <button
(click)="selectAllOrUnselectAll()" (click)="selectAllOrUnselectAll()"
[disabled]="coverAll" class="btn btn-secondary btn-sm m-0 ml-1">
class="btn btn-secondary btn-sm ml-1">
<span *ngIf="showSelectAll">{{ <span *ngIf="showSelectAll">{{
'SYSTEM_ROBOT.SELECT_ALL' | translate 'SYSTEM_ROBOT.SELECT_ALL' | translate
}}</span> }}</span>
@ -67,9 +36,7 @@
<clr-dg-column>{{ <clr-dg-column>{{
'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate 'SYSTEM_ROBOT.PERMISSION_COLUMN' | translate
}}</clr-dg-column> }}</clr-dg-column>
<clr-dg-row <clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
*clrDgItems="let p of projects; let projectIndex = index"
[clrDgItem]="p">
<clr-dg-cell> <clr-dg-cell>
<a href="javascript:void(0)" [routerLink]="getLink(p.project_id)">{{ <a href="javascript:void(0)" [routerLink]="getLink(p.project_id)">{{
p.name p.name
@ -79,62 +46,17 @@
p.creation_time | harborDatetime : 'short' p.creation_time | harborDatetime : 'short'
}}</clr-dg-cell> }}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<div class="permissions"> <robot-permissions-panel
<clr-dropdown [clrCloseMenuOnItemClick]="false"> [usedInDatagrid]="true"
<button [mode]="PermissionSelectPanelModes.DROPDOWN"
[disabled]="coverAll" [(permissionsModel)]="selectedProjectPermissionMap[p.name]"
class="btn btn-link" [candidatePermissions]="robotMetadata?.project">
clrDropdownTrigger> <button class="btn btn-link btn-sm m-0 p-0">
{{ getPermissions(p.permissions[0].access) }} {{ selectedProjectPermissionMap[p.name]?.length || 0 }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }} {{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon> <clr-icon shape="caret down"></clr-icon>
</button> </button>
<clr-dropdown-menu </robot-permissions-panel>
[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>
</clr-dg-cell> </clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
@ -142,7 +64,7 @@
#pagination #pagination
[(clrDgPage)]="currentPage" [(clrDgPage)]="currentPage"
[clrDgPageSize]="pageSize"> [clrDgPageSize]="pageSize">
<clr-dg-page-size [clrPageSizeOptions]="[5, 15, 25]">{{ <clr-dg-page-size [clrPageSizeOptions]="[5, 10]">{{
'PAGINATION.PAGE_SIZE' | translate 'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size> }}</clr-dg-page-size>
<span <span

View File

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

View File

@ -1,7 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ListAllProjectsComponent } from './list-all-projects.component'; 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 { Project } from '../../../../../../ng-swagger-gen/models/project';
import { SharedTestingModule } from '../../../../shared/shared.module'; import { SharedTestingModule } from '../../../../shared/shared.module';
@ -30,9 +28,6 @@ describe('ListAllProjectsComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ListAllProjectsComponent); fixture = TestBed.createComponent(ListAllProjectsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.defaultAccesses = clone(INITIAL_ACCESSES);
component.cachedAllProjects = [project1, project2, project3];
component.init(false);
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -40,6 +35,7 @@ describe('ListAllProjectsComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should render list', async () => { it('should render list', async () => {
component.projects = [project1, project2, project3];
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row'); 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 { Project } from '../../../../../../ng-swagger-gen/models/project';
import { clone, CustomComparator } from '../../../../shared/units/utils'; import { clone, CustomComparator } from '../../../../shared/units/utils';
import { ClrDatagridComparatorInterface } from '@clr/angular'; import { ClrDatagridComparatorInterface } from '@clr/angular';
import { Router } from '@angular/router'; import { FrontAccess } from '../system-robot-util';
import { import { Access } from '../../../../../../ng-swagger-gen/models/access';
ACTION_RESOURCE_I18N_MAP, import { forkJoin, Observable } from 'rxjs';
FrontAccess, import { finalize } from 'rxjs/operators';
FrontProjectForAdd, import { ProjectService } from '../../../../../../ng-swagger-gen/services/project.service';
INITIAL_ACCESSES, import { PermissionSelectPanelModes } from '../../../../shared/components/robot-permissions-panel/robot-permissions-panel.component';
PermissionsKinds,
} from '../system-robot-util';
import { RobotPermission } from '../../../../../../ng-swagger-gen/models/robot-permission'; 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({ @Component({
selector: 'app-list-all-projects', selector: 'app-list-all-projects',
templateUrl: './list-all-projects.component.html', templateUrl: './list-all-projects.component.html',
styleUrls: ['./list-all-projects.component.scss'], styleUrls: ['./list-all-projects.component.scss'],
}) })
export class ListAllProjectsComponent { export class ListAllProjectsComponent implements OnInit {
cachedAllProjects: Project[]; selectedRow: Project[] = [];
i18nMap = ACTION_RESOURCE_I18N_MAP; selectedRowForEdit: Project[] = [];
permissionsForAdd: RobotPermission[] = [];
selectedRow: FrontProjectForAdd[] = [];
timeComparator: ClrDatagridComparatorInterface<Project> = timeComparator: ClrDatagridComparatorInterface<Project> =
new CustomComparator<Project>('creation_time', 'date'); new CustomComparator<Project>('creation_time', 'date');
projects: FrontProjectForAdd[] = []; projects: Project[] = [];
pageSize: number = 5; pageSize: number = 5;
currentPage: number = 1; currentPage: number = 1;
defaultAccesses: FrontAccess[] = [];
@Input()
coverAll: boolean = false;
showSelectAll: boolean = true; showSelectAll: boolean = true;
myNameFilterValue: string; myNameFilterValue: string;
constructor(private router: Router) {}
init(isEdit: boolean) { @Input()
this.pageSize = 5; robotMetadata: Permissions;
this.currentPage = 1;
this.showSelectAll = true; initialAccess: Access[] = [];
this.myNameFilterValue = null; selectedProjectPermissionMap: { [key: string]: Access[] } = {};
if (isEdit) { selectedProjectPermissionMapForEdit: { [key: string]: Access[] } = {};
this.defaultAccesses = clone(INITIAL_ACCESSES); loadingData: boolean = true;
this.defaultAccesses.forEach(item => (item.checked = false)); @Input()
} else { initDataForEdit: RobotPermission[];
this.defaultAccesses = clone(INITIAL_ACCESSES);
} constructor(
if (this.cachedAllProjects && this.cachedAllProjects.length) { private projectService: ProjectService,
this.projects = clone(this.cachedAllProjects); private cdf: ChangeDetectorRef
this.resetAccess(this.defaultAccesses); ) {}
} else {
this.projects = []; ngOnInit() {
} this.loadDataFromBackend();
} }
resetAccess(accesses: FrontAccess[]) { resetAccess(accesses: FrontAccess[]) {
if (this.projects && this.projects.length) { if (this.projects && this.projects.length) {
this.projects.forEach(item => { this.projects.forEach(item => {
item.permissions = [ this.selectedProjectPermissionMap[item.name] = clone(accesses);
{
kind: PermissionsKinds.PROJECT,
namespace: item.name,
access: 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 { getLink(proId: number): string {
return `/harbor/projects/${proId}`; return `/harbor/projects/${proId}`;
} }
@ -108,26 +82,87 @@ export class ListAllProjectsComponent {
} }
this.showSelectAll = !this.showSelectAll; this.showSelectAll = !this.showSelectAll;
} }
isSelectAll(permissions: FrontAccess[]): boolean {
if (permissions?.length) { loadDataFromBackend() {
return ( this.loadingData = true;
permissions.filter(item => item.checked).length < this.projectService
permissions.length / 2 .listProjectsResponse({
); withDetail: false,
} page: 1,
return false; 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[]) { initDataForEditMode() {
if (permissions?.length) { if (this.initDataForEdit?.length) {
if (this.isSelectAll(permissions)) { this.selectedRow = [];
permissions.forEach(item => { this.projects.forEach((pro, index) => {
item.checked = true; this.initDataForEdit.forEach(item => {
if (pro.name === item.namespace) {
item.access.forEach(acc => {
this.selectedProjectPermissionMap[pro.name] =
item.access;
});
this.selectedRow.push(pro);
}
}); });
} else { this.selectedProjectPermissionMapForEdit = clone(
permissions.forEach(item => { this.selectedProjectPermissionMap
item.checked = false; );
}); this.selectedRowForEdit = clone(this.selectedRow);
} });
} }
} }
protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes;
} }

View File

@ -1,25 +1,42 @@
<clr-modal <clr-wizard
clrModalSize="lg" #wizard
[(clrModalOpen)]="addRobotOpened" [(clrWizardOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true" (clrWizardOnCancel)="cancel()">
[clrModalClosable]="true"> <clr-wizard-title>
<h3 *ngIf="!isEditMode" class="modal-title"> <h3 *ngIf="!isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.CREATE_ROBOT' | translate }} {{ 'SYSTEM_ROBOT.CREATE_ROBOT' | translate }}
</h3> </h3>
<h3 *ngIf="isEditMode" class="modal-title"> <h3 *ngIf="isEditMode" class="modal-title">
{{ 'SYSTEM_ROBOT.EDIT_ROBOT' | translate }} {{ 'SYSTEM_ROBOT.EDIT_ROBOT' | translate }}
</h3> </h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<p *ngIf="!isEditMode" class="mt-0"> <p *ngIf="!isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.CREATE_ROBOT_SUMMARY' | translate }} {{ 'SYSTEM_ROBOT.CREATE_ROBOT_SUMMARY' | translate }}
</p> </p>
<p *ngIf="isEditMode" class="mt-0"> <p *ngIf="isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.EDIT_ROBOT_SUMMARY' | translate }} {{ 'SYSTEM_ROBOT.EDIT_ROBOT_SUMMARY' | translate }}
</p> </p></clr-wizard-title
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1"> >
<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"> <section class="form-block">
<!-- name -->
<div class="clr-form-control"> <div class="clr-form-control">
<label for="name" class="clr-control-label required" <label for="name" class="clr-control-label required"
>{{ 'P2P_PROVIDER.NAME' | translate }} >{{ 'P2P_PROVIDER.NAME' | translate }}
@ -95,8 +112,17 @@
</clr-control-error> </clr-control-error>
</div> </div>
</div> </div>
<!-- expiration --> <clr-textarea-container class="mt-2">
<div class="clr-form-control"> <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" <label class="clr-control-label required"
>{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }} >{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }}
<clr-tooltip> <clr-tooltip>
@ -216,22 +242,41 @@
</clr-control-helper> </clr-control-helper>
</div> </div>
</div> </div>
<!-- 3. description --> </section>
<clr-textarea-container class="mt-description"> </form>
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label> </clr-wizard-page>
<textarea <clr-wizard-page class="pb-0">
class="input-width" <ng-template clrPageTitle>{{
clrTextarea 'ROBOT_ACCOUNT.SELECT_SYSTEM_PERMISSIONS' | translate
type="text" }}</ng-template>
id="description" <form class="clr-form clr-form-horizontal">
name="description" <section class="form-block">
[(ngModel)]="systemRobot.description"></textarea> <robot-permissions-panel
</clr-textarea-container> [mode]="PermissionSelectPanelModes.NORMAL"
<div class="clr-form-control"> [(permissionsModel)]="permissionForSystem.access"
<label class="clr-control-label mt-8px">{{ [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 'SYSTEM_ROBOT.COVER_ALL' | translate
}}</label> }}</label>
<div class="clr-control-container padding-top-3 flex"> <div class="clr-control-container">
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input <input
clrCheckbox clrCheckbox
@ -247,7 +292,7 @@
shape="info-circle" shape="info-circle"
size="24"></clr-icon> size="24"></clr-icon>
<clr-tooltip-content <clr-tooltip-content
clrPosition="top-right" clrPosition="bottom-right"
clrSize="lg" clrSize="lg"
*clrIfOpen> *clrIfOpen>
<span>{{ <span>{{
@ -258,102 +303,26 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
</clr-checkbox-wrapper> </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> </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> </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> </form>
</div> </clr-wizard-page>
<div class="modal-footer"> </clr-wizard>
<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>

View File

@ -14,11 +14,6 @@
margin: 0; margin: 0;
} }
.permission{
padding-top: 0.1rem;
color: #000;
}
.padding-left-120{ .padding-left-120{
padding-left: 126px; padding-left: 126px;
} }
@ -35,10 +30,6 @@
width: 265px; width: 265px;
} }
.mt-description {
margin-top: 2.5rem;
}
.width-table { .width-table {
width: 388px; width: 388px;
} }
@ -74,33 +65,16 @@
width: 194px; width: 194px;
} }
.check {
margin-right: 5px;
color: green;
}
.dropdown-per {
margin-top: -1px;
}
.mt-8px {
margin-top: 8px !important;
}
/* stylelint-disable */ /* stylelint-disable */
.showWarning { .showWarning {
color: #b3a000; color: #b3a000;
} }
.dropdown-item {
min-height: 20px;
display: flex;
align-items: center;
}
.dropdown-menu {
overflow-y: auto;
}
.ml-20px { .ml-20px {
margin-left: 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 { ClarityModule } from '@clr/angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Robot } from '../../../../../../ng-swagger-gen/models/robot'; import { Robot } from '../../../../../../ng-swagger-gen/models/robot';
import { import { Action, PermissionsKinds, Resource } from '../system-robot-util';
Action,
INITIAL_ACCESSES,
PermissionsKinds,
Resource,
} from '../system-robot-util';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { OperationService } from '../../../../shared/components/operation/operation.service'; import { OperationService } from '../../../../shared/components/operation/operation.service';
import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.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 { ConfigurationService } from '../../../../services/config.service';
import { Configuration } from '../../config/config'; import { Configuration } from '../../config/config';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { clone } from '../../../../shared/units/utils';
describe('NewRobotComponent', () => { describe('NewRobotComponent', () => {
let component: NewRobotComponent; let component: NewRobotComponent;
@ -103,7 +97,6 @@ describe('NewRobotComponent', () => {
fixture.autoDetectChanges(); fixture.autoDetectChanges();
component.isEditMode = false; component.isEditMode = false;
component.addRobotOpened = true; component.addRobotOpened = true;
component.defaultAccesses = clone(INITIAL_ACCESSES);
await fixture.whenStable(); await fixture.whenStable();
const nameInput = fixture.nativeElement.querySelector('#name'); const nameInput = fixture.nativeElement.querySelector('#name');
nameInput.value = ''; nameInput.value = '';
@ -117,28 +110,9 @@ describe('NewRobotComponent', () => {
fixture.autoDetectChanges(); fixture.autoDetectChanges();
component.isEditMode = true; component.isEditMode = true;
component.addRobotOpened = true; component.addRobotOpened = true;
component.defaultAccesses = clone(INITIAL_ACCESSES);
component.systemRobot = robot1; component.systemRobot = robot1;
await fixture.whenStable(); await fixture.whenStable();
const nameInput = fixture.nativeElement.querySelector('#name'); const nameInput = fixture.nativeElement.querySelector('#name');
expect(nameInput.value).toEqual('robot1'); 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 { import {
Component, Component,
EventEmitter, EventEmitter,
Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
@ -17,19 +18,22 @@ import {
finalize, finalize,
switchMap, switchMap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Access } from '../../../../../../ng-swagger-gen/models/access';
import { import {
ACTION_RESOURCE_I18N_MAP,
ExpirationType, ExpirationType,
FrontAccess, getSystemAccess,
INITIAL_ACCESSES,
NAMESPACE_ALL_PROJECTS, NAMESPACE_ALL_PROJECTS,
NAMESPACE_SYSTEM,
NEW_EMPTY_ROBOT,
onlyHasPushPermission, onlyHasPushPermission,
PermissionsKinds, PermissionsKinds,
} from '../system-robot-util'; } 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 { 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 { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { import {
@ -40,6 +44,9 @@ import {
import { OperationService } from '../../../../shared/components/operation/operation.service'; import { OperationService } from '../../../../shared/components/operation/operation.service';
import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component'; import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component';
import { errorHandler } from '../../../../shared/units/shared.utils'; 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; 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'], styleUrls: ['./new-robot.component.scss'],
}) })
export class NewRobotComponent implements OnInit, OnDestroy { export class NewRobotComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
isEditMode: boolean = false; isEditMode: boolean = false;
originalRobotForEdit: Robot; originalRobotForEdit: Robot;
@Output() @Output()
addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>(); addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>();
addRobotOpened: boolean = false; addRobotOpened: boolean = false;
systemRobot: Robot = {}; systemRobot: Robot = clone(NEW_EMPTY_ROBOT);
expirationType: string = ExpirationType.DAYS; expirationType: string = ExpirationType.DAYS;
systemExpirationDays: number; systemExpirationDays: number;
coverAll: boolean = false; coverAll: boolean = false;
@ -65,8 +71,6 @@ export class NewRobotComponent implements OnInit, OnDestroy {
loading: boolean = false; loading: boolean = false;
checkNameOnGoing: boolean = false; checkNameOnGoing: boolean = false;
loadingSystemConfig: boolean = false; loadingSystemConfig: boolean = false;
defaultAccesses: FrontAccess[] = [];
defaultAccessesForEdit: FrontAccess[] = [];
@ViewChild(ListAllProjectsComponent) @ViewChild(ListAllProjectsComponent)
listAllProjectsComponent: ListAllProjectsComponent; listAllProjectsComponent: ListAllProjectsComponent;
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
@ -75,6 +79,27 @@ export class NewRobotComponent implements OnInit, OnDestroy {
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
private _nameSubject: Subject<string> = new Subject<string>(); private _nameSubject: Subject<string> = new Subject<string>();
private _nameSubscription: Subscription; 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( constructor(
private configService: ConfigurationService, private configService: ConfigurationService,
private robotService: RobotService, private robotService: RobotService,
@ -166,36 +191,42 @@ export class NewRobotComponent implements OnInit, OnDestroy {
this._nameSubject.next(this.systemRobot.name); this._nameSubject.next(this.systemRobot.name);
} }
cancel() { cancel() {
this.wizard.reset();
this.reset();
this.addRobotOpened = false; 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() { reset() {
this.open(false); this.open(false);
this.defaultAccesses = clone(INITIAL_ACCESSES); this.systemRobot = clone(NEW_EMPTY_ROBOT);
this.listAllProjectsComponent.init(false); this.permissionForCoverAll = {
this.listAllProjectsComponent.selectedRow = []; access: [],
this.systemRobot = {}; 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.robotForm.reset();
this.expirationType = ExpirationType.DAYS; this.expirationType = ExpirationType.DAYS;
this.getSystemRobotExpiration(); this.getSystemRobotExpiration();
} }
resetForEdit(robot: Robot) { resetForEdit(robot: Robot) {
this.open(true); this.open(true);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.defaultAccesses.forEach(item => (item.checked = false));
this.originalRobotForEdit = clone(robot); 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 = this.expirationType =
robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS; robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS;
if (robot && robot.permissions && robot.permissions.length) { if (robot && robot.permissions && robot.permissions.length) {
@ -206,67 +237,17 @@ export class NewRobotComponent implements OnInit, OnDestroy {
item.namespace === NAMESPACE_ALL_PROJECTS item.namespace === NAMESPACE_ALL_PROJECTS
) { ) {
this.coverAll = true; this.coverAll = true;
if (item && item.access) { this.permissionForCoverAll = clone(item);
item.access.forEach(item2 => { this.permissionForCoverAllForEdit = clone(item);
this.defaultAccesses.forEach(item3 => {
if (
item3.resource === item2.resource &&
item3.action === item2.action
) {
item3.checked = true;
}
});
});
this.defaultAccessesForEdit = clone(
this.defaultAccesses
);
}
} }
}); });
} }
if (!this.coverAll) {
this.defaultAccesses.forEach(item => (item.checked = true));
}
this.robotForm.reset({ this.robotForm.reset({
name: this.systemRobot.name, name: this.systemRobot.name,
expiration: this.systemRobot.duration, expiration: this.systemRobot.duration,
description: this.systemRobot.description, description: this.systemRobot.description,
coverAll: this.coverAll,
}); });
this.coverAllForEdit = 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) { open(isEditMode: boolean) {
this.isNameExisting = false; this.isNameExisting = false;
@ -286,46 +267,32 @@ export class NewRobotComponent implements OnInit, OnDestroy {
return false; return false;
} }
if (this.coverAll) { if (this.coverAll) {
let flag = false; if (!this.permissionForCoverAll.access?.length) {
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) {
return false; 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; return true;
} }
@ -344,79 +311,61 @@ export class NewRobotComponent implements OnInit, OnDestroy {
) { ) {
return true; 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 ( if (
this.systemRobot.permissions.length !== !isSameObject(
this.listAllProjectsComponent.selectedRow.length this.permissionForSystem,
this.permissionForSystemForEdit
)
) { ) {
return true; return true;
} }
const map = {}; if (this.coverAll !== this.coverAllForEdit) {
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) {
return true; return true;
} }
let flag1 = true; if (this.coverAll) {
this.systemRobot.permissions.forEach(item => { if (
if (!map[item.namespace]) { !isSameObject(
flag1 = false; 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() { save() {
const robot: Robot = clone(this.systemRobot); const robot: Robot = clone(this.systemRobot);
@ -424,37 +373,27 @@ export class NewRobotComponent implements OnInit, OnDestroy {
robot.level = PermissionsKinds.SYSTEM; robot.level = PermissionsKinds.SYSTEM;
robot.duration = +this.systemRobot.duration; robot.duration = +this.systemRobot.duration;
robot.permissions = []; robot.permissions = [];
if (this.permissionForSystem?.access?.length) {
robot.permissions.push(this.permissionForSystem);
}
if (this.coverAll) { if (this.coverAll) {
const access: Access[] = []; if (this.permissionForCoverAll?.access?.length) {
this.defaultAccesses.forEach(item => { robot.permissions.push(this.permissionForCoverAll);
if (item.checked) { }
access.push({
resource: item.resource,
action: item.action,
});
}
});
robot.permissions.push({
kind: PermissionsKinds.PROJECT,
namespace: NAMESPACE_ALL_PROJECTS,
access: access,
});
} else { } else {
this.listAllProjectsComponent.selectedRow.forEach(item => { this.listAllProjectsComponent.selectedRow.forEach(item => {
const access: Access[] = []; if (
item.permissions[0].access.forEach(item2 => { this.listAllProjectsComponent.selectedProjectPermissionMap[
if (item2.checked) { item.name
access.push({ ]?.length
resource: item2.resource, ) {
action: item2.action, robot.permissions.push({
}); kind: PermissionsKinds.PROJECT,
} namespace: item.name,
}); access: this.listAllProjectsComponent
robot.permissions.push({ .selectedProjectPermissionMap[item.name],
kind: PermissionsKinds.PROJECT, });
namespace: item.name, }
access: access,
});
}); });
} }
// Push permission must work with pull permission // Push permission must work with pull permission
@ -486,7 +425,7 @@ export class NewRobotComponent implements OnInit, OnDestroy {
res => { res => {
this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(null); this.addSuccess.emit(null);
this.addRobotOpened = false; this.cancel();
operateChanges(opeMessage, OperationState.success); operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess( this.msgHandler.showSuccess(
'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY' 'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY'
@ -517,7 +456,7 @@ export class NewRobotComponent implements OnInit, OnDestroy {
res => { res => {
this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(res); this.addSuccess.emit(res);
this.addRobotOpened = false; this.cancel();
operateChanges(opeMessage, OperationState.success); operateChanges(opeMessage, OperationState.success);
}, },
error => { 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 { calculateExpiresAt(): Date {
if ( if (
this.systemRobot && this.systemRobot &&
@ -559,26 +489,9 @@ export class NewRobotComponent implements OnInit, OnDestroy {
return new Date() >= this.calculateExpiresAt(); return new Date() >= this.calculateExpiresAt();
} }
isSelectAll(permissions: FrontAccess[]): boolean { clrWizardPageOnLoad() {
if (permissions?.length) { this.showPage3 = true;
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

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

View File

@ -14,13 +14,3 @@
font-size: 16px; font-size: 16px;
opacity: 0.9; 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 { Component } from '@angular/core';
import { Project } from '../../../../../../ng-swagger-gen/models/project'; import { Project } from '../../../../../../ng-swagger-gen/models/project';
import { Router } from '@angular/router'; 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 { 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({ @Component({
selector: 'app-projects-modal', selector: 'app-projects-modal',
@ -14,12 +18,52 @@ export class ProjectsModalComponent {
robotName: string; robotName: string;
cachedAllProjects: Project[]; cachedAllProjects: Project[];
permissions: RobotPermission[] = []; permissions: RobotPermission[] = [];
i18nMap = ACTION_RESOURCE_I18N_MAP; pageSize: number = 10;
constructor(private router: Router) {} loading: boolean = false;
constructor(
private router: Router,
private projectService: ProjectService
) {}
close() { close() {
this.projectsModalOpened = false; 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 { getProject(p: RobotPermission): Project {
if (this.cachedAllProjects && this.cachedAllProjects.length) { if (this.cachedAllProjects && this.cachedAllProjects.length) {
for (let i = 0; i < this.cachedAllProjects.length; i++) { for (let i = 0; i < this.cachedAllProjects.length; i++) {
@ -33,4 +77,6 @@ export class ProjectsModalComponent {
goToLink(proId: number): void { goToLink(proId: number): void {
this.router.navigate(['harbor', 'projects', proId]); 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"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar> <clr-dg-action-bar>
<button <button
[disabled]="loadingData" [disabled]="loadingData || loadingMetadata"
[clrLoading]="addBtnState" [clrLoading]="addBtnState"
class="btn btn-secondary" class="btn btn-secondary"
(click)="openNewRobotModal(false)"> (click)="openNewRobotModal(false)">
@ -136,6 +136,7 @@
<clr-dg-column>{{ <clr-dg-column>{{
'ROBOT_ACCOUNT.ENABLED_STATE' | translate 'ROBOT_ACCOUNT.ENABLED_STATE' | translate
}}</clr-dg-column> }}</clr-dg-column>
<clr-dg-column>System Permissions</clr-dg-column>
<clr-dg-column>{{ <clr-dg-column>{{
'SYSTEM_ROBOT.PROJECTS' | translate 'SYSTEM_ROBOT.PROJECTS' | translate
}}</clr-dg-column> }}</clr-dg-column>
@ -165,6 +166,25 @@
size="16" size="16"
class="color-red red-position"></clr-icon> class="color-red red-position"></clr-icon>
</clr-dg-cell> </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> <clr-dg-cell>
<div <div
class="all-projects" class="all-projects"
@ -172,29 +192,20 @@
<span>{{ <span>{{
'SYSTEM_ROBOT.ALL_PROJECTS' | translate 'SYSTEM_ROBOT.ALL_PROJECTS' | translate
}}</span> }}</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 }} {{ r.permissionScope?.access?.length }}
{{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }} {{ 'SYSTEM_ROBOT.PERMISSIONS' | translate }}
<clr-icon shape="caret down"></clr-icon> <clr-icon
class="icon"
size="12"
shape="caret down"></clr-icon>
</button> </button>
<clr-dropdown-menu </robot-permissions-panel>
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>
</div> </div>
<span <span
*ngIf=" *ngIf="
@ -208,7 +219,7 @@
{{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }} {{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }}
</a> </a>
<span *ngIf="!getProjects(r)?.length"> <span *ngIf="!getProjects(r)?.length">
0 {{ 'SYSTEM_ROBOT.COVERED_PROJECTS' | translate }} {{ 'SCHEDULE.NONE' | translate }}
</span> </span>
</span> </span>
</clr-dg-cell> </clr-dg-cell>
@ -243,6 +254,8 @@
</clr-datagrid> </clr-datagrid>
</div> </div>
</div> </div>
<new-robot (addSuccess)="addSuccess($event)"></new-robot> <new-robot
(addSuccess)="addSuccess($event)"
[robotMetadata]="robotMetadata"></new-robot>
<view-token (refreshSuccess)="refresh()"></view-token> <view-token (refreshSuccess)="refresh()"></view-token>
<app-projects-modal></app-projects-modal> <app-projects-modal></app-projects-modal>

View File

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

View File

@ -21,15 +21,15 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { import {
ACTION_RESOURCE_I18N_MAP,
FrontRobot, FrontRobot,
getSystemAccess,
NAMESPACE_ALL_PROJECTS, NAMESPACE_ALL_PROJECTS,
NEW_EMPTY_ROBOT,
PermissionsKinds, PermissionsKinds,
} from './system-robot-util'; } from './system-robot-util';
import { ProjectsModalComponent } from './projects-modal/projects-modal.component'; import { ProjectsModalComponent } from './projects-modal/projects-modal.component';
import { forkJoin, Observable, of, Subscription } from 'rxjs'; import { forkJoin, Observable, of, Subscription } from 'rxjs';
import { FilterComponent } from '../../../shared/components/filter/filter.component'; import { FilterComponent } from '../../../shared/components/filter/filter.component';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { import {
operateChanges, operateChanges,
@ -37,7 +37,6 @@ import {
OperationState, OperationState,
} from '../../../shared/components/operation/operate'; } from '../../../shared/components/operation/operate';
import { OperationService } from '../../../shared/components/operation/operation.service'; import { OperationService } from '../../../shared/components/operation/operation.service';
import { Project } from '../../../../../ng-swagger-gen/models/project';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from '../../global-confirmation-dialog/confirmation-dialog.service'; 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 { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message';
import { RobotPermission } from '../../../../../ng-swagger-gen/models/robot-permission'; import { RobotPermission } from '../../../../../ng-swagger-gen/models/robot-permission';
import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service'; import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service';
import { Access } from '../../../../../ng-swagger-gen/models/access';
const FIRST_PROJECTS_PAGE_SIZE: number = 100; 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({ @Component({
selector: 'system-robot-accounts', selector: 'system-robot-accounts',
@ -59,7 +60,6 @@ const FIRST_PROJECTS_PAGE_SIZE: number = 100;
styleUrls: ['./system-robot-accounts.component.scss'], styleUrls: ['./system-robot-accounts.component.scss'],
}) })
export class SystemRobotAccountsComponent implements OnInit, OnDestroy { export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
pageSize: number = getPageSizeFromLocalStorage( pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.SYSTEM_ROBOT_COMPONENT PageSizeMapKeys.SYSTEM_ROBOT_COMPONENT
); );
@ -82,19 +82,22 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
searchKey: string; searchKey: string;
subscription: Subscription; subscription: Subscription;
deltaTime: number; // the different between server time and local time deltaTime: number; // the different between server time and local time
robotMetadata: Permissions;
loadingMetadata: boolean = false;
constructor( constructor(
private robotService: RobotService, private robotService: RobotService,
private projectService: ProjectService,
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
private operateDialogService: ConfirmationDialogService, private operateDialogService: ConfirmationDialogService,
private operationService: OperationService, private operationService: OperationService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private translate: TranslateService, private translate: TranslateService,
private systemInfoService: SysteminfoService private systemInfoService: SysteminfoService,
private permissionService: PermissionsService
) {} ) {}
ngOnInit() { ngOnInit() {
this.getRobotPermissions();
this.getCurrenTime(); this.getCurrenTime();
this.loadDataFromBackend();
if (!this.searchSub) { if (!this.searchSub) {
this.searchSub = this.filterComponent.filterTerms this.searchSub = this.filterComponent.filterTerms
.pipe( .pipe(
@ -169,6 +172,17 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
this.subscription = null; this.subscription = null;
} }
} }
getRobotPermissions() {
this.loadingData = true;
this.permissionService
.getPermissions()
.pipe(finalize(() => (this.loadingData = false)))
.subscribe(res => {
this.robotMetadata = res;
});
}
getCurrenTime() { getCurrenTime() {
this.systemInfoService.getSystemInfo().subscribe(res => { this.systemInfoService.getSystemInfo().subscribe(res => {
if (res?.current_time) { 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) { clrLoad(state?: ClrDatagridStateInterface) {
if (state && state.page && state.page.size) { if (state && state.page && state.page.size) {
this.pageSize = 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 = []; const arr = [];
if (r && r.permissions && r.permissions.length) { if (r && r.permissions && r.permissions.length) {
for (let i = 0; i < r.permissions.length; i++) { 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.projectsModalOpened = true;
this.projectsModalComponent.robotName = robotName; this.projectsModalComponent.robotName = robotName;
this.projectsModalComponent.permissions = permissions; this.projectsModalComponent.permissions = permissions;
this.projectsModalComponent.clrDgRefresh();
} }
refresh() { refresh() {
this.currentPage = 1; this.currentPage = 1;
@ -492,4 +425,11 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
} }
this.refresh(); 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 { Robot } from '../../../../../ng-swagger-gen/models/robot';
import { Access } from '../../../../../ng-swagger-gen/models/access'; 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 { export interface FrontRobot extends Robot {
permissionScope?: { 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 { export interface FrontAccess extends Access {
checked?: boolean; checked?: boolean;
} }
@ -43,78 +36,7 @@ export enum Action {
export const NAMESPACE_ALL_PROJECTS: string = '*'; export const NAMESPACE_ALL_PROJECTS: string = '*';
export const INITIAL_ACCESSES: FrontAccess[] = [ export const NAMESPACE_SYSTEM: string = '/';
{
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 ACTION_RESOURCE_I18N_MAP = { export const ACTION_RESOURCE_I18N_MAP = {
push: 'SYSTEM_ROBOT.PUSH_AND_PULL', // push permission contains pull permission 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', read: 'SYSTEM_ROBOT.READ',
create: 'SYSTEM_ROBOT.CREATE', create: 'SYSTEM_ROBOT.CREATE',
delete: 'SYSTEM_ROBOT.DELETE', 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', scan: 'SYSTEM_ROBOT.SCAN',
'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL',
stop: 'SYSTEM_ROBOT.STOP', stop: 'SYSTEM_ROBOT.STOP',
list: 'SYSTEM_ROBOT.LIST', 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 { export enum ExpirationType {
DEFAULT = 'default', DEFAULT = 'default',
DAYS = 'days', DAYS = 'days',
@ -168,3 +119,66 @@ export enum RobotTimeRemainColor {
WARNING = 'yellow', WARNING = 'yellow',
EXPIRED = 'red', 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 <clr-wizard
clrModalSize="md" #wizard
[(clrModalOpen)]="addRobotOpened" [(clrWizardOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true" [clrWizardSize]="'lg'"
[clrModalClosable]="true"> (clrWizardOnCancel)="cancel()">
<h3 *ngIf="!isEditMode" class="modal-title"> <clr-wizard-title
{{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT' | translate }} ><h3 *ngIf="!isEditMode" class="modal-title">
</h3> {{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT' | translate }}
<h3 *ngIf="isEditMode" class="modal-title"> </h3>
{{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT' | translate }} <h3 *ngIf="isEditMode" class="modal-title">
</h3> {{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT' | translate }}
<div class="modal-body"> </h3>
<inline-alert class="modal-title"></inline-alert>
<p *ngIf="!isEditMode" class="mt-0"> <p *ngIf="!isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT_SUMMARY' | translate }} {{ 'SYSTEM_ROBOT.CREATE_PROJECT_ROBOT_SUMMARY' | translate }}
</p> </p>
<p *ngIf="isEditMode" class="mt-0"> <p *ngIf="isEditMode" class="mt-0">
{{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT_SUMMARY' | translate }} {{ 'SYSTEM_ROBOT.EDIT_PROJECT_ROBOT_SUMMARY' | translate }}
</p> </p></clr-wizard-title
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1"> >
<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"> <section class="form-block">
<!-- name -->
<div class="clr-form-control"> <div class="clr-form-control">
<label for="name" class="clr-control-label required" <label for="name" class="clr-control-label required"
>{{ 'P2P_PROVIDER.NAME' | translate }} >{{ 'P2P_PROVIDER.NAME' | translate }}
@ -47,11 +67,11 @@
"> ">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<input <input
class="clr-input" class="clr-input input-width"
[disabled]="loading || isEditMode" [disabled]="loading || isEditMode"
type="text" type="text"
id="name" id="name"
[(ngModel)]="systemRobot.name" [(ngModel)]="robot.name"
required required
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
maxLengthExt="255" maxLengthExt="255"
@ -95,8 +115,17 @@
</clr-control-error> </clr-control-error>
</div> </div>
</div> </div>
<!-- expiration --> <clr-textarea-container class="mt-2">
<div class="clr-form-control"> <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" <label class="clr-control-label required"
>{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }} >{{ 'SYSTEM_ROBOT.EXPIRATION_TIME' | translate }}
<clr-tooltip> <clr-tooltip>
@ -144,15 +173,15 @@
</option> </option>
</select> </select>
</div> </div>
<div class="clr-input-wrapper"> <div class="clr-input-wrapper expiration">
<input <input
(input)="inputExpiration()" (input)="inputExpiration()"
class="clr-input expiration-width" class="clr-input"
name="expiration" name="expiration"
type="text" type="text"
#expiration="ngModel" #expiration="ngModel"
autocomplete="off" autocomplete="off"
[(ngModel)]="systemRobot.duration" [(ngModel)]="robot.duration"
required required
pattern="^[\-1-9]{1}[0-9]*$" pattern="^[\-1-9]{1}[0-9]*$"
id="robotTokenExpiration" id="robotTokenExpiration"
@ -165,7 +194,7 @@
<clr-control-helper <clr-control-helper
*ngIf=" *ngIf="
(isEditMode && (isEditMode &&
systemRobot?.duration > 0 && robot?.duration > 0 &&
!( !(
(expiration.dirty || (expiration.dirty ||
expiration.touched) && expiration.touched) &&
@ -201,104 +230,26 @@
</clr-control-error> </clr-control-error>
</div> </div>
</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> </section>
</form> </form>
</div> </clr-wizard-page>
<div class="modal-footer">
<span> <clr-wizard-page
<button (clrWizardPageOnCommit)="save()"
(click)="cancel()" [clrWizardPagePreventDefaultNext]="true"
id="system-robot-cancel" [clrWizardPageNextDisabled]="disabled()">
type="button" <ng-template clrPageTitle>{{
class="btn btn-outline"> 'ROBOT_ACCOUNT.SELECT_PERMISSIONS' | translate
{{ 'BUTTON.CANCEL' | translate }} }}</ng-template>
</button> <inline-alert class="modal-title"></inline-alert>
<button <form class="clr-form clr-form-horizontal mt-1">
[disabled]="disabled() || checkNameOnGoing || isNameExisting" <section class="form-block">
[clrLoading]="saveBtnState" <robot-permissions-panel
(click)="save()" [mode]="PermissionSelectPanelModes.NORMAL"
id="system-robot-save" [(permissionsModel)]="robot.permissions[0].access"
type="button" [candidatePermissions]="robotMetadata?.project">
class="btn btn-primary"> </robot-permissions-panel>
<span *ngIf="isEditMode">{{ 'BUTTON.SAVE' | translate }}</span> </section>
<span *ngIf="!isEditMode">{{ 'BUTTON.ADD' | translate }}</span> </form>
</button> </clr-wizard-page>
</span> </clr-wizard>
</div>
</clr-modal>

View File

@ -34,44 +34,26 @@
} }
.input-width { .input-width {
width: 232px; width: 16rem;
} }
.expiration-width { .expiration {
width: 80px; 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 */ /* stylelint-disable */
.showWarning { .showWarning {
color: #b3a000; color: #b3a000;
} }
.dropdown-item { .icon {
min-height: 20px; margin-top: 3px;
display: flex; }
.align-center {
align-items: center; align-items: center;
} }
.overflow-y-scroll { :host::ng-deep.modal-dialog {
overflow-y: auto; width: 46.2rem;
}
.ml-20px {
margin-left: 20px;
} }

View File

@ -16,30 +16,30 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { import {
ACTION_RESOURCE_I18N_MAP,
ExpirationType, ExpirationType,
FrontAccess, NEW_EMPTY_ROBOT,
INITIAL_ACCESSES,
onlyHasPushPermission, onlyHasPushPermission,
PermissionsKinds, PermissionsKinds,
} from '../../../left-side-nav/system-robot-accounts/system-robot-util'; } from '../../../left-side-nav/system-robot-accounts/system-robot-util';
import { Robot } from '../../../../../../ng-swagger-gen/models/robot'; import { Robot } from '../../../../../../ng-swagger-gen/models/robot';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { ClrLoadingState } from '@clr/angular'; import { ClrLoadingState, ClrWizard } from '@clr/angular';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.service'; import { RobotService } from '../../../../../../ng-swagger-gen/services/robot.service';
import { OperationService } from '../../../../shared/components/operation/operation.service'; import { OperationService } from '../../../../shared/components/operation/operation.service';
import { clone } from '../../../../shared/units/utils'; import { clone, isSameArrayValue } from '../../../../shared/units/utils';
import { import {
operateChanges, operateChanges,
OperateInfo, OperateInfo,
OperationState, OperationState,
} from '../../../../shared/components/operation/operate'; } from '../../../../shared/components/operation/operate';
import { Access } from '../../../../../../ng-swagger-gen/models/access';
import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component'; import { InlineAlertComponent } from '../../../../shared/components/inline-alert/inline-alert.component';
import { errorHandler } from '../../../../shared/units/shared.utils'; 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; const MINI_SECONDS_ONE_DAY: number = 60 * 24 * 60 * 1000;
@Component({ @Component({
selector: 'add-robot', selector: 'add-robot',
templateUrl: './add-robot.component.html', 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 { export class AddRobotComponent implements OnInit, OnDestroy {
@Input() projectId: number; @Input() projectId: number;
@Input() projectName: string; @Input() projectName: string;
i18nMap = ACTION_RESOURCE_I18N_MAP;
isEditMode: boolean = false; isEditMode: boolean = false;
originalRobotForEdit: Robot; originalRobotForEdit: Robot;
@Output() @Output()
addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>(); addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>();
addRobotOpened: boolean = false; addRobotOpened: boolean = false;
systemRobot: Robot = {}; robot: Robot = clone(NEW_EMPTY_ROBOT);
expirationType: string = ExpirationType.DAYS; expirationType: string = ExpirationType.DAYS;
isNameExisting: boolean = false; isNameExisting: boolean = false;
loading: boolean = false; loading: boolean = false;
checkNameOnGoing: boolean = false; checkNameOnGoing: boolean = false;
defaultAccesses: FrontAccess[] = [];
defaultAccessesForEdit: FrontAccess[] = [];
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
inlineAlertComponent: InlineAlertComponent; inlineAlertComponent: InlineAlertComponent;
@ViewChild('robotForm', { static: true }) robotForm: NgForm; @ViewChild('robotBasicForm', { static: true }) robotBasicForm: NgForm;
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
private _nameSubject: Subject<string> = new Subject<string>(); private _nameSubject: Subject<string> = new Subject<string>();
private _nameSubscription: Subscription; private _nameSubscription: Subscription;
@Input()
robotMetadata: Permissions;
@ViewChild('wizard') wizard: ClrWizard;
constructor( constructor(
private robotService: RobotService, private robotService: RobotService,
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
@ -119,10 +121,10 @@ export class AddRobotComponent implements OnInit, OnDestroy {
} }
} }
isExpirationInvalid(): boolean { isExpirationInvalid(): boolean {
return this.systemRobot.duration < -1; return this.robot.duration < -1;
} }
inputExpiration() { inputExpiration() {
if (+this.systemRobot.duration === -1) { if (+this.robot.duration === -1) {
this.expirationType = ExpirationType.NEVER; this.expirationType = ExpirationType.NEVER;
} else { } else {
this.expirationType = ExpirationType.DAYS; this.expirationType = ExpirationType.DAYS;
@ -130,60 +132,38 @@ export class AddRobotComponent implements OnInit, OnDestroy {
} }
changeExpirationType() { changeExpirationType() {
if (this.expirationType === ExpirationType.DAYS) { if (this.expirationType === ExpirationType.DAYS) {
this.systemRobot.duration = null; this.robot.duration = null;
} }
if (this.expirationType === ExpirationType.NEVER) { if (this.expirationType === ExpirationType.NEVER) {
this.systemRobot.duration = -1; this.robot.duration = -1;
} }
} }
inputName() { inputName() {
this._nameSubject.next(this.systemRobot.name); this._nameSubject.next(this.robot.name);
} }
cancel() { cancel() {
this.wizard.reset();
this.reset();
this.addRobotOpened = false; 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() { reset() {
this.open(false); this.open(false);
this.defaultAccesses = clone(INITIAL_ACCESSES); this.robot = clone(NEW_EMPTY_ROBOT);
this.systemRobot = {}; this.robotBasicForm.reset();
this.robotForm.reset();
this.expirationType = ExpirationType.DAYS; this.expirationType = ExpirationType.DAYS;
} }
resetForEdit(robot: Robot) { resetForEdit(robot: Robot) {
this.open(true); this.open(true);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.defaultAccesses.forEach(item => (item.checked = false));
this.originalRobotForEdit = clone(robot); this.originalRobotForEdit = clone(robot);
this.systemRobot = robot; this.robot = clone(robot);
this.expirationType = this.expirationType =
robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS; robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS;
this.defaultAccesses.forEach(item => { this.robotBasicForm.reset({
this.systemRobot.permissions[0].access.forEach(item2 => { name: this.robot.name,
if ( expiration: this.robot.duration,
item.resource === item2.resource && description: this.robot.description,
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,
}); });
} }
open(isEditMode: boolean) { open(isEditMode: boolean) {
@ -200,75 +180,37 @@ export class AddRobotComponent implements OnInit, OnDestroy {
return !this.canEdit(); return !this.canEdit();
} }
canAdd(): boolean { canAdd(): boolean {
let flag = false; return (
this.defaultAccesses.forEach(item => { this.robot?.permissions[0]?.access?.length > 0 &&
if (item.checked) { !this.robotBasicForm.invalid
flag = true; );
}
});
if (!flag) {
return false;
}
return !this.robotForm.invalid;
} }
canEdit() { canEdit() {
if (!this.canAdd()) { if (!this.canAdd()) {
return false; return false;
} }
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
if (this.systemRobot.duration != this.originalRobotForEdit.duration) { if (this.robot.duration != this.originalRobotForEdit.duration) {
return true; return true;
} }
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
if ( if (this.robot.description != this.originalRobotForEdit.description) {
this.systemRobot.description !=
this.originalRobotForEdit.description
) {
return true; return true;
} }
if ( return !isSameArrayValue(
this.getAccessNum(this.defaultAccesses) !== this.robot.permissions[0].access,
this.getAccessNum(this.defaultAccessesForEdit) this.originalRobotForEdit.permissions[0].access
) { );
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;
} }
save() { save() {
const robot: Robot = clone(this.systemRobot); const robot: Robot = clone(this.robot);
robot.disable = false; robot.disable = false;
robot.level = PermissionsKinds.PROJECT; robot.level = PermissionsKinds.PROJECT;
robot.duration = +this.systemRobot.duration; robot.duration = +this.robot.duration;
const access: Access[] = []; robot.permissions[0].kind = PermissionsKinds.PROJECT;
this.defaultAccesses.forEach(item => { robot.permissions[0].namespace = this.projectName;
if (item.checked) {
access.push({
resource: item.resource,
action: item.action,
});
}
});
robot.permissions = [
{
namespace: this.projectName,
kind: PermissionsKinds.PROJECT,
access: access,
},
];
// Push permission must work with pull permission // Push permission must work with pull permission
if (onlyHasPushPermission(access)) { if (onlyHasPushPermission(robot.permissions[0].access)) {
this.inlineAlertComponent.showInlineError( this.inlineAlertComponent.showInlineError(
'SYSTEM_ROBOT.PUSH_PERMISSION_TOOLTIP' 'SYSTEM_ROBOT.PUSH_PERMISSION_TOOLTIP'
); );
@ -276,7 +218,7 @@ export class AddRobotComponent implements OnInit, OnDestroy {
} }
this.saveBtnState = ClrLoadingState.LOADING; this.saveBtnState = ClrLoadingState.LOADING;
if (this.isEditMode) { if (this.isEditMode) {
robot.disable = this.systemRobot.disable; robot.disable = this.robot.disable;
const opeMessage = new OperateInfo(); const opeMessage = new OperateInfo();
opeMessage.name = 'SYSTEM_ROBOT.UPDATE_ROBOT'; opeMessage.name = 'SYSTEM_ROBOT.UPDATE_ROBOT';
opeMessage.data.id = robot.id; opeMessage.data.id = robot.id;
@ -292,7 +234,7 @@ export class AddRobotComponent implements OnInit, OnDestroy {
res => { res => {
this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(null); this.addSuccess.emit(null);
this.addRobotOpened = false; this.cancel();
operateChanges(opeMessage, OperationState.success); operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess( this.msgHandler.showSuccess(
'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY' 'SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY'
@ -324,7 +266,7 @@ export class AddRobotComponent implements OnInit, OnDestroy {
this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS;
this.saveBtnState = ClrLoadingState.SUCCESS; this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(res); this.addSuccess.emit(res);
this.addRobotOpened = false; this.cancel();
operateChanges(opeMessage, OperationState.success); operateChanges(opeMessage, OperationState.success);
}, },
error => { 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 { calculateExpiresAt(): Date {
if ( if (this.robot && this.robot.creation_time && this.robot.duration > 0) {
this.systemRobot &&
this.systemRobot.creation_time &&
this.systemRobot.duration > 0
) {
return new Date( return new Date(
new Date(this.systemRobot.creation_time).getTime() + new Date(this.robot.creation_time).getTime() +
this.systemRobot.duration * MINI_SECONDS_ONE_DAY this.robot.duration * MINI_SECONDS_ONE_DAY
); );
} }
return null; return null;
@ -364,26 +294,6 @@ export class AddRobotComponent implements OnInit, OnDestroy {
shouldShowWarning(): boolean { shouldShowWarning(): boolean {
return new Date() >= this.calculateExpiresAt(); return new Date() >= this.calculateExpiresAt();
} }
isSelectAll(permissions: FrontAccess[]): boolean {
if (permissions?.length) { protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes;
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;
});
}
}
}
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { of, Subscription } from 'rxjs'; 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 { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { RobotAccountComponent } from './robot-account.component'; import { RobotAccountComponent } from './robot-account.component';
import { UserPermissionService } from '../../../shared/services'; import { UserPermissionService } from '../../../shared/services';

View File

@ -13,10 +13,7 @@ import {
UserPermissionService, UserPermissionService,
USERSTATICPERMISSION, USERSTATICPERMISSION,
} from '../../../shared/services'; } from '../../../shared/services';
import { import { PermissionsKinds } from '../../left-side-nav/system-robot-accounts/system-robot-util';
ACTION_RESOURCE_I18N_MAP,
PermissionsKinds,
} from '../../left-side-nav/system-robot-accounts/system-robot-util';
import { import {
clone, clone,
getPageSizeFromLocalStorage, getPageSizeFromLocalStorage,
@ -50,6 +47,9 @@ import {
import { errorHandler } from '../../../shared/units/shared.utils'; import { errorHandler } from '../../../shared/units/shared.utils';
import { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../../global-confirmation-dialog/confirmation-message';
import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service'; 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({ @Component({
selector: 'app-robot-account', selector: 'app-robot-account',
@ -57,7 +57,6 @@ import { SysteminfoService } from '../../../../../ng-swagger-gen/services/system
styleUrls: ['./robot-account.component.scss'], styleUrls: ['./robot-account.component.scss'],
}) })
export class RobotAccountComponent implements OnInit, OnDestroy { export class RobotAccountComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
pageSize: number = getPageSizeFromLocalStorage( pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.PROJECT_ROBOT_COMPONENT PageSizeMapKeys.PROJECT_ROBOT_COMPONENT
); );
@ -83,6 +82,9 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
projectId: number; projectId: number;
projectName: string; projectName: string;
deltaTime: number; // the different between server time and local time deltaTime: number; // the different between server time and local time
loadingMetadata: boolean = false;
robotMetadata: Permissions;
constructor( constructor(
private robotService: RobotService, private robotService: RobotService,
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
@ -92,7 +94,8 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private translate: TranslateService, private translate: TranslateService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private systemInfoService: SysteminfoService private systemInfoService: SysteminfoService,
private permissionService: PermissionsService
) {} ) {}
ngOnInit() { ngOnInit() {
this.getCurrenTime(); 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() { getCurrenTime() {
this.systemInfoService.getSystemInfo().subscribe(res => { this.systemInfoService.getSystemInfo().subscribe(res => {
if (res?.current_time) { if (res?.current_time) {
@ -214,6 +228,9 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
forkJoin(...permissionsList).subscribe( forkJoin(...permissionsList).subscribe(
Rules => { Rules => {
this.hasRobotCreatePermission = Rules[0] as boolean; this.hasRobotCreatePermission = Rules[0] as boolean;
if (this.hasRobotCreatePermission) {
this.getRobotPermissions();
}
this.hasRobotUpdatePermission = Rules[1] as boolean; this.hasRobotUpdatePermission = Rules[1] as boolean;
this.hasRobotDeletePermission = Rules[2] as boolean; this.hasRobotDeletePermission = Rules[2] as boolean;
this.hasRobotReadPermission = Rules[3] as boolean; this.hasRobotReadPermission = Rules[3] as boolean;
@ -418,4 +435,5 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
} }
this.refresh(); this.refresh();
} }
protected readonly PermissionSelectPanelModes = PermissionSelectPanelModes;
} }

View File

@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemainingTimeComponent } from './remaining-time.component'; import { RemainingTimeComponent } from './remaining-time.component';
import { Component, ViewChild } from '@angular/core'; 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 { RobotTimeRemainColor } from '../../../base/left-side-nav/system-robot-accounts/system-robot-util';
import { SharedTestingModule } from '../../shared.module'; 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'; } from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features'; import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers'; import { CanvasRenderer } from 'echarts/renderers';
import { RobotPermissionsPanelComponent } from './components/robot-permissions-panel/robot-permissions-panel.component';
// register necessary components // register necessary components
echarts.use([ echarts.use([
@ -175,6 +176,7 @@ ClarityIcons.add({
RemainingTimeComponent, RemainingTimeComponent,
LabelSelectorComponent, LabelSelectorComponent,
AppLevelAlertsComponent, AppLevelAlertsComponent,
RobotPermissionsPanelComponent,
], ],
exports: [ exports: [
TranslateModule, TranslateModule,
@ -217,6 +219,7 @@ ClarityIcons.add({
RemainingTimeComponent, RemainingTimeComponent,
LabelSelectorComponent, LabelSelectorComponent,
AppLevelAlertsComponent, AppLevelAlertsComponent,
RobotPermissionsPanelComponent,
], ],
providers: [ providers: [
{ provide: EndpointService, useClass: EndpointDefaultService }, { provide: EndpointService, useClass: EndpointDefaultService },

View File

@ -378,7 +378,45 @@
"INVALID_VALUE": "Der Wert der Ablaufzeit ist ungültig", "INVALID_VALUE": "Der Wert der Ablaufzeit ist ungültig",
"NEVER_EXPIRED": "Läuft nie ab", "NEVER_EXPIRED": "Läuft nie ab",
"NAME_PREFIX": "Prefix für den Namen der Robot-Zugänge", "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": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -378,7 +378,45 @@
"INVALID_VALUE": "The value of the expiration time is invalid", "INVALID_VALUE": "The value of the expiration time is invalid",
"NEVER_EXPIRED": "Never Expires", "NEVER_EXPIRED": "Never Expires",
"NAME_PREFIX": "Robot Name Prefix", "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": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -379,7 +379,45 @@
"INVALID_VALUE": "The value of the expiration time is invalid", "INVALID_VALUE": "The value of the expiration time is invalid",
"NEVER_EXPIRED": "Never Expired", "NEVER_EXPIRED": "Never Expired",
"NAME_PREFIX": "Robot Name Prefix", "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": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -370,7 +370,45 @@
"INVALID_VALUE": "La valeur de la date d'expiration est invalide", "INVALID_VALUE": "La valeur de la date d'expiration est invalide",
"NEVER_EXPIRED": "Ne jamais expirer", "NEVER_EXPIRED": "Ne jamais expirer",
"NAME_PREFIX": "Préfixe du nom du compte robot", "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": { "WEBHOOK": {
"EDIT_BUTTON": "Éditer", "EDIT_BUTTON": "Éditer",

View File

@ -376,7 +376,45 @@
"INVALID_VALUE": "Valor do tempo de expiração inválido.", "INVALID_VALUE": "Valor do tempo de expiração inválido.",
"NEVER_EXPIRED": "Não expirar nunca", "NEVER_EXPIRED": "Não expirar nunca",
"NAME_PREFIX": "Prefixo para contas de automação", "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": {
"GROUP": "Grupo", "GROUP": "Grupo",

View File

@ -378,7 +378,45 @@
"INVALID_VALUE": "The value of the expiration time is invalid", "INVALID_VALUE": "The value of the expiration time is invalid",
"NEVER_EXPIRED": "Never Expired", "NEVER_EXPIRED": "Never Expired",
"NAME_PREFIX": "Robot Name Prefix", "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": { "WEBHOOK": {
"EDIT_BUTTON": "DÜZENLE", "EDIT_BUTTON": "DÜZENLE",

View File

@ -377,7 +377,45 @@
"INVALID_VALUE": "无效的过期日期", "INVALID_VALUE": "无效的过期日期",
"NEVER_EXPIRED": "永不过期", "NEVER_EXPIRED": "永不过期",
"NAME_PREFIX": "机器人账户名称前缀", "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": { "WEBHOOK": {
"EDIT_BUTTON": "编辑", "EDIT_BUTTON": "编辑",

View File

@ -377,7 +377,45 @@
"INVALID_VALUE": "無效的過期日期", "INVALID_VALUE": "無效的過期日期",
"NEVER_EXPIRED": "永不過期", "NEVER_EXPIRED": "永不過期",
"NAME_PREFIX": "機器人名稱前綴", "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": { "WEBHOOK": {
"EDIT_BUTTON": "編輯", "EDIT_BUTTON": "編輯",