Merge pull request #13662 from AllForNothing/robot

Add system robot account UI
This commit is contained in:
Will Sun 2020-12-04 15:37:00 +08:00 committed by GitHub
commit cf9b73d9a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 3698 additions and 835 deletions

View File

@ -47,6 +47,7 @@ import { HarborLibraryModule } from "../lib/harbor-library.module";
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AllPipesModule } from './all-pipes/all-pipes.module';
import { DistributionModule } from './distribution/distribution.module';
import { SystemRobotAccountsModule } from './system-robot-accounts/system-robot-accounts.module';
registerLocaleData(zh, 'zh-cn');
registerLocaleData(es, 'es-es');
registerLocaleData(localeFr, 'fr-fr');
@ -88,6 +89,7 @@ export function getCurrentLanguage(translateService: TranslateService) {
HarborLibraryModule,
AllPipesModule,
DistributionModule,
SystemRobotAccountsModule
],
exports: [
],

View File

@ -42,6 +42,10 @@
<a clrVerticalNavLink routerLink="/harbor/users" routerLinkActive="active">
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
</a>
<a clrVerticalNavLink routerLink="/harbor/robot-accounts" routerLinkActive="active">
<clr-icon shape="robot-head" clrVerticalNavIcon></clr-icon>
{{"SYSTEM_ROBOT.ROBOT_ACCOUNT_NAV" | translate}}
</a>
<a *ngIf='isLdapMode || isHttpAuthMode || isOidcMode' clrVerticalNavLink
routerLink="/harbor/groups" routerLinkActive="active">

View File

@ -21,7 +21,6 @@ import { ConfigurationService } from "./config.service";
import { ConfirmMessageHandler } from "./config.msg.utils";
import { ConfigurationAuthComponent } from "./auth/config-auth.component";
import { ConfigurationEmailComponent } from "./email/config-email.component";
import { RobotApiRepository } from "../project/robot-account/robot.api.repository";
import { ConfigurationScannerComponent } from "./scanner/config-scanner.component";
import { NewScannerModalComponent } from "./scanner/new-scanner-modal/new-scanner-modal.component";
import { NewScannerFormComponent } from "./scanner/new-scanner-form/new-scanner-form.component";
@ -44,7 +43,6 @@ import { ScannerMetadataComponent } from "./scanner/scanner-metadata/scanner-met
providers: [
ConfigurationService,
ConfirmMessageHandler,
RobotApiRepository,
ConfigScannerService,
]
})

View File

@ -18,6 +18,27 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
import { CookieModule } from 'ngx-cookie';
import { MarkdownModule } from 'ngx-markdown';
import { ClarityIconsApi } from '@clr/icons/clr-icons-api';
// ClarityIcons is publicly accessible from the browser's window object.
declare const ClarityIcons: ClarityIconsApi;
// Add custom icons to ClarityIcons
// Add robot head icon
ClarityIcons.add({"robot-head": `
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">
<defs><style>.cls-1{fill:none;}</style></defs><g id="Layer_2" data-name="Layer 2">
<circle cx="12.62" cy="18.6" r="1.5"/><circle cx="23.5" cy="18.5" r="1.5"/>
<path d="M22,28H14a1,1,0,0,1,0-2h8a1,1,0,0,1,0,2Z"/>
<path d="M35,25.22a1,1,0,0,1-1-1V19.38a1,1,0,1,1,2,0v4.84A1,1,0,0,1,35,25.22Z"/>
<path d="M1,25a1,1,0,0,1-1-1V19a1,1,0,0,1,2,0v5A1,1,0,0,1,1,25Z"/>
<path d="M19,8.26A3.26,3.26,0,1,1,22.26,5,3.26,3.26,0,0,1,19,8.26Zm0-4.92A1.66,1.66,0,1,0,20.66,5,1.67,1.67,0,0,0,19,3.34Z"/>
<path d="M29.1,10.49a1,1,0,0,0-.86-.49H20V7.58H18V12h9.67A19.51,19.51,0,0,1,30,21.42,
19.06,19.06,0,0,1,27,32H9.05A19.06,19.06,0,0,1,6,21.42,
19.51,19.51,0,0,1,8.33,12H16V10H7.76a1,1,0,0,0-.86.49A21.18,
21.18,0,0,0,4,21.42,21,21,0,0,0,7.71,33.58a1,1,0,0,0,.81.42h19a1,1,0,0,0,
.81-.42A21,21,0,0,0,32,21.42,21.18,21.18,0,0,0,29.1,10.49Z"/>
<rect class="cls-1" width="36" height="36"/></g></svg>`});
@NgModule({
imports: [

View File

@ -67,6 +67,7 @@ import { DistributionInstancesComponent } from './distribution/distribution-inst
import { PolicyComponent } from './project/p2p-provider/policy/policy.component';
import { TaskListComponent } from './project/p2p-provider/task-list/task-list.component';
import { P2pProviderComponent } from './project/p2p-provider/p2p-provider.component';
import { SystemRobotAccountsComponent } from './system-robot-accounts/system-robot-accounts.component';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -112,6 +113,11 @@ const harborRoutes: Routes = [
component: UserComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'robot-accounts',
component: SystemRobotAccountsComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'groups',
component: GroupComponent,

View File

@ -26,7 +26,6 @@ import { MemberComponent } from './member/member.component';
import { AddMemberComponent } from './member/add-member/add-member.component';
import { AddGroupComponent } from './member/add-group/add-group.component';
import { MemberService } from './member/member.service';
import { RobotService } from './robot-account/robot-account.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
import { HelmChartModule } from './helm-chart/helm-chart.module';
import { RobotAccountComponent } from './robot-account/robot-account.component';
@ -78,7 +77,7 @@ import { P2pProviderComponent } from './p2p-provider/p2p-provider.component';
HelmChartModule,
SummaryModule,
TagFeatureIntegrationModule,
AllPipesModule
AllPipesModule,
],
declarations: [
ProjectComponent,
@ -120,12 +119,11 @@ import { P2pProviderComponent } from './p2p-provider/p2p-provider.component';
PolicyComponent,
AddP2pPolicyComponent,
TaskListComponent,
P2pProviderComponent
P2pProviderComponent,
],
exports: [ProjectComponent, ListProjectComponent],
providers: [
MemberService,
RobotService,
WebhookService,
ConfigScannerService,
ArtifactDefaultService,

View File

@ -1,163 +1,108 @@
<clr-modal [(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
<inline-alert #copyAlert class="modal-title"></inline-alert>
<clr-modal clrModalSize="md" [(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<h3 *ngIf="!isEditMode" class="modal-title">{{"SYSTEM_ROBOT.CREATE_PROJECT_ROBOT" | translate}}</h3>
<h3 *ngIf="isEditMode" class="modal-title">{{"SYSTEM_ROBOT.EDIT_PROJECT_ROBOT" | translate}}</h3>
<div class="modal-body">
<form #robotForm="ngForm">
<inline-alert class="modal-title"></inline-alert>
<p *ngIf="!isEditMode" class="mt-0">{{"SYSTEM_ROBOT.CREATE_PROJECT_ROBOT_SUMMARY" | translate}}</p>
<p *ngIf="isEditMode" class="mt-0">{{"SYSTEM_ROBOT.EDIT_PROJECT_ROBOT_SUMMARY" | translate}}</p>
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1">
<section class="form-block">
<div class="clr-row">
<div class="clr-col-4 permission permission-dark">
<label class="col-md-3 required">
{{'ROBOT_ACCOUNT.NAME' | translate}}
</label>
<!-- name -->
<div class="clr-form-control">
<label for="name" class="clr-control-label required">{{'P2P_PROVIDER.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="((name.dirty || name.touched) && name.invalid) || isNameExisting">
<div class="clr-input-wrapper">
<input class="clr-input"
[disabled]="loading || isEditMode"
type="text" id="name"
[(ngModel)]="systemRobot.name"
required
pattern='[^" ~#$%]+'
maxLengthExt="255"
autocomplete="off"
size="30" name="name" #name="ngModel" (input)="inputName()">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkNameOnGoing"></span>
</div>
<clr-control-error *ngIf="((name.dirty || name.touched) && name.invalid) || isNameExisting">
<span *ngIf="!((name.dirty || name.touched) && name.invalid) && isNameExisting">{{'ROBOT_ACCOUNT.ACCOUNT_EXISTING' | translate}}</span>
<span *ngIf="(name.dirty || name.touched) && name.invalid">{{ 'ROBOT_ACCOUNT.ROBOT_NAME' | translate }}</span>
</clr-control-error>
</div>
<div class="clr-col padding-left-0">
<div class="clr-control-container" [class.clr-error]="!isRobotNameValid">
</div>
<!-- expiration -->
<div class="clr-form-control">
<label class="clr-control-label required">{{"SYSTEM_ROBOT.EXPIRATION_TIME" | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{"SYSTEM_ROBOT.EXPIRATION_TIME_EXPLAIN" | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<div class="clr-control-container" [class.clr-error]="((expiration.dirty || expiration.touched) && expiration.invalid) || isExpirationInvalid()">
<div class="input-width flex">
<div class="clr-select-wrapper">
<select [ngModelOptions]="{standalone: true}" (change)="changeExpirationType()" [(ngModel)]="expirationType" id="expiration-type" class="clr-select">
<option value="days">{{"SYSTEM_ROBOT.EXPIRATION_DAYS" | translate}}</option>
<option value="never">{{"SYSTEM_ROBOT.EXPIRATION_NEVER" | translate}}</option>
</select>
</div>
<div class="clr-input-wrapper">
<input class="clr-input input-width" type="text"
[(ngModel)]="robot.name"
size="30"
name="robot_name"
id="robot_name"
#robotName="ngModel"
required
pattern='[^" ~#$%]+'
maxLengthExt="255"
<input (input)="inputExpiration()" class="clr-input expiration-width" name="expiration" type="text"
#expiration="ngModel"
autocomplete="off"
(keyup)='handleValidation()'>
[(ngModel)]="systemRobot.duration" required
pattern="^[\-1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20"/>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isRobotNameValid" class="tooltip-content">
{{ nameTooltipText | translate }}
</clr-control-error>
</div>
<clr-control-error *ngIf="((expiration.dirty || expiration.touched) && expiration.invalid)|| isExpirationInvalid()">
{{"SYSTEM_ROBOT.EXPIRATION_REQUIRED" | translate}}
</clr-control-error>
</div>
</div>
<div class="clr-row mt-1">
<div class="clr-col-4 permission permission-dark">
<label class="col-md-3">{{'ROBOT_ACCOUNT.EXPIRES_AT' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'ROBOT_ACCOUNT.EXPIRATION_TOOLTIP' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
</div>
<div class="clr-col padding-left-0 date">
<input class="input-width-date" type="date" readonly clrDate name="expiresAt"
[(clrDate)]="expiresDate" placeholder="{{expiresDatePlaceholder}}">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="neverExpired" [(ngModel)]="isNeverExpired" (change)="switch()" />
<label>{{"ROBOT_ACCOUNT.NEVER_EXPIRED" | translate}}</label>
</clr-checkbox-wrapper>
</div>
</div>
<div class="clr-row mt-1">
<div class="clr-col-4 permission permission-dark">
<label class="col-md-3">{{'REPLICATION.DESCRIPTION' |translate}}</label>
</div>
<div class="clr-col padding-left-0">
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input class="clr-input input-width" type="text" size="255"
[(ngModel)]="robot.description"
name="robot_desc" id="robot_desc">
</div>
</div>
</div>
</div>
<div class="clr-row mt-1">
<div class="clr-col-4 permission permission-dark">
<label class="col-md-3">
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
</label>
</div>
<div class="clr-col padding-left-0">
<table class="table table-noborder m-0 w-90">
<tr>
<th></th>
<th class="left">{{'ROBOT_ACCOUNT.PUSH' | translate}}</th>
<th class="left">{{'ROBOT_ACCOUNT.PULL' | translate}}</th>
</tr>
<tr>
<td class="left">
<span>{{'ROBOT_ACCOUNT.PERMISSIONS_ARTIFACT' | translate}}</span>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'ROBOT_ACCOUNT.PULL_IS_MUST' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</td>
<td>
<input type="checkbox" name="image-permission-push"
[(ngModel)]="imagePermissionPush" clrCheckbox>
</td>
<td class="clr-form-control-disabled">
<input disabled type="checkbox" name="image-permission-pull"
[(ngModel)]="imagePermissionPull" clrCheckbox>
</td>
</tr>
<tr *ngIf="withHelmChart">
<td class="left">{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</td>
<td>
<input type="checkbox"
[(ngModel)]="robot.access.isPushChart"
name="helm-permission" clrCheckbox>
</td>
<td>
<input type="checkbox"
[(ngModel)]="robot.access.isPullChart"
name="helm-permission" clrCheckbox>
</td>
</tr>
</table>
<!-- 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 clrPosition="bottom-left" *clrIfOpen>
<div clrDropdownItem *ngFor="let item of defaultAccesses" (click)="chooseAccess(item)">
<clr-icon class="check" shape="check" [style.visibility]="item.checked ? 'visible' : 'hidden'"></clr-icon>
<span>{{i18nMap[item.action] | translate}} {{i18nMap[item.resource] | translate}}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL'
| translate}}</button>
<button type="button" [disabled]="shouldDisable" class="btn btn-primary"
(click)="onSubmit()">{{'BUTTON.SAVE'
| translate}}</button>
<span>
<button (click)="cancel()" id="system-robot-cancel" type="button" class="btn btn-outline">{{'BUTTON.CANCEL'
| translate}}</button>
<button [disabled]="disabled()|| checkNameOnGoing" [clrLoading]="saveBtnState" (click)="save()" id="system-robot-save" type="button"
class="btn btn-primary">
<span *ngIf="isEditMode">{{'BUTTON.SAVE'| translate}}</span>
<span *ngIf="!isEditMode">{{'BUTTON.ADD'| translate}}</span>
</button>
</span>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="copyToken" class="copy-token"
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<div class="modal-title">
<h3 class="modal-title">
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
{{ createSuccess | translate}}</h3>
<div class="alert alert-info" role="alert">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
</div>
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
</div>
</div>
</div>
</div>
<div class="modal-body">
<section class="form-block show-info">
<div class="form-group robot-name">
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
| translate}}</label>
<span>{{robotAccount}}</span>
</div>
<div class="form-group robot-token">
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
translate}}</label>
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
(onCopyError)="onCpError($event)" inputSize="50" headerTitle=""
defaultValue="{{robotToken}}" class="copy-input"></hbr-copy-input>
</div>
<a [href]="downLoadHref" [download]="downLoadFileName"><button class="btn mr-0" (click)="closeModal()">{{'ROBOT_ACCOUNT.EXPORT_TO_FILE' | translate}}</button></a>
</section>
</div>
</clr-modal>

View File

@ -1,34 +1,3 @@
.rule-width {
width: 100%;
}
.input-width {
width: 300px;
}
.copy-token {
.success-icon {
color: #318700;
}
.show-info {
.robot-name {
margin: 30px 0;
label {
margin-right: 30px;
}
}
.robot-token {
margin-bottom: 20px;
label {
margin-right: 24px;
}
.copy-input {
display: inline-block;
}
}
}
}
.padding-left-0 {
padding-left: 0;
}
@ -52,4 +21,28 @@
}
.input-width-date {
width: 265px;
}
.flex {
display: flex;
align-items: center;
justify-content: space-between;
}
.input-width {
width: 232px;
}
.expiration-width {
width: 80px;
}
.check {
margin-right: 5px;
color: green;
}
.dropdown-per {
margin-left: -12px;
}
.mt-description {
width: 238px;
}
.mt-8px {
margin-top: 8px !important;
}

View File

@ -1,55 +1,48 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClarityModule } from '@clr/angular';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AddRobotComponent } from './add-robot.component';
import { FormsModule } from '@angular/forms';
import { RobotService } from "../robot-account.service";
import { of } from "rxjs";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppConfigService } from "../../../services/app-config.service";
import { ErrorHandler } from "../../../../lib/utils/error-handler";
import { TranslateModule } from '@ngx-translate/core';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { delay } from "rxjs/operators";
import { RobotService } from "../../../../../ng-swagger-gen/services/robot.service";
import { OperationService } from "../../../../lib/components/operation/operation.service";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { SharedModule } from "../../../shared/shared.module";
describe('AddRobotComponent', () => {
let component: AddRobotComponent;
let fixture: ComponentFixture<AddRobotComponent>;
let fakeRobotService = {
listRobotAccount: function () {
return of([{
name: "robot$" + 1
}, {
name: "abc"
}]);
const fakedRobotService = {
ListRobot() {
return of([]).pipe(delay(0));
}
};
let fakeMessageHandlerService = {
showSuccess: function() {}
};
let fakeAppConfigService = {
getConfig: function() {
return {
with_chartmuseum: true
};
const fakedMessageHandlerService = {
showSuccess() {
},
error() {
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddRobotComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot(),
FormsModule
FormsModule,
SharedModule
],
providers: [
TranslateService,
ErrorHandler,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: RobotService, useValue: fakeRobotService }
OperationService,
{ provide: RobotService, useValue: fakedRobotService },
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));

View File

@ -2,254 +2,278 @@ import {
Component,
OnInit,
Input,
ViewChild,
OnDestroy,
Output,
EventEmitter,
ChangeDetectorRef
OnDestroy, Output, EventEmitter, ViewChild,
} from "@angular/core";
import { Robot } from "../robot";
import { NgForm } from "@angular/forms";
import { Subject } from "rxjs";
import { debounceTime, finalize } from "rxjs/operators";
import { RobotService } from "../robot-account.service";
import { TranslateService } from "@ngx-translate/core";
import { debounceTime, distinctUntilChanged, filter, finalize, switchMap } from "rxjs/operators";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import {
ACTION_RESOURCE_I18N_MAP,
ExpirationType,
FrontAccess, INITIAL_ACCESSES, PermissionsKinds
} from "../../../system-robot-accounts/system-robot-util";
import { Robot } from "../../../../../ng-swagger-gen/models/robot";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { AppConfigService } from "../../../services/app-config.service";
import { ErrorHandler } from "../../../../lib/utils/error-handler";
const ONE_THOUSAND: number = 1000;
const NEVER_EXPIRED: number = -1;
import { NgForm } from "@angular/forms";
import { ClrLoadingState } from "@clr/angular";
import { Subject, Subscription } from "rxjs";
import { RobotService } from "../../../../../ng-swagger-gen/services/robot.service";
import { OperationService } from "../../../../lib/components/operation/operation.service";
import { clone } from "../../../../lib/utils/utils";
import { operateChanges, OperateInfo, OperationState } from "../../../../lib/components/operation/operate";
import { errorHandler } from "../../../../lib/utils/shared/shared.utils";
import { Access } from "../../../../../ng-swagger-gen/models/access";
@Component({
selector: "add-robot",
templateUrl: "./add-robot.component.html",
styleUrls: ["./add-robot.component.scss"]
})
export class AddRobotComponent implements OnInit, OnDestroy {
addRobotOpened: boolean;
copyToken: boolean;
robotToken: string;
robotAccount: string;
downLoadFileName: string = '';
downLoadHref: SafeUrl = '';
isSubmitOnGoing = false;
closable: boolean = false;
staticBackdrop: boolean = true;
createSuccess: string;
isRobotNameValid: boolean = true;
checkOnGoing: boolean = false;
robot: Robot = new Robot();
robotNameChecker: Subject<string> = new Subject<string>();
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
robotForm: NgForm;
imagePermissionPush: boolean = true;
imagePermissionPull: boolean = true;
withHelmChart: boolean;
@Input() projectId: number;
@Input() projectName: string;
@Output() create = new EventEmitter<boolean>();
@ViewChild("robotForm", {static: true}) currentForm: NgForm;
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
private _expiresDate: Date;
isNeverExpired: boolean = false;
expiresDatePlaceholder: string = ' ';
i18nMap = ACTION_RESOURCE_I18N_MAP;
isEditMode: boolean = false;
originalRobotForEdit: Robot;
@Output()
addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>();
addRobotOpened: boolean = false;
systemRobot: Robot = {};
expirationType: string = ExpirationType.DAYS;
isNameExisting: boolean = false;
loading: boolean = false;
checkNameOnGoing: boolean = false;
defaultAccesses: FrontAccess[] = [];
defaultAccessesForEdit: FrontAccess[] = [];
@ViewChild(InlineAlertComponent)
inlineAlertComponent: InlineAlertComponent;
@ViewChild('robotForm', { static: true }) robotForm: NgForm;
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
private _nameSubject: Subject<string> = new Subject<string>();
private _nameSubscription: Subscription;
constructor(
private robotService: RobotService,
private translate: TranslateService,
private errorHandler: ErrorHandler,
private cdr: ChangeDetectorRef,
private messageHandlerService: MessageHandlerService,
private sanitizer: DomSanitizer,
private appConfigService: AppConfigService
private msgHandler: MessageHandlerService,
private operationService: OperationService
) {}
ngOnInit(): void {
this.withHelmChart = this.appConfigService.getConfig().with_chartmuseum;
this.robotNameChecker.pipe(debounceTime(800)).subscribe((name: string) => {
let cont = this.currentForm.controls["robot_name"];
if (cont) {
this.isRobotNameValid = cont.valid;
if (this.isRobotNameValid) {
this.checkOnGoing = true;
this.robotService
.listRobotAccount(this.projectId)
.pipe(
finalize(() => {
this.checkOnGoing = false;
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
})
)
.subscribe(
response => {
if (response && response.length) {
if (
response.find(target => {
return target.name === "robot$" + cont.value;
})
) {
this.isRobotNameValid = false;
this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING";
}
}
},
error => {
this.errorHandler.error(error);
}
);
} else {
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
}
}
});
this.subscribeName();
}
openAddRobotModal(): void {
if (this.isSubmitOnGoing) {
return;
ngOnDestroy() {
if (this._nameSubscription) {
this._nameSubscription.unsubscribe();
this._nameSubscription = null;
}
this.robot.name = "";
this.robot.description = "";
this.addRobotOpened = true;
this.imagePermissionPush = true;
this.imagePermissionPull = true;
this.isRobotNameValid = true;
this.robot = new Robot();
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
this.isNeverExpired = false;
this.expiresDate = null;
this.expiresDatePlaceholder = ' ';
this.copyAlert.close();
}
onCancel(): void {
subscribeName() {
if (!this._nameSubscription) {
this._nameSubscription = this._nameSubject
.pipe(
debounceTime(500),
distinctUntilChanged(),
filter(name => {
if (this.isEditMode && this.originalRobotForEdit && this.originalRobotForEdit.name === name) {
return false;
}
return name.length > 0;
}),
switchMap((name) => {
this.isNameExisting = false;
this.checkNameOnGoing = true;
return this.robotService.ListRobot({
q: encodeURIComponent(`Level=${PermissionsKinds.PROJECT},ProjectID=${this.projectId},name=${name}`)
}).pipe(finalize(() => this.checkNameOnGoing = false));
}))
.subscribe(res => {
if (res && res.length > 0) {
this.isNameExisting = true;
}
});
}
}
isExpirationInvalid(): boolean {
return this.systemRobot.duration < -1;
}
inputExpiration() {
if (+this.systemRobot.duration === -1) {
this.expirationType = ExpirationType.NEVER;
} else {
this.expirationType = ExpirationType.DAYS;
}
}
changeExpirationType() {
if (this.expirationType === ExpirationType.DAYS) {
this.systemRobot.duration = null;
}
if (this.expirationType === ExpirationType.NEVER) {
this.systemRobot.duration = -1;
}
}
inputName() {
this._nameSubject.next(this.systemRobot.name);
}
cancel() {
this.addRobotOpened = false;
}
ngOnDestroy(): void {
this.robotNameChecker.unsubscribe();
}
onSubmit(): void {
if (this.isSubmitOnGoing) {
return;
}
// set value to robot.access.isPullImage and robot.access.isPushOrPullImage when submit
if ( this.imagePermissionPush && this.imagePermissionPull) {
this.robot.access.isPullImage = false;
this.robot.access.isPushOrPullImage = true;
} else {
this.robot.access.isPullImage = true;
this.robot.access.isPushOrPullImage = false;
}
if (this.isNeverExpired) {
this.robot.expires_at = NEVER_EXPIRED;
} else {
if (this.expiresDate) {
if (this.expiresDate <= new Date()) {
this.copyAlert.showInlineError("ROBOT_ACCOUNT.INVALID_VALUE");
return;
} else {
this.robot.expires_at = Math.floor(this.expiresDate.getTime() / ONE_THOUSAND);
}
getPermissions(): number {
let count: number = 0;
this.defaultAccesses.forEach(item => {
if (item.checked) {
count ++;
}
});
return count;
}
chooseAccess(access: FrontAccess) {
access.checked = !access.checked;
}
reset() {
this.open(false);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.systemRobot = {};
this.robotForm.reset();
this.expirationType = ExpirationType.DAYS;
}
resetForEdit(robot: Robot) {
this.open(true);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.defaultAccesses.forEach( item => item.checked = false);
this.originalRobotForEdit = clone(robot);
this.systemRobot = robot;
this.expirationType =
robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS;
this.defaultAccesses.forEach(item => {
this.systemRobot.permissions[0].access.forEach(item2 => {
if (item.resource === item2.resource && item.action === item2.action) {
item.checked = true;
}
});
});
this.defaultAccessesForEdit = clone(this.defaultAccesses);
this.robotForm.reset({
name: this.systemRobot.name,
expiration: this.systemRobot.duration,
description: this.systemRobot.description,
});
}
open(isEditMode: boolean) {
this.isEditMode = isEditMode;
this.addRobotOpened = true;
this.inlineAlertComponent.close();
}
disabled(): boolean {
if (!this.isEditMode) {
return !this.canAdd();
}
this.isSubmitOnGoing = true;
this.robotService
.addRobotAccount(
this.projectId,
this.robot,
this.projectName
)
.subscribe(
response => {
this.isSubmitOnGoing = false;
this.robotToken = response.token;
this.robotAccount = response.name;
this.copyToken = true;
this.create.emit(true);
this.translate
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount })
.subscribe((res: string) => {
this.createSuccess = res;
});
this.addRobotOpened = false;
// export to token file
const downLoadUrl = `data:text/json;charset=utf-8, ${encodeURIComponent(JSON.stringify(response))}`;
this.downLoadHref = this.sanitizer.bypassSecurityTrustUrl(downLoadUrl);
this.downLoadFileName = `${response.name}.json`;
},
error => {
this.isSubmitOnGoing = false;
this.copyAlert.showInlineError(error);
}
);
return !this.canEdit();
}
isValid(): boolean {
return (
this.currentForm &&
this.currentForm.valid &&
!this.isSubmitOnGoing &&
this.isRobotNameValid &&
!this.checkOnGoing
);
}
get shouldDisable(): boolean {
if (this.robot && this.robot.access) {
return (
!this.isValid() ||
(!this.robot.access.isPushOrPullImage && !this.robot.access.isPullImage
&& !this.robot.access.isPullChart && !this.robot.access.isPushChart)
);
canAdd(): boolean {
let flag = false;
this.defaultAccesses.forEach( item => {
if (item.checked) {
flag = true;
}
});
if (!flag) {
return false;
}
return !this.robotForm.invalid;
}
// Handle the form validation
handleValidation(): void {
let cont = this.currentForm.controls["robot_name"];
if (cont) {
this.robotNameChecker.next(cont.value);
canEdit() {
if (!this.canAdd()) {
return false;
}
}
onCpError($event: any): void {
if (this.copyAlert) {
this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR");
// tslint:disable-next-line:triple-equals
if (this.systemRobot.duration != this.originalRobotForEdit.duration) {
return true;
}
// tslint:disable-next-line:triple-equals
if (this.systemRobot.description != this.originalRobotForEdit.description) {
return true;
}
if (this.getAccessNum(this.defaultAccesses) !== this.getAccessNum(this.defaultAccessesForEdit)) {
return true;
}
let flag = true;
this.defaultAccessesForEdit.forEach(item => {
this.defaultAccesses.forEach(item2 => {
if (item.resource === item2.resource && item.action === item2.action && item.checked !== item2.checked) {
flag = false;
}
});
});
return !flag;
}
onCpSuccess($event: any): void {
this.copyToken = false;
this.translate
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
.subscribe((res: string) => {
this.messageHandlerService.showSuccess(res);
save() {
this.saveBtnState = ClrLoadingState.LOADING;
const robot: Robot = clone(this.systemRobot);
robot.disable = false;
robot.level = PermissionsKinds.PROJECT;
robot.duration = +this.systemRobot.duration;
const access: Access[] = [];
this.defaultAccesses.forEach(item => {
if (item.checked) {
access.push({
resource: item.resource,
action: item.action
});
}
closeModal() {
this.copyToken = false;
}
switch() {
if (this.isNeverExpired) {
this.expiresDate = null;
this.translate.get('ROBOT_ACCOUNT.NEVER_EXPIRED').subscribe(value => {
this.expiresDatePlaceholder = value;
}
});
robot.permissions = [{
namespace: this.projectName,
kind: PermissionsKinds.PROJECT,
access: access
}];
if (this.isEditMode) {
robot.disable = this.systemRobot.disable;
const opeMessage = new OperateInfo();
opeMessage.name = "SYSTEM_ROBOT.UPDATE_ROBOT";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.UpdateRobot({
robotId: this.originalRobotForEdit.id,
robot
}).subscribe( res => {
this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(null);
this.addRobotOpened = false;
operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess("SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY");
}, error => {
this.saveBtnState = ClrLoadingState.ERROR;
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
this.inlineAlertComponent.showInlineError(error);
});
} else {
this.expiresDatePlaceholder = ' ';
const opeMessage = new OperateInfo();
opeMessage.name = "SYSTEM_ROBOT.ADD_ROBOT";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.CreateRobot({
robot: robot
}).subscribe( res => {
this.saveBtnState = ClrLoadingState.SUCCESS;
this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(res);
this.addRobotOpened = false;
operateChanges(opeMessage, OperationState.success);
}, error => {
this.saveBtnState = ClrLoadingState.ERROR;
this.inlineAlertComponent.showInlineError(error);
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
});
}
}
get expiresDate(): Date {
return this._expiresDate;
}
set expiresDate(date: Date) {
if (date) {
this.isNeverExpired = false;
}
this._expiresDate = date;
getAccessNum(access: FrontAccess[]): number {
let count: number = 0;
access.forEach(item => {
if (item.checked) {
count ++;
}
});
return count;
}
}

View File

@ -4,9 +4,8 @@
<div class="flex-xs-middle option-left">
</div>
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"ROBOT_ACCOUNT.FILTER_PLACEHOLDER" | translate}}'
(filterEvt)="doSearch($event)" [currentValue]="searchRobot"></hbr-filter>
<span class="refresh-btn" (click)="retrieve()">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"ROBOT_ACCOUNT.FILTER_PLACEHOLDER" | translate}}'></hbr-filter>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
@ -14,10 +13,10 @@
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar>
<button class="btn btn-secondary" [disabled]="!hasRobotCreatePermission" (click)="openAddRobotModal()">
<button [disabled]="!hasRobotCreatePermission" [clrLoading]="addBtnState" class="btn btn-secondary" (click)="openNewRobotModal(false)">
<span>
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'ROBOT_ACCOUNT.NEW_ROBOT_ACCOUNT'
| translate }}
| translate }}
</span>
</button>
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="btn
@ -25,48 +24,86 @@
<span>{{'MEMBER.ACTION' | translate}}<clr-icon shape="caret
down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<button clrDropdownItem [disabled]="!hasRobotUpdatePermission||!(selectedRow.length ==
1)" (click)="changeAccountStatus(selectedRow)">
<span *ngIf="selectedRow[0] && !selectedRow[0].disabled
|| selectedRow.length!==1">{{'ROBOT_ACCOUNT.DISABLE_ACCOUNT'
| translate}}</span>
<span *ngIf="selectedRow.length == 1 && selectedRow[0]
&& selectedRow[0].disabled">{{'ROBOT_ACCOUNT.ENABLE_ACCOUNT'
| translate}}</span>
<button [disabled]="!hasRobotReadPermission || !(selectedRows && selectedRows.length === 1) || !selectedRows[0].editable" clrDropdownItem (click)="openTokenModal()">
<clr-icon shape="details" size="16"></clr-icon>&nbsp;
<span id="system-robot-token">{{"SYSTEM_ROBOT.VIEW_SECRET" | translate}}</span>
</button>
<button [disabled]="!hasRobotUpdatePermission || !(selectedRows && selectedRows.length === 1) || !selectedRows[0].editable" clrDropdownItem (click)="openNewRobotModal(true)">
<clr-icon shape="edit" size="16"></clr-icon>&nbsp;
<span id="system-robot-edit">{{'BUTTON.EDIT' | translate}}</span>
</button>
<button *ngIf="selectedRows && selectedRows.length === 1 && selectedRows[0].disable" type="button" class="btn btn-secondary"
(click)="disableOrEnable()"
[disabled]="!hasRobotUpdatePermission ||!(selectedRows && selectedRows.length === 1 && selectedRows[0].disable) || !selectedRows[0].editable">
<clr-icon size="16" shape="success-standard"></clr-icon>&nbsp;
<span id="distribution-enable">{{'WEBHOOK.ENABLED_BUTTON' | translate}}</span>
</button>
<button *ngIf="!(selectedRows && selectedRows.length === 1 && selectedRows[0].disable)"
type="button"
class="btn btn-secondary"
(click)="disableOrEnable()"
[disabled]="!hasRobotUpdatePermission ||!(selectedRows && selectedRows.length === 1 && !selectedRows[0].disable) || !selectedRows[0].editable">
<clr-icon size="16" shape="ban"></clr-icon>&nbsp;
<span id="distribution-disable">{{'WEBHOOK.DISABLED_BUTTON' | translate}}</span>
</button>
<div class="dropdown-divider"></div>
<button clrDropdownItem (click)="openDeleteRobotsDialog(selectedRow)" [disabled]="!hasRobotDeletePermission || !selectedRow.length">{{'ROBOT_ACCOUNT.DELETE'
| translate}}</button>
<button [disabled]="!hasRobotDeletePermission || !(selectedRows && selectedRows.length >=1)" clrDropdownItem (click)="openDeleteRobotsDialog()">
<clr-icon shape="window-close" size="16"></clr-icon>&nbsp;
<span id="system-robot-delete">{{'BUTTON.DELETE' | translate}}</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-datagrid [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRows">
<clr-dg-column>{{'ROBOT_ACCOUNT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PERMISSION_COLUMN" | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.CREATETION' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.EXPIRATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRED_AT' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{
'SYSTEM_ROBOT.NOT_FOUND' | translate
}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let r of robots" [clrDgItem]="r">
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="r.disabled">
<clr-dg-cell>
{{r.name}}
<span *ngIf="!r.editable" class="label label-warning ml-1">{{'SYSTEM_ROBOT.LEGACY' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="r.disable">
<clr-icon shape="check-circle" *ngSwitchCase="false" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="true" size="16" class="color-red red-position"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{r.description}}</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown [clrCloseMenuOnItemClick]="false" *ngIf="r.permissions[0]?.access?.length">
<button class="btn btn-link" clrDropdownTrigger>
{{r.permissions[0]?.access?.length}} {{"SYSTEM_ROBOT.PERMISSIONS" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div clrDropdownItem *ngFor="let item of r.permissions[0]?.access">
<span>{{i18nMap[item.action] | translate}} {{i18nMap[item.resource] | translate}}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</clr-dg-cell>
<clr-dg-cell>{{r.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{r.expires_at === -1?("ROBOT_ACCOUNT.NEVER_EXPIRED" | translate):(r.expires_at * 1000 | date: 'short')}}</clr-dg-cell>
<clr-dg-cell>{{r.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="15">
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="total">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="robots?.length">{{pagination.firstItem + 1}}
<span *ngIf="total">{{pagination.firstItem + 1}}
-
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{robots?.length}} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
{{total}} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<add-robot [projectId]="projectId" [projectName]="projectName" (create)="createAccount($event)"></add-robot>
</div>
<add-robot [projectId]="projectId" [projectName]="projectName" (addSuccess)="addSuccess($event)"></add-robot>
<view-token (refreshSuccess)="refresh()"></view-token>

View File

@ -26,3 +26,11 @@
}
}
}
.permissions {
height: 16px;
display: flex;
align-items: center;
}
.datagrid-host {
position: inherit;
}

View File

@ -1,63 +1,135 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ClarityModule } from '@clr/angular';
import { of } from 'rxjs';
import { ActivatedRoute, Router } from "@angular/router";
import { of, Subscription } from 'rxjs';
import { ActivatedRoute } from "@angular/router";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { RobotService } from "./robot-account.service";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { TranslateModule } from '@ngx-translate/core';
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import { RobotAccountComponent } from './robot-account.component';
import { UserPermissionService } from "../../../lib/services";
import { ErrorHandler } from "../../../lib/utils/error-handler";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { delay } from "rxjs/operators";
import { Action, PermissionsKinds, Resource } from "../../system-robot-accounts/system-robot-util";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientTestingModule } from "@angular/common/http/testing";
describe('RobotAccountComponent', () => {
let component: RobotAccountComponent;
let fixture: ComponentFixture<RobotAccountComponent>;
let robotService = {
listRobotAccount: function () {
return of([]);
const robot1: Robot = {
id: 1,
name: 'robot1',
level: PermissionsKinds.PROJECT,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'tthf54hfth4545dfgd5g454grd54gd54g',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project1',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const robot2: Robot = {
id: 2,
name: 'robot2',
level: PermissionsKinds.PROJECT,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'fsdf454654654fs6dfe',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project2',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const robot3: Robot = {
id: 3,
name: 'robot3',
level: PermissionsKinds.PROJECT,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'fsdg48454fse84',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project3',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const mockUserPermissionService = {
getPermission() {
return of(true);
}
};
let mockConfirmationDialogService = null;
let mockUserPermissionService = {
getPermission: function () {
return 1;
const fakedRobotService = {
ListRobotResponse() {
const res: HttpResponse<Array<Robot>> = new HttpResponse<Array<Robot>>({
headers: new HttpHeaders({'x-total-count': '3'}),
body: [robot1, robot2, robot3]
});
return of(res).pipe(delay(0));
}
};
let mockErrorHandler = {
error: function () { }
const fakedMessageHandlerService = {
showSuccess() {
},
error() {
}
};
let mockMessageHandlerService = null;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [
CUSTOM_ELEMENTS_SCHEMA
NO_ERRORS_SCHEMA
],
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot()
TranslateModule.forRoot(),
HttpClientTestingModule
],
providers: [
{
provide: ActivatedRoute, useValue: {
paramMap: of({ get: (key) => 'value' }),
snapshot: {
parent: {
params: { id: 1 }
params: { id: 1 },
data: null
},
data: 1
}
}
},
TranslateService,
{ provide: RobotService, useValue: robotService },
{ provide: ConfirmationDialogService, useClass: ConfirmationDialogService },
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
ConfirmationDialogService,
OperationService,
{ provide: UserPermissionService, useValue: mockUserPermissionService },
{ provide: ErrorHandler, useValue: mockErrorHandler },
{ provide: MessageHandlerService, useValue: mockMessageHandlerService },
OperationService
{ provide: RobotService, useValue: fakedRobotService},
],
declarations: [RobotAccountComponent]
}).compileComponents();
@ -66,10 +138,17 @@ describe('RobotAccountComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(RobotAccountComponent);
component = fixture.componentInstance;
component.searchSub = new Subscription();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render project robot list', async () => {
fixture.autoDetectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(3);
});
});

View File

@ -1,31 +1,30 @@
import {
Component,
OnInit,
ViewChild,
OnDestroy,
ChangeDetectorRef
} from "@angular/core";
import { AddRobotComponent } from "./add-robot/add-robot.component";
import { ActivatedRoute, Router } from "@angular/router";
import { Robot } from "./robot";
import { Project } from "./../project";
import { finalize, catchError, map } from "rxjs/operators";
import { TranslateService } from "@ngx-translate/core";
import { Subscription, forkJoin, Observable, throwError as observableThrowError } from "rxjs";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { RobotService } from "./robot-account.service";
import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message";
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import {
ConfirmationTargets,
ConfirmationState,
ConfirmationButtons
} from "../../shared/shared.const";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ClrDatagridStateInterface, ClrLoadingState } from "@clr/angular";
import { catchError, debounceTime, distinctUntilChanged, finalize, map, switchMap } from "rxjs/operators";
import { forkJoin, Observable, of, Subscription } from "rxjs";
import { UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services";
import { ErrorHandler } from "../../../lib/utils/error-handler";
import {
ACTION_RESOURCE_I18N_MAP,
PermissionsKinds
} from "../../system-robot-accounts/system-robot-util";
import { clone, DEFAULT_PAGE_SIZE } from "../../../lib/utils/utils";
import { ViewTokenComponent } from "../../system-robot-accounts/view-token/view-token.component";
import { FilterComponent } from "../../../lib/components/filter/filter.component";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../shared/shared.const";
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { ActivatedRoute } from "@angular/router";
import { Project } from "../../../../ng-swagger-gen/models/project";
import { HttpErrorResponse } from "@angular/common/http";
import { errorHandler } from "../../../lib/utils/shared/shared.utils";
import { operateChanges, OperateInfo, OperationState } from "../../../lib/components/operation/operate";
import { errorHandler as errorHandlerFn } from "../../../lib/utils/shared/shared.utils";
import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message";
import { AddRobotComponent } from "./add-robot/add-robot.component";
import { TranslateService } from "@ngx-translate/core";
import { DomSanitizer } from "@angular/platform-browser";
@Component({
selector: "app-robot-account",
@ -33,197 +32,267 @@ import { errorHandler as errorHandlerFn } from "../../../lib/utils/shared/shared
styleUrls: ["./robot-account.component.scss"]
})
export class RobotAccountComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 1;
total: number = 0;
robots: Robot[] = [];
selectedRows: Robot[] = [];
loading: boolean = true;
addBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
@ViewChild(AddRobotComponent)
addRobotComponent: AddRobotComponent;
selectedRow: Robot[] = [];
robotsCopy: Robot[] = [];
loading = false;
searchRobot: string;
projectName: string;
timerHandler: any;
batchChangeInfos: {};
isDisabled: boolean;
isDisabledTip: string = "ROBOT_ACCOUNT.DISABLE_ACCOUNT";
robots: Robot[];
projectId: number;
newRobotComponent: AddRobotComponent;
@ViewChild(ViewTokenComponent)
viewTokenComponent: ViewTokenComponent;
@ViewChild(FilterComponent, {static: true})
filterComponent: FilterComponent;
searchSub: Subscription;
searchKey: string;
subscription: Subscription;
hasRobotCreatePermission: boolean;
hasRobotUpdatePermission: boolean;
hasRobotDeletePermission: boolean;
constructor(
private route: ActivatedRoute,
private robotService: RobotService,
private OperateDialogService: ConfirmationDialogService,
private operationService: OperationService,
private translate: TranslateService,
private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler,
private ref: ChangeDetectorRef,
private messageHandlerService: MessageHandlerService
hasRobotReadPermission: boolean;
projectId: number;
projectName: string;
constructor(private robotService: RobotService,
private msgHandler: MessageHandlerService,
private operateDialogService: ConfirmationDialogService,
private operationService: OperationService,
private userPermissionService: UserPermissionService,
private route: ActivatedRoute,
private translate: TranslateService,
private sanitizer: DomSanitizer,
) {
this.subscription = OperateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT
) {
this.delRobots(message.data);
this.subscription = operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT
) {
this.deleteRobots(message.data);
}
if ( message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT_ENABLE_OR_DISABLE) {
this.operateRobot();
}
}
}
);
this.forceRefreshView(2000);
}
ngOnInit(): void {
ngOnInit() {
this.projectId = +this.route.snapshot.parent.params["id"];
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
let project = <Project>resolverData["projectResolver"];
this.projectName = project.name;
}
this.searchRobot = "";
this.retrieve();
this.getPermissionsList(this.projectId);
this.getPermissionsList();
if (!this.searchSub) {
this.searchSub = this.filterComponent.filterTerms.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(robotSearchName => {
this.currentPage = 1;
this.selectedRows = [];
const queryParam: RobotService.ListRobotParams = {
page: this.currentPage,
pageSize: this.pageSize,
};
this.searchKey = robotSearchName;
if (this.searchKey) {
queryParam.q = encodeURIComponent(`Level=${PermissionsKinds.PROJECT},ProjectID=${this.projectId},name=~${this.searchKey}`);
}
this.loading = true;
return this.robotService.ListRobotResponse(queryParam)
.pipe(finalize(() => {
this.loading = false;
}));
})).subscribe(response => {
this.total = Number.parseInt(
response.headers.get('x-total-count')
);
this.robots = response.body as Robot[];
}, error => {
this.msgHandler.handleError(error);
});
}
}
getPermissionsList(projectId: number): void {
getPermissionsList(): void {
let permissionsList = [];
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.CREATE));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.UPDATE));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.DELETE));
permissionsList.push(this.userPermissionService.getPermission(this.projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.CREATE));
permissionsList.push(this.userPermissionService.getPermission(this.projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.UPDATE));
permissionsList.push(this.userPermissionService.getPermission(this.projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.DELETE));
permissionsList.push(this.userPermissionService.getPermission(this.projectId,
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.READ));
forkJoin(...permissionsList).subscribe(Rules => {
this.hasRobotCreatePermission = Rules[0] as boolean;
this.hasRobotUpdatePermission = Rules[1] as boolean;
this.hasRobotDeletePermission = Rules[2] as boolean;
}, error => this.errorHandler.error(error));
this.hasRobotReadPermission = Rules[3] as boolean;
}, error => this.msgHandler.error(error));
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
ngOnDestroy() {
if (this.searchSub) {
this.searchSub.unsubscribe();
this.searchSub = null;
}
}
openAddRobotModal(): void {
this.addRobotComponent.openAddRobotModal();
}
openDeleteRobotsDialog(robots: Robot[]) {
let robotNames = robots.map(robot => robot.name).join(",");
let deletionMessage = new ConfirmationMessage(
"ROBOT_ACCOUNT.DELETION_TITLE",
"ROBOT_ACCOUNT.DELETION_SUMMARY",
robotNames,
robots,
ConfirmationTargets.ROBOT_ACCOUNT,
ConfirmationButtons.DELETE_CANCEL
);
this.OperateDialogService.openComfirmDialog(deletionMessage);
}
delRobots(robots: Robot[]): void {
if (robots && robots.length < 1) {
return;
clrLoad(state?: ClrDatagridStateInterface) {
if (state && state.page && state.page.size) {
this.pageSize = state.page.size;
}
let robotsDelete$ = robots.map(robot => this.delOperate(robot));
forkJoin(robotsDelete$)
.pipe(
finalize(() => {
this.retrieve();
this.selectedRow = [];
})
)
.subscribe(() => { }
, error => {
this.errorHandler.error(error);
this.selectedRows = [];
const queryParam: RobotService.ListRobotParams = {
page: this.currentPage,
pageSize: this.pageSize,
q: encodeURIComponent(`Level=${PermissionsKinds.PROJECT},ProjectID=${this.projectId}`)
};
if (this.searchKey) {
queryParam.q += encodeURIComponent(`,name=~${this.searchKey}`);
}
this.loading = true;
this.robotService.ListRobotResponse(queryParam)
.pipe(finalize(() => this.loading = false))
.subscribe(
response => {
this.total = Number.parseInt(
response.headers.get('x-total-count')
);
this.robots = response.body as Robot[];
},
err => {
this.msgHandler.error(err);
});
}
openNewRobotModal(isEditMode: boolean) {
if (isEditMode) {
this.newRobotComponent.resetForEdit(clone(this.selectedRows[0]));
} else {
this.newRobotComponent.reset();
}
}
openTokenModal() {
this.viewTokenComponent.open();
this.viewTokenComponent.robot = clone(this.selectedRows[0]);
}
refresh() {
this.currentPage = 1;
this.selectedRows = [];
this.clrLoad();
}
deleteRobots(robots: Robot[]) {
let observableLists: Observable<any>[] = [];
if (robots && robots.length) {
robots.forEach(item => {
observableLists.push(this.deleteRobot(item));
});
forkJoin(...observableLists).subscribe(resArr => {
let error;
if (resArr && resArr.length) {
resArr.forEach(item => {
if (item instanceof HttpErrorResponse) {
error = errorHandler(item);
}
});
}
if (error) {
this.msgHandler.handleError(error);
} else {
this.msgHandler.showSuccess('SYSTEM_ROBOT.DELETE_ROBOT_SUCCESS');
}
this.refresh();
});
}
}
delOperate(robot: Robot) {
// init operation info
deleteRobot(robot: Robot): Observable<any> {
let operMessage = new OperateInfo();
operMessage.name = "OPERATION.DELETE_ROBOT";
operMessage.name = 'SYSTEM_ROBOT.DELETE_ROBOT';
operMessage.data.id = robot.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = robot.name;
this.operationService.publishInfo(operMessage);
return this.robotService
.deleteRobotAccount(this.projectId, robot.id)
.pipe(
map(
() => operateChanges(operMessage, OperationState.success),
catchError(error => {
const errorMsg = errorHandlerFn(error);
this.translate.get(errorMsg).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return observableThrowError(error);
}
)
));
return this.robotService.DeleteRobot({robotId: robot.id}).pipe(
map(() => {
operateChanges(operMessage, OperationState.success);
}),
catchError(error => {
const message = errorHandler(error);
operateChanges(operMessage, OperationState.failure, message);
return of(error);
})
);
}
openDeleteRobotsDialog() {
const robotNames = this.selectedRows.map(robot => robot.name).join(",");
const deletionMessage = new ConfirmationMessage(
"ROBOT_ACCOUNT.DELETION_TITLE",
"ROBOT_ACCOUNT.DELETION_SUMMARY",
robotNames,
this.selectedRows,
ConfirmationTargets.ROBOT_ACCOUNT,
ConfirmationButtons.DELETE_CANCEL
);
this.operateDialogService.openComfirmDialog(deletionMessage);
}
createAccount(created: boolean): void {
if (created) {
this.retrieve();
disableOrEnable() {
const title: string = this.selectedRows[0].disable ? "SYSTEM_ROBOT.ENABLE_TITLE" : "SYSTEM_ROBOT.DISABLE_TITLE";
const summary: string = this.selectedRows[0].disable ? "SYSTEM_ROBOT.ENABLE_SUMMARY" : "SYSTEM_ROBOT.DISABLE_SUMMARY";
const deletionMessage = new ConfirmationMessage(
title,
summary,
this.selectedRows[0].name,
this.selectedRows[0],
ConfirmationTargets.ROBOT_ACCOUNT_ENABLE_OR_DISABLE,
this.selectedRows[0].disable ? ConfirmationButtons.ENABLE_CANCEL : ConfirmationButtons.DISABLE_CANCEL
);
this.operateDialogService.openComfirmDialog(deletionMessage);
}
operateRobot() {
const robot: Robot = clone(this.selectedRows[0]);
const successMessage: string = robot.disable ? "SYSTEM_ROBOT.ENABLE_ROBOT_SUCCESSFULLY" : "SYSTEM_ROBOT.DISABLE_ROBOT_SUCCESSFULLY";
robot.disable = !robot.disable;
delete robot.secret;
const opeMessage = new OperateInfo();
opeMessage.name = robot.disable ? "SYSTEM_ROBOT.DISABLE_TITLE" : "SYSTEM_ROBOT.ENABLE_TITLE";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.UpdateRobot({
robot: robot,
robotId: robot.id
}).subscribe( res => {
operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess(successMessage);
this.refresh();
}, error => {
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
this.msgHandler.showSuccess(error);
});
}
addSuccess(robot: Robot) {
if (robot) {
this.viewTokenComponent.open();
this.viewTokenComponent.tokenModalOpened = false;
this.viewTokenComponent.robot = clone(robot);
this.viewTokenComponent.copyToken = true;
this.translate
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: robot.name })
.subscribe((res: string) => {
this.viewTokenComponent.createSuccess = res;
});
// export to token file
const downLoadUrl = `data:text/json;charset=utf-8, ${encodeURIComponent(JSON.stringify(robot))}`;
this.viewTokenComponent.downLoadHref = this.sanitizer.bypassSecurityTrustUrl(downLoadUrl);
this.viewTokenComponent.downLoadFileName = `${robot.name}.json`;
}
}
forceRefreshView(duration: number): void {
// Reset timer
if (this.timerHandler) {
clearInterval(this.timerHandler);
}
this.timerHandler = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => {
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
}
}, duration);
}
doSearch(value: string): void {
this.searchRobot = value;
this.retrieve();
}
retrieve(): void {
this.loading = true;
this.selectedRow = [];
this.robotService
.listRobotAccount(this.projectId)
.pipe(finalize(() => (this.loading = false)))
.subscribe(
response => {
this.robots = response.filter(x =>
x.name.split('$')[1].includes(this.searchRobot)
);
this.robotsCopy = response.map(x => Object.assign({}, x));
this.forceRefreshView(2000);
},
error => {
this.messageHandlerService.handleError(error);
}
);
}
changeAccountStatus(robots: Robot): void {
let id: number | string = robots[0].id;
this.isDisabled = robots[0].disabled ? false : true;
this.robotService
.toggleDisabledAccount(this.projectId, id, this.isDisabled)
.subscribe(response => {
this.retrieve();
});
this.refresh();
}
}

View File

@ -1,19 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { RobotService } from './robot-account.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RobotApiRepository } from "./robot.api.repository";
describe('RobotService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [RobotService, RobotApiRepository]
});
});
it('should be created', inject([RobotService], (service: RobotService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -1,76 +0,0 @@
import { throwError as observableThrowError, Observable } from "rxjs";
import { map, catchError } from "rxjs/operators";
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { RobotApiRepository } from "./robot.api.repository";
import { Robot } from "./robot";
@Injectable()
export class RobotService {
constructor(
private http: HttpClient,
private robotApiRepository: RobotApiRepository
) { }
/** addRobotAccount
* projectId
* robot: Robot
* projectName
*/
public addRobotAccount(projectId: number, robot: Robot, projectName: string): Observable<any> {
let access = [];
if (robot.access.isPullImage) {
access.push({ "resource": `/project/${projectId}/repository`, "action": "pull" });
}
if (robot.access.isPushOrPullImage) {
access.push({ "resource": `/project/${projectId}/repository`, "action": "push" });
}
if (robot.access.isPullChart) {
access.push({ "resource": `/project/${projectId}/helm-chart`, "action": "read" });
}
if (robot.access.isPushChart) {
access.push({ "resource": `/project/${projectId}/helm-chart-version`, "action": "create" });
}
let param = {
name: robot.name,
expires_at: +robot.expires_at,
description: robot.description,
access
};
return this.robotApiRepository.postRobot(projectId, param);
}
public deleteRobotAccount(projectId, id): Observable<any> {
return this.robotApiRepository.deleteRobot(projectId, id);
}
public listRobotAccount(projectId): Observable<any> {
return this.robotApiRepository.listRobot(projectId);
}
public getRobotAccount(projectId, id): Observable<any> {
return this.robotApiRepository.getRobot(projectId, id);
}
public toggleDisabledAccount(projectId, id, isDisabled): Observable<any> {
let data = {
Disabled: isDisabled
};
return this.robotApiRepository.toggleDisabledAccount(projectId, id, data);
}
}

View File

@ -1,43 +0,0 @@
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { throwError as observableThrowError, Observable, pipe } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { Robot } from './robot';
import { CURRENT_BASE_HREF } from "../../../lib/utils/utils";
@Injectable()
export class RobotApiRepository {
constructor(private http: HttpClient) {}
public postRobot(projectId, param): Observable<any> {
return this.http
.post(`${ CURRENT_BASE_HREF }/projects/${projectId}/robots`, param)
.pipe(catchError(error => observableThrowError(error)));
}
public deleteRobot(projectId, id): Observable<any> {
return this.http
.delete(`${ CURRENT_BASE_HREF }/projects/${projectId}/robots/${id}`)
.pipe(catchError(error => observableThrowError(error)));
}
public listRobot(projectId): Observable<Robot[]> {
return this.http
.get(`${ CURRENT_BASE_HREF }/projects/${projectId}/robots`)
.pipe(map(response => response as Robot[]))
.pipe(catchError(error => observableThrowError(error)));
}
public getRobot(projectId, id): Observable<Robot[]> {
return this.http
.get(`${ CURRENT_BASE_HREF }/projects/${projectId}/robots/${id}`)
.pipe(map(response => response as Robot[]))
.pipe(catchError(error => observableThrowError(error)));
}
public toggleDisabledAccount(projectId, id, data): Observable<any> {
return this.http
.put(`${ CURRENT_BASE_HREF }/projects/${projectId}/robots/${id}`, data)
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -1,25 +0,0 @@
export class Robot {
project_id: number;
id: number;
name: string;
description: string;
expires_at: number;
disabled: boolean;
creation_time?: Date;
access: {
isPullImage: boolean;
isPushOrPullImage: boolean;
isPushChart: boolean;
isPullChart: boolean;
};
constructor () {
this.access = <any>{};
this.access.isPullImage = false;
this.access.isPushOrPullImage = true;
this.access.isPushChart = true;
this.access.isPullChart = true;
}
}

View File

@ -49,7 +49,8 @@ export const enum ConfirmationTargets {
P2P_PROVIDER,
P2P_PROVIDER_EXECUTE,
P2P_PROVIDER_STOP,
P2P_PROVIDER_DELETE
P2P_PROVIDER_DELETE,
ROBOT_ACCOUNT_ENABLE_OR_DISABLE
}
export const enum ActionType {

View File

@ -47,6 +47,7 @@ import { IServiceConfig, SERVICE_CONFIG } from "../../lib/entities/service.confi
import { ErrorHandler } from "../../lib/utils/error-handler";
import { HarborLibraryModule } from "../../lib/harbor-library.module";
import { CURRENT_BASE_HREF, V1_BASE_HREF } from "../../lib/utils/utils";
import { ViewTokenComponent } from "../system-robot-accounts/view-token/view-token.component";
const uiLibConfig: IServiceConfig = {
enablei18Support: true,
@ -102,7 +103,8 @@ const uiLibConfig: IServiceConfig = {
ListRepositoryROComponent,
GaugeComponent,
DateValidatorDirective,
ListChartVersionRoComponent
ListChartVersionRoComponent,
ViewTokenComponent,
],
exports: [
CoreModule,
@ -124,7 +126,8 @@ const uiLibConfig: IServiceConfig = {
DateValidatorDirective,
FormsModule,
ReactiveFormsModule,
ListChartVersionRoComponent
ListChartVersionRoComponent,
ViewTokenComponent,
],
providers: [
SessionService,

View File

@ -0,0 +1,54 @@
<clr-datagrid [clrDgPreserveSelection]="true" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button [disabled]="coverAll" class="btn btn-secondary btn-sm" clrDropdownTrigger>
{{"SYSTEM_ROBOT.RESET_PERMISSION" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<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 (click)="selectAllOrUnselectAll()" [disabled]="coverAll" class="btn btn-secondary btn-sm ml-1">
<span *ngIf="showSelectAll">{{"SYSTEM_ROBOT.SELECT_ALL" | translate}}</span>
<span *ngIf="!showSelectAll">{{"SYSTEM_ROBOT.UNSELECT_ALL" | translate}}</span>
</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'" [(clrFilterValue)]="myNameFilterValue">{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PERMISSION_COLUMN" | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of projects; let projectIndex = index;" [clrDgItem]="p">
<clr-dg-cell>
<a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a>
</clr-dg-cell>
<clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button [disabled]="coverAll" class="btn btn-link" clrDropdownTrigger>
{{getPermissions(p.permissions[0].access)}} {{"SYSTEM_ROBOT.PERMISSIONS" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<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-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize">
<clr-dg-page-size [clrPageSizeOptions]="[5,15,25]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span>{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{projects?.length
}} {{'PROJECT.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,12 @@
.check {
margin-right: 5px;
color: green;
}
.permissions {
height: 16px;
display: flex;
align-items: center;
}
.datagrid-host {
position: inherit;
}

View File

@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ListAllProjectsComponent } from './list-all-projects.component';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { RouterTestingModule } from "@angular/router/testing";
import { TranslateModule } from "@ngx-translate/core";
import { clone } from "../../../lib/utils/utils";
import { INITIAL_ACCESSES } from "../system-robot-util";
import { Project } from "../../../../ng-swagger-gen/models/project";
describe('ListAllProjectsComponent', () => {
let component: ListAllProjectsComponent;
let fixture: ComponentFixture<ListAllProjectsComponent>;
const project1: Project = {
project_id: 1,
name: 'project1'
};
const project2: Project = {
project_id: 2,
name: 'project2'
};
const project3: Project = {
project_id: 3,
name: 'project3'
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
RouterTestingModule,
TranslateModule.forRoot(),
],
declarations: [ ListAllProjectsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ListAllProjectsComponent);
component = fixture.componentInstance;
component.defaultAccesses = clone(INITIAL_ACCESSES);
component.cachedAllProjects = [project1, project2, project3];
component.init(false);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render list', async () => {
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(3);
});
});

View File

@ -0,0 +1,110 @@
import { Component, Input, OnInit, } from '@angular/core';
import { Project } from "../../../../ng-swagger-gen/models/project";
import { clone, CustomComparator } from "../../../lib/utils/utils";
import { ClrDatagridComparatorInterface } from "@clr/angular";
import { Router } from "@angular/router";
import { Permission } from "../../../../ng-swagger-gen/models/permission";
import {
ACTION_RESOURCE_I18N_MAP,
FrontAccess,
FrontProjectForAdd, INITIAL_ACCESSES,
PermissionsKinds
} from "../system-robot-util";
@Component({
selector: 'app-list-all-projects',
templateUrl: './list-all-projects.component.html',
styleUrls: ['./list-all-projects.component.scss']
})
export class ListAllProjectsComponent implements OnInit {
cachedAllProjects: Project[];
i18nMap = ACTION_RESOURCE_I18N_MAP;
permissionsForAdd: Permission[] = [];
selectedRow: FrontProjectForAdd[] = [];
timeComparator: ClrDatagridComparatorInterface<Project> = new CustomComparator<Project>("creation_time", "date");
projects: FrontProjectForAdd[] = [];
pageSize: number = 5;
currentPage: number = 1;
defaultAccesses: FrontAccess[] = [];
@Input()
coverAll: boolean = false;
showSelectAll: boolean = true;
myNameFilterValue: string;
constructor(private router: Router) {
}
ngOnInit(): void {
}
init(isEdit: boolean) {
this.pageSize = 5;
this.currentPage = 1;
this.showSelectAll = true;
this.myNameFilterValue = null;
if (isEdit) {
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.defaultAccesses.forEach( item => item.checked = false);
} else {
this.defaultAccesses = clone(INITIAL_ACCESSES);
}
if (this.cachedAllProjects && this.cachedAllProjects.length) {
this.projects = clone(this.cachedAllProjects);
this.resetAccess(this.defaultAccesses);
} else {
this.projects = [];
}
}
resetAccess(accesses: FrontAccess[]) {
if (this.projects && this.projects.length) {
this.projects.forEach(item => {
item.permissions = [{
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;
}
goToLink(proId: number): void {
this.router.navigate(["harbor", "projects", proId]);
}
selectAllOrUnselectAll() {
if (this.showSelectAll) {
if (this.myNameFilterValue) {
this.projects.forEach(item => {
let flag = false;
if (item.name.indexOf(this.myNameFilterValue) !== -1) {
this.selectedRow.forEach(item2 => {
if (item2.name === item.name) {
flag = true;
}
});
if (!flag) {
this.selectedRow.push(item);
}
}
});
} else {
this.selectedRow = this.projects;
}
} else {
this.selectedRow = [];
}
this.showSelectAll = !this.showSelectAll;
}
}

View File

@ -0,0 +1,129 @@
<clr-modal clrModalSize="lg" [(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<h3 *ngIf="!isEditMode" class="modal-title">{{"SYSTEM_ROBOT.CREATE_ROBOT" | translate}}</h3>
<h3 *ngIf="isEditMode" class="modal-title">{{"SYSTEM_ROBOT.EDIT_ROBOT" | translate}}</h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<p *ngIf="!isEditMode" class="mt-0">{{"SYSTEM_ROBOT.CREATE_ROBOT_SUMMARY" | translate}}</p>
<p *ngIf="isEditMode" class="mt-0">{{"SYSTEM_ROBOT.EDIT_ROBOT_SUMMARY" | translate}}</p>
<form #robotForm="ngForm" class="clr-form clr-form-horizontal mt-1">
<section class="form-block">
<!-- name -->
<div class="clr-form-control">
<label for="name" class="clr-control-label required">{{'P2P_PROVIDER.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="((name.dirty || name.touched) && name.invalid) || isNameExisting">
<div class="clr-input-wrapper">
<input class="clr-input input-width"
[disabled]="loading || isEditMode"
type="text" id="name"
[(ngModel)]="systemRobot.name"
required
pattern='[^" ~#$%]+'
maxLengthExt="255"
autocomplete="off"
size="30" name="name" #name="ngModel" (input)="inputName()">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkNameOnGoing"></span>
</div>
<clr-control-error *ngIf="((name.dirty || name.touched) && name.invalid) || isNameExisting">
<span *ngIf="!((name.dirty || name.touched) && name.invalid) && isNameExisting">{{'ROBOT_ACCOUNT.ACCOUNT_EXISTING' | translate}}</span>
<span *ngIf="(name.dirty || name.touched) && name.invalid">{{ 'ROBOT_ACCOUNT.ROBOT_NAME' | translate }}</span>
</clr-control-error>
</div>
</div>
<!-- expiration -->
<div class="clr-form-control">
<label class="clr-control-label required">{{"SYSTEM_ROBOT.EXPIRATION_TIME" | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{"SYSTEM_ROBOT.EXPIRATION_TIME_EXPLAIN" | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<div class="clr-control-container input-width flex" [class.clr-error]="((expiration.dirty || expiration.touched) && expiration.invalid) || isExpirationInvalid()">
<div class="clr-select-wrapper">
<select [ngModelOptions]="{standalone: true}" (change)="changeExpirationType()" [(ngModel)]="expirationType" id="expiration-type" class="clr-select">
<option value="default">{{systemExpirationDays}} {{"SYSTEM_ROBOT.EXPIRATION_DEFAULT" | translate}}</option>
<option value="days">{{"SYSTEM_ROBOT.EXPIRATION_DAYS" | translate}}</option>
<option value="never">{{"SYSTEM_ROBOT.EXPIRATION_NEVER" | translate}}</option>
</select>
</div>
<div class="clr-input-wrapper">
<input (input)="inputExpiration()" [disabled]="loadingSystemConfig" class="clr-input expiration-width" name="expiration" type="text"
#expiration="ngModel"
autocomplete="off"
[(ngModel)]="systemRobot.duration" required
pattern="^[\-1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20"/>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!loadingSystemConfig"></span>
<clr-control-error *ngIf="((expiration.dirty || expiration.touched) && expiration.invalid)|| isExpirationInvalid()">
{{"SYSTEM_ROBOT.EXPIRATION_REQUIRED" | translate}}
</clr-control-error>
</div>
</div>
</div>
<!-- 3. description -->
<clr-textarea-container class="mt-description">
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea class="input-width"
clrTextarea
type="text"
id="description"
name="description"
[(ngModel)]="systemRobot.description"
></textarea>
</clr-textarea-container>
<div class="clr-form-control">
<label class="clr-control-label mt-8px">{{"SYSTEM_ROBOT.COVER_ALL" | translate}}</label>
<div class="clr-control-container padding-top-3 flex">
<clr-checkbox-wrapper>
<input
clrCheckbox
id="coverAll"
name="coverAll"
type="checkbox"
[(ngModel)]="coverAll"
/>
<label>
<clr-tooltip>
<clr-icon class="opacity-07" clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{"SYSTEM_ROBOT.COVER_ALL_EXPLAIN" | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
</clr-checkbox-wrapper>
<clr-dropdown [style.visibility]="coverAll? 'visible' : 'hidden'" class="dropdown-per" [clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{getPermissions()}} {{"SYSTEM_ROBOT.PERMISSIONS" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<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>
<div class="clr-form-control" [class.clr-form-control-disabled]="coverAll" >
<app-list-all-projects [coverAll]="coverAll" [class.disabled]="coverAll" class="all-projects"></app-list-all-projects>
</div>
</form>
</div>
<div class="modal-footer">
<span class="message" [style.visibility]="coverAll? 'visible' : 'hidden'">{{"SYSTEM_ROBOT.COVER_ALL_SUMMARY" | translate}}</span>
<span>
<button (click)="cancel()" id="system-robot-cancel" type="button" class="btn btn-outline">{{'BUTTON.CANCEL'
| translate}}</button>
<button [disabled]="disabled() || checkNameOnGoing" [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

@ -0,0 +1,71 @@
.rule-width {
width: 100%;
}
.input-width {
width: 350px;
}
.padding-left-0 {
padding-left: 0;
}
.no-margin {
margin: 0;
}
.permission{
padding-top: 0.1rem;
color: #000000;
}
.padding-left-120{
padding-left: 126px;
}
.w-90{
width: 90%;
}
.date {
margin-top: -0.9rem;
}
.input-width-date {
width: 265px;
}
.mt-description {
margin-top: 2.5rem;
}
.width-table {
width: 388px;
}
.all-projects {
width: 100%;
}
.opacity-07 {
opacity: 0.7;
}
.disabled {
opacity: 0.5;
}
.modal-footer {
align-items: center;
justify-content: space-between;
}
.message {
margin-left: 10px;
}
.flex {
display: flex;
align-items: center;
justify-content: space-between;
}
.expiration-width {
width: 194px;
}
.check {
margin-right: 5px;
color: green;
}
.dropdown-per {
margin-top: -1px;
}
.mt-8px {
margin-top: 8px !important;
}

View File

@ -0,0 +1,136 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewRobotComponent } from './new-robot.component';
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { TranslateModule } from "@ngx-translate/core";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { Action, INITIAL_ACCESSES, PermissionsKinds, Resource } from "../system-robot-util";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { ConfigurationService } from "../../config/config.service";
import { Configuration } from "../../../lib/components/config/config";
import { FormsModule } from "@angular/forms";
import { clone } from "../../../lib/utils/utils";
describe('NewRobotComponent', () => {
let component: NewRobotComponent;
let fixture: ComponentFixture<NewRobotComponent>;
const robot1: Robot = {
id: 1,
name: 'robot1',
level: PermissionsKinds.SYSTEM,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'tthf54hfth4545dfgd5g454grd54gd54g',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project1',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const fakedRobotService = {
ListRobot() {
return of([]).pipe(delay(0));
}
};
const mockConfigurationService = {
getConfiguration() {
const config: Configuration = new Configuration();
config.robot_token_duration = {
value : 10000,
editable: true
};
return of(config).pipe(delay(0));
}
};
const fakedMessageHandlerService = {
showSuccess() {
},
error() {
}
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot(),
FormsModule
],
declarations: [ NewRobotComponent ],
providers: [
OperationService,
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
{ provide: RobotService, useValue: fakedRobotService},
{ provide: ConfigurationService, useValue: mockConfigurationService},
],
schemas: [
NO_ERRORS_SCHEMA
],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NewRobotComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show "name is required"', 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 = "";
nameInput.dispatchEvent(new Event('input'));
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
let el = fixture.nativeElement.querySelector('clr-control-error');
expect(el).toBeTruthy();
});
it('should be edit model', async () => {
fixture.autoDetectChanges();
component.isEditMode = true;
component.addRobotOpened = true;
component.defaultAccesses = clone(INITIAL_ACCESSES);
component.systemRobot = robot1;
await fixture.whenStable();
const nameInput = fixture.nativeElement.querySelector('#name');
expect(nameInput.value).toEqual('robot1');
});
it('should be valid', async () => {
fixture.autoDetectChanges();
component.isEditMode = false;
component.addRobotOpened = true;
component.defaultAccesses = clone(INITIAL_ACCESSES);
await fixture.whenStable();
const nameInput = fixture.nativeElement.querySelector('#name');
nameInput.value = "test";
nameInput.dispatchEvent(new Event('input'));
const expiration = fixture.nativeElement.querySelector('#robotTokenExpiration');
expiration.value = 10;
expiration.dispatchEvent(new Event('input'));
component.coverAll = true;
await fixture.whenStable();
expect(component.disabled()).toBeFalsy();
});
});

View File

@ -0,0 +1,423 @@
import {
Component,
EventEmitter,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { ConfigurationService } from "../../config/config.service";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { ListAllProjectsComponent } from "../list-all-projects/list-all-projects.component";
import { NgForm } from "@angular/forms";
import { debounceTime, distinctUntilChanged, filter, finalize, switchMap } from "rxjs/operators";
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { Access } from "../../../../ng-swagger-gen/models/access";
import {
ACTION_RESOURCE_I18N_MAP, ExpirationType,
FrontAccess, INITIAL_ACCESSES,
NAMESPACE_ALL_PROJECTS,
PermissionsKinds,
} from "../system-robot-util";
import { clone } from "../../../lib/utils/utils";
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { ClrLoadingState } from "@clr/angular";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { Subject, Subscription } from "rxjs";
import { operateChanges, OperateInfo, OperationState } from "../../../lib/components/operation/operate";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { errorHandler } from "../../../lib/utils/shared/shared.utils";
const MINUETS_ONE_DAY: number = 60 * 24;
@Component({
selector: 'new-robot',
templateUrl: './new-robot.component.html',
styleUrls: ['./new-robot.component.scss']
})
export class NewRobotComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
isEditMode: boolean = false;
originalRobotForEdit: Robot;
@Output()
addSuccess: EventEmitter<Robot> = new EventEmitter<Robot>();
addRobotOpened: boolean = false;
systemRobot: Robot = {};
expirationType: string = ExpirationType.DAYS;
systemExpirationDays: number;
coverAll: boolean = false;
coverAllForEdit: boolean = false;
isNameExisting: boolean = false;
loading: boolean = false;
checkNameOnGoing: boolean = false;
loadingSystemConfig: boolean = false;
defaultAccesses: FrontAccess[] = [];
defaultAccessesForEdit: FrontAccess[] = [];
@ViewChild(ListAllProjectsComponent)
listAllProjectsComponent: ListAllProjectsComponent;
@ViewChild(InlineAlertComponent)
inlineAlertComponent: InlineAlertComponent;
@ViewChild('robotForm', { static: true }) robotForm: NgForm;
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
private _nameSubject: Subject<string> = new Subject<string>();
private _nameSubscription: Subscription;
constructor( private configService: ConfigurationService,
private robotService: RobotService,
private msgHandler: MessageHandlerService,
private operationService: OperationService) {
}
ngOnInit(): void {
this.subscribeName();
}
ngOnDestroy() {
if (this._nameSubscription) {
this._nameSubscription.unsubscribe();
this._nameSubscription = null;
}
}
subscribeName() {
if (!this._nameSubscription) {
this._nameSubscription = this._nameSubject
.pipe(
debounceTime(500),
distinctUntilChanged(),
filter(name => {
if (this.isEditMode && this.originalRobotForEdit && this.originalRobotForEdit.name === name) {
return false;
}
return name.length > 0;
}),
switchMap((name) => {
this.isNameExisting = false;
this.checkNameOnGoing = true;
return this.robotService.ListRobot({
q: encodeURIComponent(`name=${name}`)
}).pipe(finalize(() => this.checkNameOnGoing = false));
}))
.subscribe(res => {
if (res && res.length > 0) {
this.isNameExisting = true;
}
});
}
}
isExpirationInvalid(): boolean {
return this.systemRobot.duration < -1;
}
inputExpiration() {
if (+this.systemRobot.duration === -1) {
this.expirationType = ExpirationType.NEVER;
} else {
this.expirationType = ExpirationType.DAYS;
}
}
changeExpirationType() {
if (this.expirationType === ExpirationType.DEFAULT) {
this.systemRobot.duration = this.systemExpirationDays;
}
if (this.expirationType === ExpirationType.DAYS) {
this.systemRobot.duration = this.systemExpirationDays;
}
if (this.expirationType === ExpirationType.NEVER) {
this.systemRobot.duration = -1;
}
}
getSystemRobotExpiration() {
this.loadingSystemConfig = true;
this.configService.getConfiguration()
.pipe(finalize(() => this.loadingSystemConfig = false))
.subscribe(res => {
if (res && res.robot_token_duration && res.robot_token_duration.value) {
this.systemRobot.duration = Math.floor(res.robot_token_duration.value / MINUETS_ONE_DAY);
this.systemExpirationDays = this.systemRobot.duration;
}
});
}
inputName() {
this._nameSubject.next(this.systemRobot.name);
}
cancel() {
this.addRobotOpened = false;
}
getPermissions(): number {
let count: number = 0;
this.defaultAccesses.forEach(item => {
if (item.checked) {
count ++;
}
});
return count;
}
chooseAccess(access: FrontAccess) {
access.checked = !access.checked;
}
reset() {
this.open(false);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.listAllProjectsComponent.init(false);
this.listAllProjectsComponent.selectedRow = [];
this.systemRobot = {};
this.robotForm.reset();
this.expirationType = ExpirationType.DAYS;
this.getSystemRobotExpiration();
}
resetForEdit(robot: Robot) {
this.open(true);
this.defaultAccesses = clone(INITIAL_ACCESSES);
this.defaultAccesses.forEach( item => item.checked = false);
this.originalRobotForEdit = clone(robot);
this.systemRobot = robot;
this.expirationType =
robot.duration === -1 ? ExpirationType.NEVER : ExpirationType.DAYS;
if (robot && robot.permissions && robot.permissions.length) {
this.coverAll = false;
robot.permissions.forEach(item => {
if (item.kind === PermissionsKinds.PROJECT
&& item.namespace === NAMESPACE_ALL_PROJECTS) {
this.coverAll = true;
if (item && item.access) {
item.access.forEach(item2 => {
this.defaultAccesses.forEach(item3 => {
if (item3.resource === item2.resource && item3.action === item2.action) {
item3.checked = true;
}
});
});
this.defaultAccessesForEdit = clone(this.defaultAccesses);
}
}
});
}
this.robotForm.reset({
name: this.systemRobot.name,
expiration: this.systemRobot.duration,
description: this.systemRobot.description,
coverAll: this.coverAll
});
this.coverAllForEdit = this.coverAll;
this.listAllProjectsComponent.init(true);
this.listAllProjectsComponent.selectedRow = [];
const map = {};
this.listAllProjectsComponent.projects.forEach( (pro, index) => {
if (this.systemRobot && this.systemRobot.permissions) {
this.systemRobot.permissions.forEach( item => {
if (pro.name === item.namespace) {
item.access.forEach(acc => {
pro.permissions[0].access.forEach(item3 => {
if (item3.resource === acc.resource && item3.action === acc.action) {
item3.checked = true;
}
});
});
map[index] = true;
this.listAllProjectsComponent.selectedRow.push(pro);
}
});
}
});
this.listAllProjectsComponent.defaultAccesses.forEach( item => item.checked = true);
this.listAllProjectsComponent.projects.forEach( (pro, index) => {
if (!map[index]) {
pro.permissions[0].access.forEach( item => {
item.checked = true;
});
}
});
}
open(isEditMode: boolean) {
this.isEditMode = isEditMode;
this.addRobotOpened = true;
this.inlineAlertComponent.close();
}
disabled(): boolean {
if (!this.isEditMode) {
return !this.canAdd();
}
return !this.canEdit();
}
canAdd(): boolean {
if (this.robotForm.invalid) {
return false;
}
if (this.coverAll) {
let flag = false;
this.defaultAccesses.forEach(item => {
if (item.checked) {
flag = true;
}
});
if (flag) {
return true;
}
}
if (!this.listAllProjectsComponent || !this.listAllProjectsComponent.selectedRow ||
!this.listAllProjectsComponent.selectedRow.length) {
return false;
}
for (let i = 0; i < this.listAllProjectsComponent.selectedRow.length; i++) {
let flag = false;
for (let j = 0; j < this.listAllProjectsComponent.selectedRow[i].permissions[0].access.length; j++) {
if (this.listAllProjectsComponent.selectedRow[i].permissions[0].access[j].checked) {
flag = true;
}
}
if (!flag) {
return false;
}
}
return true;
}
canEdit() {
if (!this.canAdd()) {
return false;
}
// tslint:disable-next-line:triple-equals
if (this.systemRobot.duration != this.originalRobotForEdit.duration) {
return true;
}
// tslint:disable-next-line:triple-equals
if (this.systemRobot.description != this.originalRobotForEdit.description) {
return true;
}
if (this.coverAll !== this.coverAllForEdit) {
return true;
}
if (this.coverAll) {
let flag = true;
this.defaultAccessesForEdit.forEach(item => {
this.defaultAccesses.forEach(item2 => {
if (item.resource === item2.resource && item.action === item2.action && item.checked !== item2.checked) {
flag = false;
}
});
});
return !flag;
}
if (this.systemRobot.permissions.length !== this.listAllProjectsComponent.selectedRow.length) {
return true;
}
const map = {};
let accessFlag = true;
this.listAllProjectsComponent.selectedRow.forEach(item => {
this.systemRobot.permissions.forEach( item2 => {
if (item.name === item2.namespace) {
map[item.name] = true;
if (item2.access.length !== this.getAccessNum(item.permissions[0].access)) {
accessFlag = false;
}
item2.access.forEach(arr => {
item.permissions[0].access.forEach(arr2 => {
if (arr.resource === arr2.resource && arr.action === arr2.action && !arr2.checked) {
accessFlag = false;
}
});
});
}
});
});
if (!accessFlag) {
return true;
}
let flag1 = true;
this.systemRobot.permissions.forEach(item => {
if (!map[item.namespace]) {
flag1 = false;
}
});
return !flag1;
}
save() {
this.saveBtnState = ClrLoadingState.LOADING;
const robot: Robot = clone(this.systemRobot);
robot.disable = false;
robot.level = PermissionsKinds.SYSTEM;
robot.duration = +this.systemRobot.duration;
robot.permissions = [];
if (this.coverAll) {
const access: Access[] = [];
this.defaultAccesses.forEach(item => {
if (item.checked) {
access.push({
resource: item.resource,
action: item.action
});
}
});
robot.permissions.push({
kind: PermissionsKinds.PROJECT,
namespace: NAMESPACE_ALL_PROJECTS,
access: access
});
} else {
this.listAllProjectsComponent.selectedRow.forEach( item => {
const access: Access[] = [];
item.permissions[0].access.forEach(item2 => {
if (item2.checked) {
access.push({
resource: item2.resource,
action: item2.action
});
}
});
robot.permissions.push({
kind: PermissionsKinds.PROJECT,
namespace: item.name,
access: access
});
});
}
if (this.isEditMode) {
robot.disable = this.systemRobot.disable;
const opeMessage = new OperateInfo();
opeMessage.name = "SYSTEM_ROBOT.UPDATE_ROBOT";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.UpdateRobot({
robotId: this.originalRobotForEdit.id,
robot
}).subscribe( res => {
this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(null);
this.addRobotOpened = false;
operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess("SYSTEM_ROBOT.UPDATE_ROBOT_SUCCESSFULLY");
}, error => {
this.saveBtnState = ClrLoadingState.ERROR;
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
this.inlineAlertComponent.showInlineError(error);
});
} else {
const opeMessage = new OperateInfo();
opeMessage.name = "SYSTEM_ROBOT.ADD_ROBOT";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.CreateRobot({
robot: robot
}).subscribe( res => {
this.saveBtnState = ClrLoadingState.SUCCESS;
this.addSuccess.emit(res);
this.addRobotOpened = false;
operateChanges(opeMessage, OperationState.success);
}, error => {
this.saveBtnState = ClrLoadingState.ERROR;
this.inlineAlertComponent.showInlineError(error);
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
});
}
}
getAccessNum(access: FrontAccess[]): number {
let count: number = 0;
access.forEach(item => {
if (item.checked) {
count ++;
}
});
return count;
}
}

View File

@ -0,0 +1,49 @@
<clr-modal clrModalSize="lg" [(clrModalOpen)]="projectsModalOpened"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<h3 class="modal-title">
<span>{{"SYSTEM_ROBOT.PROJECTS_MODAL_TITLE" | translate}}</span>
<div class="nav-divider"></div>
<span class="name">{{robotName}}</span>
</h3>
<div class="modal-body">
<p class="mt-0">{{"SYSTEM_ROBOT.PROJECTS_MODAL_SUMMARY" | translate}}</p>
<clr-datagrid>
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PERMISSION_COLUMN" | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of permissions" [clrDgItem]="p">
<clr-dg-cell>
<a href="javascript:void(0)" (click)="goToLink(getProject(p)?.project_id)">{{p.namespace}}</a>
</clr-dg-cell>
<clr-dg-cell>
<div class="permissions">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{p.access?.length}} {{"SYSTEM_ROBOT.PERMISSIONS" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div clrDropdownItem *ngFor="let item of p.access">
<span>{{i18nMap[item.action] | translate}} {{i18nMap[item.resource] | translate}}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</clr-dg-cell>
<clr-dg-cell>{{getProject(p)?.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="10">
<clr-dg-page-size [clrPageSizeOptions]="[10,20,30]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span>{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{permissions?.length
}} {{'PROJECT.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<div class="modal-footer">
<button type="button" (click)="close()" class="btn btn-primary">{{'BUTTON.CLOSE'
| translate}}</button>
</div>
</clr-modal>

View File

@ -0,0 +1,23 @@
.nav-divider {
display: inline-block;
width: 1px;
height: 30px;
position: relative;
top: 10px;
opacity: 0.15;
content: '';
margin-right: 1rem;
margin-left: 1rem;
}
.name {
font-size: 16px;
opacity: 0.9;
}
.permissions {
height: 16px;
display: flex;
align-items: center;
}
.datagrid-host {
position: inherit;
}

View File

@ -0,0 +1,75 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectsModalComponent } from './projects-modal.component';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { TranslateModule } from "@ngx-translate/core";
import { RouterTestingModule } from "@angular/router/testing";
import { Project } from "../../../../ng-swagger-gen/models/project";
import { Permission } from "../../../../ng-swagger-gen/models/permission";
import { Action, PermissionsKinds, Resource } from "../system-robot-util";
describe('ProjectsModalComponent', () => {
let component: ProjectsModalComponent;
let fixture: ComponentFixture<ProjectsModalComponent>;
const project1: Project = {
project_id: 1,
name: 'project1'
};
const project2: Project = {
project_id: 2,
name: 'project2'
};
const permissions: Permission[] = [
{
kind: PermissionsKinds.PROJECT,
namespace: project1.name,
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
},
{
kind: PermissionsKinds.PROJECT,
namespace: project2.name,
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
];
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
RouterTestingModule,
TranslateModule.forRoot(),
],
declarations: [ ProjectsModalComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProjectsModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render list', async () => {
component.projectsModalOpened = true;
component.permissions = permissions;
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(2);
});
});

View File

@ -0,0 +1,38 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Permission } from "../../../../ng-swagger-gen/models/permission";
import { Project } from "../../../../ng-swagger-gen/models/project";
import { Router } from "@angular/router";
import { ACTION_RESOURCE_I18N_MAP } from "../system-robot-util";
@Component({
selector: 'app-projects-modal',
templateUrl: './projects-modal.component.html',
styleUrls: ['./projects-modal.component.scss']
})
export class ProjectsModalComponent implements OnInit {
projectsModalOpened: boolean = false;
robotName: string;
cachedAllProjects: Project[];
permissions: Permission[] = [];
i18nMap = ACTION_RESOURCE_I18N_MAP;
constructor(private router: Router) { }
ngOnInit(): void {
}
close() {
this.projectsModalOpened = false;
}
getProject(p: Permission): Project {
if (this.cachedAllProjects && this.cachedAllProjects.length) {
for (let i = 0; i < this.cachedAllProjects.length; i++) {
if (p.namespace === this.cachedAllProjects[i].name) {
return this.cachedAllProjects[i];
}
}
}
return null;
}
goToLink(proId: number): void {
this.router.navigate(["harbor", "projects", proId]);
}
}

View File

@ -0,0 +1,116 @@
<h2 class="custom-h2">{{"SYSTEM_ROBOT.ROBOT_ACCOUNT_NAV" | translate}}</h2>
<div class="row robot-space">
<div>
<div class="row flex-items-xs-between rightPos">
<div class="flex-xs-middle option-left">
</div>
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"ROBOT_ACCOUNT.FILTER_PLACEHOLDER" | translate}}'></hbr-filter>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar>
<button [disabled]="loadingData" [clrLoading]="addBtnState" class="btn btn-secondary" (click)="openNewRobotModal(false)">
<span>
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'ROBOT_ACCOUNT.NEW_ROBOT_ACCOUNT'
| translate }}
</span>
</button>
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="btn
btn-link" clrDropdownTrigger>
<span>{{'MEMBER.ACTION' | translate}}<clr-icon shape="caret
down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<button [disabled]="loadingData || !(selectedRows && selectedRows.length === 1)" clrDropdownItem (click)="openTokenModal()">
<clr-icon shape="details" size="16"></clr-icon>&nbsp;
<span id="system-robot-token">{{"SYSTEM_ROBOT.VIEW_SECRET" | translate}}</span>
</button>
<button [disabled]="loadingData || !(selectedRows && selectedRows.length === 1)" clrDropdownItem (click)="openNewRobotModal(true)">
<clr-icon shape="edit" size="16"></clr-icon>&nbsp;
<span id="system-robot-edit">{{'BUTTON.EDIT' | translate}}</span>
</button>
<button *ngIf="selectedRows && selectedRows.length === 1 && selectedRows[0].disable" type="button" class="btn btn-secondary"
(click)="disableOrEnable()"
[disabled]="!(selectedRows && selectedRows.length === 1 && selectedRows[0].disable)">
<clr-icon size="16" shape="success-standard"></clr-icon>&nbsp;
<span id="distribution-enable">{{'WEBHOOK.ENABLED_BUTTON' | translate}}</span>
</button>
<button *ngIf="!(selectedRows && selectedRows.length === 1 && selectedRows[0].disable)"
type="button"
class="btn btn-secondary"
(click)="disableOrEnable()"
[disabled]="!(selectedRows && selectedRows.length === 1 && !selectedRows[0].disable)">
<clr-icon size="16" shape="ban"></clr-icon>&nbsp;
<span id="distribution-disable">{{'WEBHOOK.DISABLED_BUTTON' | translate}}</span>
</button>
<div class="dropdown-divider"></div>
<button [disabled]="loadingData || !(selectedRows && selectedRows.length >=1)" clrDropdownItem (click)="openDeleteRobotsDialog()">
<clr-icon shape="window-close" size="16"></clr-icon>&nbsp;
<span id="system-robot-delete">{{'BUTTON.DELETE' | translate}}</span>
</button>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRows">
<clr-dg-column>{{'ROBOT_ACCOUNT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PROJECTS" | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.CREATETION' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRES_AT' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{
'SYSTEM_ROBOT.NOT_FOUND' | translate
}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let r of robots" [clrDgItem]="r">
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="r.disable">
<clr-icon shape="check-circle" *ngSwitchCase="false" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="true" size="16" class="color-red red-position"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>
<div class="all-projects" *ngIf="r.permissionScope?.coverAll">
<span>{{"SYSTEM_ROBOT.ALL_PROJECTS" | translate}}</span>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button class="btn btn-link" clrDropdownTrigger>
{{r.permissionScope?.access?.length}} {{"SYSTEM_ROBOT.PERMISSIONS" | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div clrDropdownItem *ngFor="let item of r.permissionScope?.access">
<span>{{i18nMap[item.action] | translate}} {{i18nMap[item.resource] | translate}}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<span *ngIf="!r.permissionScope || !r.permissionScope?.coverAll">
<a href="javascript:void(0)" (click)="openProjectModal(getProjects(r), r.name)">
{{getProjects(r)?.length?getProjects(r)?.length:""}} {{'SYSTEM_ROBOT.COVERED_PROJECTS' | translate}}
</a>
</span>
</clr-dg-cell>
<clr-dg-cell>{{r.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{r.expires_at === -1?("ROBOT_ACCOUNT.NEVER_EXPIRED" | translate):(r.expires_at * 1000 | date: 'short')}}</clr-dg-cell>
<clr-dg-cell>{{r.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="total">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="total">{{pagination.firstItem + 1}}
-
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{total}} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<new-robot (addSuccess)="addSuccess($event)"></new-robot>
<view-token (refreshSuccess)="refresh()"></view-token>
<app-projects-modal></app-projects-modal>

View File

@ -0,0 +1,35 @@
.robot-space {
margin-top: 28px;
position: relative;
clr-icon.red-position {
margin-left: 2px;
}
.rightPos {
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
.option-left {
padding-left: 16px;
position: relative;
top: 10px;
}
.option-right {
padding-right: 16px;
.refresh-btn {
cursor: pointer;
}
}
}
}
.all-projects {
display: flex;
align-items: center;
height: 16px;
}

View File

@ -0,0 +1,161 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { TranslateModule } from "@ngx-translate/core";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { SystemRobotAccountsComponent } from './system-robot-accounts.component';
import { RobotService } from "../../../ng-swagger-gen/services/robot.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { of, Subscription } from "rxjs";
import { delay } from "rxjs/operators";
import { Robot } from "../../../ng-swagger-gen/models/robot";
import { Action, PermissionsKinds, Resource } from "./system-robot-util";
import { Project } from "../../../ng-swagger-gen/models/project";
import { ProjectService } from "../../../ng-swagger-gen/services/project.service";
import { MessageHandlerService } from "../shared/message-handler/message-handler.service";
import { ConfirmationDialogService } from "../shared/confirmation-dialog/confirmation-dialog.service";
import { OperationService } from "../../lib/components/operation/operation.service";
import { HttpClientTestingModule } from "@angular/common/http/testing";
describe('SystemRobotAccountsComponent', () => {
let component: SystemRobotAccountsComponent;
let fixture: ComponentFixture<SystemRobotAccountsComponent>;
const project1: Project = {
project_id: 1,
name: 'project1'
};
const project2: Project = {
project_id: 2,
name: 'project2'
};
const project3: Project = {
project_id: 3,
name: 'project3'
};
const robot1: Robot = {
id: 1,
name: 'robot1',
level: PermissionsKinds.SYSTEM,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'tthf54hfth4545dfgd5g454grd54gd54g',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project1',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const robot2: Robot = {
id: 2,
name: 'robot2',
level: PermissionsKinds.SYSTEM,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'fsdf454654654fs6dfe',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project2',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const robot3: Robot = {
id: 3,
name: 'robot3',
level: PermissionsKinds.SYSTEM,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'fsdg48454fse84',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project3',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const mockProjectService = {
listProjectsResponse: () => {
const res: HttpResponse<Array<Project>> = new HttpResponse<Array<Project>>({
headers: new HttpHeaders({'x-total-count': '3'}),
body: [project1, project2, project3]
});
return of(res).pipe(delay(0));
}
};
const fakedRobotService = {
ListRobotResponse() {
const res: HttpResponse<Array<Robot>> = new HttpResponse<Array<Robot>>({
headers: new HttpHeaders({'x-total-count': '3'}),
body: [robot1, robot2, robot3]
});
return of(res).pipe(delay(0));
}
};
const fakedMessageHandlerService = {
showSuccess() {
},
error() {
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot(),
HttpClientTestingModule
],
declarations: [ SystemRobotAccountsComponent ],
providers: [
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
ConfirmationDialogService,
OperationService,
{ provide: RobotService, useValue: fakedRobotService},
{ provide: ProjectService, useValue: mockProjectService},
],
schemas: [
NO_ERRORS_SCHEMA
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SystemRobotAccountsComponent);
component = fixture.componentInstance;
component.searchSub = new Subscription();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render robot list', async () => {
fixture.autoDetectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(3);
});
});

View File

@ -0,0 +1,367 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NewRobotComponent } from './new-robot/new-robot.component';
import { ViewTokenComponent } from './view-token/view-token.component';
import { RobotService } from "../../../ng-swagger-gen/services/robot.service";
import { Robot } from "../../../ng-swagger-gen/models/robot";
import { clone, DEFAULT_PAGE_SIZE } from "../../lib/utils/utils";
import { ClrDatagridStateInterface, ClrLoadingState } from "@clr/angular";
import { catchError, debounceTime, distinctUntilChanged, finalize, map, switchMap } from "rxjs/operators";
import { MessageHandlerService } from "../shared/message-handler/message-handler.service";
import {
ACTION_RESOURCE_I18N_MAP,
FrontRobot,
NAMESPACE_ALL_PROJECTS,
PermissionsKinds
} from "./system-robot-util";
import { ProjectsModalComponent } from "./projects-modal/projects-modal.component";
import { Permission } from "../../../ng-swagger-gen/models/permission";
import { forkJoin, Observable, of, Subscription } from "rxjs";
import { FilterComponent } from "../../lib/components/filter/filter.component";
import { ProjectService } from "../../../ng-swagger-gen/services/project.service";
import { ConfirmationMessage } from "../shared/confirmation-dialog/confirmation-message";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
import { ConfirmationDialogService } from "../shared/confirmation-dialog/confirmation-dialog.service";
import { HttpErrorResponse } from "@angular/common/http";
import { errorHandler } from "../../lib/utils/shared/shared.utils";
import { operateChanges, OperateInfo, OperationState } from "../../lib/components/operation/operate";
import { OperationService } from "../../lib/components/operation/operation.service";
import { Observable as __Observable } from "rxjs/internal/Observable";
import { Project } from "../../../ng-swagger-gen/models/project";
import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
const FIRST_PROJECTS_PAGE_SIZE: number = 100;
@Component({
selector: 'system-robot-accounts',
templateUrl: './system-robot-accounts.component.html',
styleUrls: ['./system-robot-accounts.component.scss']
})
export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
i18nMap = ACTION_RESOURCE_I18N_MAP;
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 1;
total: number = 0;
robots: FrontRobot[] = [];
selectedRows: FrontRobot[] = [];
loading: boolean = true;
loadingData: boolean = false;
addBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
hasGetAllProjects: boolean = false;
@ViewChild(NewRobotComponent)
newRobotComponent: NewRobotComponent;
@ViewChild(ViewTokenComponent)
viewTokenComponent: ViewTokenComponent;
@ViewChild(ProjectsModalComponent)
projectsModalComponent: ProjectsModalComponent;
@ViewChild(FilterComponent, {static: true})
filterComponent: FilterComponent;
searchSub: Subscription;
searchKey: string;
subscription: Subscription;
constructor(private robotService: RobotService,
private projectService: ProjectService,
private msgHandler: MessageHandlerService,
private operateDialogService: ConfirmationDialogService,
private operationService: OperationService,
private sanitizer: DomSanitizer,
private translate: TranslateService,
) {
this.subscription = operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT
) {
this.deleteRobots(message.data);
}
if ( message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT_ENABLE_OR_DISABLE) {
this.operateRobot();
}
}
);
}
ngOnInit() {
this.loadDataFromBackend();
if (!this.searchSub) {
this.searchSub = this.filterComponent.filterTerms.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(robotSearchName => {
this.currentPage = 1;
this.selectedRows = [];
const queryParam: RobotService.ListRobotParams = {
page: this.currentPage,
pageSize: this.pageSize,
};
this.searchKey = robotSearchName;
if (this.searchKey) {
queryParam.q = encodeURIComponent(`name=~${this.searchKey}`);
}
this.loading = true;
return this.robotService.ListRobotResponse(queryParam)
.pipe(finalize(() => {
this.loading = false;
}));
})).subscribe(response => {
this.total = Number.parseInt(
response.headers.get('x-total-count')
);
this.robots = response.body as Robot[];
this.calculateProjects();
}, error => {
this.msgHandler.handleError(error);
});
}
}
ngOnDestroy() {
if (this.searchSub) {
this.searchSub.unsubscribe();
this.searchSub = null;
}
}
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);
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 = 1; 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) {
let arr = [];
res.forEach(item => {
arr = arr.concat(item);
});
if (this.newRobotComponent && this.newRobotComponent.listAllProjectsComponent) {
this.newRobotComponent.listAllProjectsComponent.cachedAllProjects = arr;
}
if (this.projectsModalComponent) {
this.projectsModalComponent.cachedAllProjects = arr;
}
}
});
}
}
}, error => {
this.loadingData = false;
this.addBtnState = ClrLoadingState.ERROR;
});
}
clrLoad(state?: ClrDatagridStateInterface) {
if (state && state.page && state.page.size) {
this.pageSize = state.page.size;
}
this.selectedRows = [];
const queryParam: RobotService.ListRobotParams = {
page: this.currentPage,
pageSize: this.pageSize,
};
if (this.searchKey) {
queryParam.q = encodeURIComponent(`name=~${this.searchKey}`);
}
this.loading = true;
this.robotService.ListRobotResponse(queryParam)
.pipe(finalize(() => this.loading = false))
.subscribe(
response => {
this.total = Number.parseInt(
response.headers.get('x-total-count')
);
this.robots = response.body as Robot[];
this.calculateProjects();
},
err => {
this.msgHandler.error(err);
});
}
openNewRobotModal(isEditMode: boolean) {
if (isEditMode) {
this.newRobotComponent.resetForEdit(clone(this.selectedRows[0]));
} else {
this.newRobotComponent.reset();
}
}
openTokenModal() {
this.viewTokenComponent.open();
this.viewTokenComponent.robot = clone(this.selectedRows[0]);
}
calculateProjects() {
if (this.robots && this.robots.length) {
for (let i = 0 ; i < this.robots.length; i++) {
if (this.robots[i] && this.robots[i].permissions && this.robots[i].permissions.length) {
for (let j = 0 ; j < this.robots[i].permissions.length; j++) {
if (this.robots[i].permissions[j].kind === PermissionsKinds.PROJECT
&& this.robots[i].permissions[j].namespace === NAMESPACE_ALL_PROJECTS) {
this.robots[i].permissionScope = {
coverAll: true,
access: this.robots[i].permissions[j].access
};
break;
}
}
}
}
}
}
getProjects(r: FrontRobot): Permission[] {
const arr = [];
if (r && r.permissions && r.permissions.length) {
for (let i = 0 ; i < r.permissions.length; i++) {
if (r.permissions[i].kind === PermissionsKinds.PROJECT
) {
arr.push(r.permissions[i]);
}
}
}
return arr;
}
openProjectModal(permissions: Permission[], robotName: string) {
this.projectsModalComponent.projectsModalOpened = true;
this.projectsModalComponent.robotName = robotName;
this.projectsModalComponent.permissions = permissions;
}
refresh() {
this.currentPage = 1;
this.selectedRows = [];
this.clrLoad();
}
deleteRobots(robots: Robot[]) {
let observableLists: Observable<any>[] = [];
if (robots && robots.length) {
robots.forEach(item => {
observableLists.push(this.deleteRobot(item));
});
forkJoin(...observableLists).subscribe(resArr => {
let error;
if (resArr && resArr.length) {
resArr.forEach(item => {
if (item instanceof HttpErrorResponse) {
error = errorHandler(item);
}
});
}
if (error) {
this.msgHandler.handleError(error);
} else {
this.msgHandler.showSuccess('SYSTEM_ROBOT.DELETE_ROBOT_SUCCESS');
}
this.refresh();
});
}
}
deleteRobot(robot: Robot): Observable<any> {
let operMessage = new OperateInfo();
operMessage.name = 'SYSTEM_ROBOT.DELETE_ROBOT';
operMessage.data.id = robot.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = robot.name;
this.operationService.publishInfo(operMessage);
return this.robotService.DeleteRobot({robotId: robot.id}).pipe(
map(() => {
operateChanges(operMessage, OperationState.success);
}),
catchError(error => {
const message = errorHandler(error);
operateChanges(operMessage, OperationState.failure, message);
return of(error);
})
);
}
openDeleteRobotsDialog() {
const robotNames = this.selectedRows.map(robot => robot.name).join(",");
const deletionMessage = new ConfirmationMessage(
"ROBOT_ACCOUNT.DELETION_TITLE",
"ROBOT_ACCOUNT.DELETION_SUMMARY",
robotNames,
this.selectedRows,
ConfirmationTargets.ROBOT_ACCOUNT,
ConfirmationButtons.DELETE_CANCEL
);
this.operateDialogService.openComfirmDialog(deletionMessage);
}
disableOrEnable() {
const title: string = this.selectedRows[0].disable ? "SYSTEM_ROBOT.ENABLE_TITLE" : "SYSTEM_ROBOT.DISABLE_TITLE";
const summary: string = this.selectedRows[0].disable ? "SYSTEM_ROBOT.ENABLE_SUMMARY" : "SYSTEM_ROBOT.DISABLE_SUMMARY";
const deletionMessage = new ConfirmationMessage(
title,
summary,
this.selectedRows[0].name,
this.selectedRows[0],
ConfirmationTargets.ROBOT_ACCOUNT_ENABLE_OR_DISABLE,
this.selectedRows[0].disable ? ConfirmationButtons.ENABLE_CANCEL : ConfirmationButtons.DISABLE_CANCEL
);
this.operateDialogService.openComfirmDialog(deletionMessage);
}
operateRobot() {
const robot: Robot = clone(this.selectedRows[0]);
const successMessage: string = robot.disable ? "SYSTEM_ROBOT.ENABLE_ROBOT_SUCCESSFULLY" : "SYSTEM_ROBOT.DISABLE_ROBOT_SUCCESSFULLY";
robot.disable = !robot.disable;
delete robot.secret;
const opeMessage = new OperateInfo();
opeMessage.name = robot.disable ? "SYSTEM_ROBOT.DISABLE_TITLE" : "SYSTEM_ROBOT.ENABLE_TITLE";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
this.robotService.UpdateRobot({
robot: robot,
robotId: robot.id
}).subscribe( res => {
operateChanges(opeMessage, OperationState.success);
this.msgHandler.showSuccess(successMessage);
this.refresh();
}, error => {
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
this.msgHandler.showSuccess(error);
});
}
addSuccess(robot: Robot) {
if (robot) {
this.viewTokenComponent.open();
this.viewTokenComponent.tokenModalOpened = false;
this.viewTokenComponent.robot = clone(robot);
this.viewTokenComponent.copyToken = true;
this.translate
.get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: robot.name })
.subscribe((res: string) => {
this.viewTokenComponent.createSuccess = res;
});
// export to token file
const downLoadUrl = `data:text/json;charset=utf-8, ${encodeURIComponent(JSON.stringify(robot))}`;
this.viewTokenComponent.downLoadHref = this.sanitizer.bypassSecurityTrustUrl(downLoadUrl);
this.viewTokenComponent.downLoadFileName = `${robot.name}.json`;
}
this.refresh();
}
}

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SystemRobotAccountsComponent } from './system-robot-accounts.component';
import { SharedModule } from '../shared/shared.module';
import { NewRobotComponent } from './new-robot/new-robot.component';
import { ProjectModule } from '../project/project.module';
import { ListAllProjectsComponent } from './list-all-projects/list-all-projects.component';
import { ProjectsModalComponent } from './projects-modal/projects-modal.component';
@NgModule({
declarations: [
SystemRobotAccountsComponent,
NewRobotComponent,
ListAllProjectsComponent,
ProjectsModalComponent
],
imports: [
CommonModule,
SharedModule,
ProjectModule
]
})
export class SystemRobotAccountsModule { }

View File

@ -0,0 +1,83 @@
import { Robot } from "../../../ng-swagger-gen/models/robot";
import { Access } from "../../../ng-swagger-gen/models/access";
import { Project } from "../../../ng-swagger-gen/models/project";
export interface FrontRobot extends Robot {
permissionScope?: {
coverAll?: boolean,
access?: Array<Access>;
};
}
export interface FrontProjectForAdd extends Project {
permissions?: Array<{
kind?: string;
namespace?: string;
access?: Array<FrontAccess>;
}>;
}
export interface FrontAccess extends Access {
checked?: boolean;
}
export enum PermissionsKinds {
PROJECT = 'project',
SYSTEM = 'system'
}
export enum Resource {
ARTIFACT = 'repository',
HELM_CHART = 'helm-chart',
HELM_CHART_VERSION = 'helm-chart-version'
}
export enum Action {
PUSH = 'push',
PULL = 'pull',
READ = 'read',
CREATE = 'create'
}
export const NAMESPACE_ALL_PROJECTS: string = '*';
export const INITIAL_ACCESSES: FrontAccess[] = [
{
"resource": "repository",
"action": "push",
"checked": true
},
{
"resource": "repository",
"action": "pull",
"checked": true
},
{
"resource": "helm-chart",
"action": "read",
"checked": true
},
{
"resource": "helm-chart-version",
"action": "create",
"checked": true
}
];
export const ACTION_RESOURCE_I18N_MAP = {
'push': 'ROBOT_ACCOUNT.PUSH',
'pull': 'ROBOT_ACCOUNT.PULL',
'read': 'SYSTEM_ROBOT.READ',
'create': 'SYSTEM_ROBOT.CREATE',
'repository': 'SYSTEM_ROBOT.ARTIFACT',
'helm-chart': 'SYSTEM_ROBOT.HELM',
'helm-chart-version': 'SYSTEM_ROBOT.HELM_VERSION'
};
export enum ExpirationType {
DEFAULT= 'default',
DAYS = 'days',
NEVER = 'never'
}

View File

@ -0,0 +1,93 @@
<clr-modal [(clrModalOpen)]="tokenModalOpened" class="copy-token"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<div class="modal-title">
<span>{{"SYSTEM_ROBOT.REFRESH_SECRET" | translate}}</span>
<div class="nav-divider"></div>
<span class="name">{{robot?.name}}</span>
</div>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<p class="mt-0">{{"SYSTEM_ROBOT.REFRESH_SECRET_SUMMARY" | translate}}</p>
<section class="form-block show-info">
<form #secretForm="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label for="new-token" class="clr-control-label">{{"SYSTEM_ROBOT.NEW_TOKEN" | translate}}</label>
<div class="clr-control-container" [class.clr-error]="(newToken.dirty || newToken.touched) && newToken.invalid">
<div class="clr-input-wrapper">
<input class="clr-input"
type="password" id="new-token"
name="newToken"
autocomplete="off"
#newToken = "ngModel"
[(ngModel)]="newSecret"
pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).{8,128}$"
placeholder="{{'SYSTEM_ROBOT.PLACEHOLDER'| translate}}">
</div>
<clr-control-error *ngIf="(newToken.dirty || newToken.touched) && newToken.invalid">
{{'SYSTEM_ROBOT.SECRET' | translate}}
</clr-control-error>
</div>
</div>
<div class="clr-form-control">
<label for="confirm-token" class="clr-control-label">{{"SYSTEM_ROBOT.CONFIRM_SECRET" | translate}}</label>
<div class="clr-control-container" [class.clr-error]="notSame() || (confirmToken.dirty || confirmToken.touched) && confirmToken.invalid">
<div class="clr-input-wrapper">
<input class="clr-input"
type="password" id="confirm-token"
name="confirmToken"
autocomplete="off"
#confirmToken = "ngModel"
[(ngModel)]="confirmSecret"
pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).{8,128}$"
placeholder="{{'SYSTEM_ROBOT.SECRET_AGAIN'| translate}}">
</div>
<clr-control-error *ngIf="notSame() || ((confirmToken.dirty || confirmToken.touched) && confirmToken.invalid)">
<span *ngIf="!notSame()">{{'SYSTEM_ROBOT.SECRET' | translate}}</span>
<span *ngIf="notSame()">{{'SYSTEM_ROBOT.INCONSISTENT' | translate}}</span>
</clr-control-error>
</div>
</div>
</form>
</section>
</div>
<div class="modal-footer">
<button (click)="cancel()" id="refresh-token-cancel" type="button" class="btn btn-outline">{{'BUTTON.CANCEL'
| translate}}</button>
<button [disabled]="!canRefresh()" (click)="refreshToken()" [clrLoading]="btnState" id="refresh-token-refresh" type="button" class="btn btn-primary">{{"SYSTEM_ROBOT.REFRESH" | translate}}</button>
</div>
</clr-modal>
<clr-modal clrModalSize="lg" [(clrModalOpen)]="copyToken" class="copy-token"
[clrModalStaticBackdrop]="true" [clrModalClosable]="false">
<div class="modal-title">
<h3 class="modal-title">
<clr-icon class="alert-icon success-icon" shape="check-circle" size="50"></clr-icon>
{{ createSuccess | translate}}</h3>
</div>
<div class="modal-body">
<div class="alert alert-info" role="alert">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<clr-icon class="alert-icon" shape="info-circle"></clr-icon>
</div>
<span class="alert-text">{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}</span>
</div>
</div>
</div>
<section class="form-block show-info">
<div class="form-group robot-name">
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.NAME'
| translate}}</label>
<span>{{robot?.name}}</span>
</div>
<div class="form-group robot-token">
<label class="form-group-label-override">{{'ROBOT_ACCOUNT.TOKEN' |
translate}}</label>
<hbr-copy-input (onCopySuccess)="onCpSuccess($event)"
inputSize="50" headerTitle=""
defaultValue="{{robot?.secret}}" class="copy-input"></hbr-copy-input>
</div>
<a [href]="downLoadHref" [download]="downLoadFileName"><button class="btn mr-0" (click)="closeModal()">{{'ROBOT_ACCOUNT.EXPORT_TO_FILE' | translate}}</button></a>
</section>
</div>
</clr-modal>

View File

@ -0,0 +1,49 @@
.copy-token {
.success-icon {
color: #318700;
}
.show-info {
.robot-name {
margin: 30px 0;
label {
margin-right: 30px;
}
}
.robot-token {
margin-bottom: 20px;
label {
margin-right: 24px;
}
.copy-input {
display: inline-block;
}
}
}
}
:host::ng-deep {
hbr-copy-input {
.command-input {
width: 32rem;
}
}
}
.nav-divider {
display: inline-block;
width: 1px;
height: 30px;
position: relative;
top: 10px;
opacity: 0.15;
content: '';
margin-right: 1rem;
margin-left: 1rem;
}
.name {
font-size: 16px;
opacity: 0.9;
}
.align-center {
align-items: center;
}

View File

@ -0,0 +1,115 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewTokenComponent } from './view-token.component';
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { TranslateModule } from "@ngx-translate/core";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { Action, PermissionsKinds, Resource } from "../system-robot-util";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { SharedModule } from "../../shared/shared.module";
describe('ViewTokenComponent', () => {
let component: ViewTokenComponent;
let fixture: ComponentFixture<ViewTokenComponent>;
const robot1: Robot = {
id: 1,
name: 'robot1',
level: PermissionsKinds.SYSTEM,
disable: false,
expires_at: (new Date().getTime() + 100000) % 1000,
description: 'for test',
secret: 'tthf54hfth4545dfgd5g454grd54gd54g',
permissions: [
{
kind: PermissionsKinds.PROJECT,
namespace: 'project1',
access: [
{
resource: Resource.ARTIFACT,
action: Action.PUSH
}
]
}
]
};
const fakedMessageHandlerService = {
showSuccess() {
},
error() {
}
};
const fakedRobotService = {
UpdateRobot() {
return of(null).pipe(delay(0));
}
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot(),
FormsModule,
SharedModule
],
declarations: [ ViewTokenComponent ],
providers: [
{ provide: RobotService, useValue: fakedRobotService},
OperationService,
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
],
schemas: [
NO_ERRORS_SCHEMA
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ViewTokenComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show invalid secret', async () => {
await fixture.whenStable();
component.tokenModalOpened = true;
component.robot = robot1;
fixture.detectChanges();
await fixture.whenStable();
const newSecretInput: HTMLInputElement = fixture.nativeElement.querySelector('#new-token');
newSecretInput.value = '123';
newSecretInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const error = fixture.nativeElement.querySelector('clr-control-error');
expect(error).toBeTruthy();
});
it('should show secrets inconsistent', async () => {
await fixture.whenStable();
component.tokenModalOpened = true;
component.robot = robot1;
fixture.detectChanges();
await fixture.whenStable();
const newSecretInput: HTMLInputElement = fixture.nativeElement.querySelector('#new-token');
newSecretInput.value = 'Harbor12345';
newSecretInput.dispatchEvent(new Event('input'));
const confirmSecretInput: HTMLInputElement = fixture.nativeElement.querySelector('#confirm-token');
confirmSecretInput.value = 'Harbor123456';
confirmSecretInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const error = fixture.nativeElement.querySelector('clr-control-error');
expect(error).toBeTruthy();
});
});

View File

@ -0,0 +1,122 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { RobotService } from "../../../../ng-swagger-gen/services/robot.service";
import { ClrLoadingState } from "@clr/angular";
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { Robot } from "../../../../ng-swagger-gen/models/robot";
import { clone } from "../../../lib/utils/utils";
import { NgForm } from "@angular/forms";
import { operateChanges, OperateInfo, OperationState } from "../../../lib/components/operation/operate";
import { OperationService } from "../../../lib/components/operation/operation.service";
import { errorHandler } from "../../../lib/utils/shared/shared.utils";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { Observable } from "rxjs";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
@Component({
selector: 'view-token',
templateUrl: './view-token.component.html',
styleUrls: ['./view-token.component.scss']
})
export class ViewTokenComponent implements OnInit {
tokenModalOpened: boolean = false;
robot: Robot;
newSecret: string;
confirmSecret: string;
btnState: ClrLoadingState = ClrLoadingState.DEFAULT;
@ViewChild(InlineAlertComponent)
inlineAlertComponent: InlineAlertComponent;
@ViewChild('secretForm', { static: true }) secretForm: NgForm;
@Output()
refreshSuccess: EventEmitter<boolean> = new EventEmitter<boolean>();
copyToken: boolean = false;
createSuccess: string;
downLoadFileName: string = '';
downLoadHref: SafeUrl = '';
constructor(private robotService: RobotService,
private operationService: OperationService,
private msgHandler: MessageHandlerService,
private sanitizer: DomSanitizer,
private translate: TranslateService) { }
ngOnInit(): void {
}
cancel() {
this.tokenModalOpened = false;
}
open() {
this.tokenModalOpened = true;
this.inlineAlertComponent.close();
this.copyToken = false;
this.createSuccess = null;
this.newSecret = null;
this.confirmSecret = null;
this.downLoadFileName = '';
this.downLoadHref = '';
this.secretForm.reset();
}
refreshToken() {
this.btnState = ClrLoadingState.LOADING;
const robot: Robot = clone(this.robot);
const opeMessage = new OperateInfo();
opeMessage.name = "SYSTEM_ROBOT.REFRESH_SECRET";
opeMessage.data.id = robot.id;
opeMessage.state = OperationState.progressing;
opeMessage.data.name = robot.name;
this.operationService.publishInfo(opeMessage);
if (this.newSecret) {
robot.secret = this.newSecret;
}
this.robotService.RefreshSec({
robotId: robot.id,
robotSec: {
secret: robot.secret
}
}).subscribe(res => {
this.btnState = ClrLoadingState.SUCCESS;
operateChanges(opeMessage, OperationState.success);
this.refreshSuccess.emit(true);
this.cancel();
if (res && res.secret) {
this.robot.secret = res.secret;
this.copyToken = true;
this.createSuccess = 'SYSTEM_ROBOT.REFRESH_SECRET_SUCCESS';
// export to token file
const downLoadUrl = `data:text/json;charset=utf-8, ${encodeURIComponent(JSON.stringify(robot))}`;
this.downLoadHref = this.sanitizer.bypassSecurityTrustUrl(downLoadUrl);
this.downLoadFileName = `${robot.name}.json`;
} else {
this.msgHandler.showSuccess('SYSTEM_ROBOT.REFRESH_SECRET_SUCCESS');
}
}, error => {
this.btnState = ClrLoadingState.ERROR;
this.inlineAlertComponent.showInlineError(error);
operateChanges(opeMessage, OperationState.failure, errorHandler(error));
});
}
canRefresh() {
if (!this.newSecret && !this.confirmSecret) {
return true;
}
return this.newSecret && this.confirmSecret && this.newSecret === this.confirmSecret && this.secretForm.valid;
}
onCpSuccess($event: any): void {
this.copyToken = false;
this.tokenModalOpened = false;
this.translate
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robot.name })
.subscribe((res: string) => {
this.msgHandler.showSuccess(res);
});
}
closeModal() {
this.copyToken = false;
this.tokenModalOpened = false;
}
notSame(): boolean {
return this.secretForm.valid && this.newSecret && this.confirmSecret && this.newSecret !== this.confirmSecret;
}
}

View File

@ -243,3 +243,7 @@ artifact-list-tab {
}
}
}
.nav-divider {
background-color: $nav-divider-bg-color;
}

View File

@ -38,5 +38,6 @@ $harbor-icon-drop-shadow-x: 58px;
$command-input-color: #eaedf0;
$command-input-bg-color: #2e4757;
$label-hover-bg-color: #28404d;
$nav-divider-bg-color: #fafafa;
@import "./common.scss";

View File

@ -38,6 +38,7 @@ $hbr-result-tip-histogram-inner-bg-color: #fff;
$harbor-icon-translate-x: 100%;
$harbor-icon-drop-shadow-x: -56px;
$command-input-color: none;
$command-input-bg-color: none;
$command-input-bg-color: #ededed;
$label-hover-bg-color: #eee;
$nav-divider-bg-color: #000;
@import "./common.scss";

View File

@ -1592,5 +1592,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "Einträge pro Seite"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -357,7 +357,7 @@
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
"ACCOUNT_EXISTING": "Robot Account is already exists.",
"ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity",
"ALERT_TEXT": "This is the only time to copy this secret.You won't have another opportunity",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
@ -1592,5 +1592,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -358,7 +358,7 @@
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.",
"ACCOUNT_EXISTING": "Robot Account is already exists.",
"ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity",
"ALERT_TEXT": "This is the only time to copy this secret.You won't have another opportunity",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
@ -1590,5 +1590,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -349,7 +349,7 @@
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "ne peut pas contenir de caractères spéciaux(~#$%) et la longueur maximale devrait être de 255 caractères.",
"ACCOUNT_EXISTING": "le robot est existe déjà.",
"ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity",
"ALERT_TEXT": "This is the only time to copy this secret.You won't have another opportunity",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
@ -1560,5 +1560,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -1588,6 +1588,68 @@
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -1592,5 +1592,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -356,7 +356,7 @@
"FILTER_PLACEHOLDER": "过滤机器人账户",
"ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255。",
"ACCOUNT_EXISTING": "机器人账户已经存在。",
"ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会",
"ALERT_TEXT": "这是唯一一次复制当前令牌的机会",
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认",
@ -1589,5 +1589,67 @@
},
"PAGINATION": {
"PAGE_SIZE": "页面大小"
},
"SYSTEM_ROBOT": {
"READ": "读取",
"CREATE": "创建",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "添加机器人",
"UPDATE_ROBOT": "更新机器人",
"UPDATE_ROBOT_SUCCESSFULLY": "更新机器人成功",
"PLACEHOLDER": "请输入新令牌",
"SECRET": "令牌长度在8到20之间且需包含至少一个大写字符一个小写字符和一个数字。",
"REFRESH_SECRET": "刷新令牌",
"REFRESH_SECRET_SUCCESS": "刷新令牌成功",
"DELETE_ROBOT": "删除机器人",
"DELETE_ROBOT_SUCCESS": "删除机器人成功",
"ENABLE_TITLE": "启用机器人",
"ENABLE_SUMMARY": "您想启用机器人 {{param}}?",
"DISABLE_TITLE": "禁用机器人",
"DISABLE_SUMMARY": "你想禁用机器人 {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "启用机器人成功",
"DISABLE_ROBOT_SUCCESSFULLY": "禁用机器人成功",
"ROBOT_ACCOUNT": "机器人账户",
"PROJECTS": "覆盖项目数",
"ALL_PROJECTS": "全部项目且具有",
"PERMISSIONS": "项权限",
"REFRESH_SECRET_SUMMARY": "刷新当前机器人账户令牌",
"TOKEN": "令牌",
"NEW_TOKEN": "新令牌",
"REFRESH": "刷新",
"PROJECTS_MODAL_TITLE": "当前机器人账户覆盖的项目",
"PROJECTS_MODAL_SUMMARY": "以下为当前机器人账户覆盖的项目列表",
"CREATE_ROBOT": "创建系统级机器人账户",
"CREATE_ROBOT_SUMMARY": "创建机器人账户。您可以选中\"覆盖全部项目\"项来覆盖当前所有项目以及未来新增的项目",
"EDIT_ROBOT": "编辑系统级机器人账户",
"EDIT_ROBOT_SUMMARY": "编辑机器人账户。您可以选中\"覆盖全部项目\"项来覆盖当前所有项目以及未来新增的项目",
"EXPIRATION_TIME": "过期时间",
"EXPIRATION_TIME_EXPLAIN": "当前机器人账户的令牌有效时间(单位为天且起始点为创建时间)。如想设置为永不过期, 请输入\"-1\"。",
"EXPIRATION_DEFAULT": "天(默认)",
"EXPIRATION_DAYS": "指定天数",
"EXPIRATION_NEVER": "永不过期",
"EXPIRATION_REQUIRED": "请输入有效的过期时间",
"COVER_ALL": "覆盖全部项目",
"COVER_ALL_EXPLAIN": "选中此项来覆盖当前所有项目以及未来新增的项目",
"COVER_ALL_SUMMARY": "已选中当前所有项目以及未来新增的项目",
"RESET_PERMISSION": "重置初始权限",
"PERMISSION_COLUMN": "权限",
"EXPIRES_AT": "有效期至",
"VIEW_SECRET": "刷新令牌",
"LEGACY": "旧",
"CREATE_PROJECT_ROBOT": "创建机器人账号",
"CREATE_PROJECT_ROBOT_SUMMARY": "为当前项目创建机器人账号",
"EDIT_PROJECT_ROBOT": "编辑机器人账号",
"EDIT_PROJECT_ROBOT_SUMMARY": "为当前项目编辑机器人账号",
"NOT_FOUND": "未发现任何机器人账户!",
"SELECT_ALL": "全选",
"UNSELECT_ALL": "全不选",
"ROBOT_ACCOUNT_NAV": "机器人账户",
"COVERED_PROJECTS": "个项目",
"CONFIRM_SECRET": "确认令牌",
"SECRET_AGAIN": "请再次输入令牌",
"INCONSISTENT": "两次输入不一致"
}
}

View File

@ -1575,6 +1575,68 @@
"NEED_HELP": "Please ask your system admin to add a provider first"
},
"PAGINATION": {
"PAGE_SIZE": "Page size"
"PAGE_SIZE": "Page size"
},
"SYSTEM_ROBOT": {
"READ": "Read",
"CREATE": "Create",
"ARTIFACT": "Artifact",
"HELM": "Helm Chart",
"HELM_VERSION": "Helm Chart Version",
"ADD_ROBOT": "Add Robot",
"UPDATE_ROBOT": "Update Robot",
"UPDATE_ROBOT_SUCCESSFULLY": "Updated robot successfully",
"PLACEHOLDER": "Input new secret",
"SECRET": "Secret should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.",
"REFRESH_SECRET": "Refresh Secret",
"REFRESH_SECRET_SUCCESS": "Refreshed secret successfully",
"DELETE_ROBOT": "Delete Robot",
"DELETE_ROBOT_SUCCESS": "Deleted robot(s) successfully",
"ENABLE_TITLE": "Enable Robot",
"ENABLE_SUMMARY": "Do you want to enable robot {{param}}?",
"DISABLE_TITLE": "Disable Robot",
"DISABLE_SUMMARY": "Do you want to disable robot {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "Enabled robot successfully",
"DISABLE_ROBOT_SUCCESSFULLY": "Disabled robot successfully",
"ROBOT_ACCOUNT": "Robot account",
"PROJECTS": "Projects",
"ALL_PROJECTS": "All projects with",
"PERMISSIONS": "PERMISSION(S)",
"REFRESH_SECRET_SUMMARY": "Refresh the secret for this robot account",
"TOKEN": "Secret",
"NEW_TOKEN": "New Secret",
"REFRESH": "REFRESH",
"PROJECTS_MODAL_TITLE": "Projects for Robot Account",
"PROJECTS_MODAL_SUMMARY": "There are the projects covered by this robot account.",
"CREATE_ROBOT": "Create System Robot Account",
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover specific projects. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EDIT_ROBOT": "Edit System Robot Account",
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account. Choose \"Cover all projects\" to be applied to all exiting and future projects",
"EXPIRATION_TIME": "Expiration time",
"EXPIRATION_TIME_EXPLAIN": "The expiration time(in days and the starting point is creation time) of the token of the robot account. For being never expired, please enter \"-1\".",
"EXPIRATION_DEFAULT": "days(default)",
"EXPIRATION_DAYS": "Specify # of days",
"EXPIRATION_NEVER": "Never",
"EXPIRATION_REQUIRED": "Valid expiration time is required",
"COVER_ALL": "Cover all projects",
"COVER_ALL_EXPLAIN": "Check to be applied to all existing and future projects",
"COVER_ALL_SUMMARY": "All current and future projects selected.",
"RESET_PERMISSION": "RESET PERMISSIONS",
"PERMISSION_COLUMN": "Permissions",
"EXPIRES_AT": "Expires at",
"VIEW_SECRET": "REFRESH SECRET",
"LEGACY": "Legacy",
"CREATE_PROJECT_ROBOT": "Create Robot Account",
"CREATE_PROJECT_ROBOT_SUMMARY": "Create a robot account for this project",
"EDIT_PROJECT_ROBOT": "Edit Robot Account",
"EDIT_PROJECT_ROBOT_SUMMARY": "Edit a robot account for this project",
"NOT_FOUND": "We couldn't find any robots!",
"SELECT_ALL": "SELECT ALL",
"UNSELECT_ALL": "UNSELECT ALL",
"ROBOT_ACCOUNT_NAV": "Robot Accounts",
"COVERED_PROJECTS": "PROJECT(S)",
"CONFIRM_SECRET": "Confirm Secret",
"SECRET_AGAIN": "Input secret again",
"INCONSISTENT": "Two secrets are inconsistent"
}
}

View File

@ -14,4 +14,4 @@
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget1" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>
</span>
</div>
</div>
</div>