Merge pull request #6823 from pureshine/robot-account

Support Robot account in Harbor
This commit is contained in:
Fangyuan Cheng 2019-01-29 18:36:26 +08:00 committed by GitHub
commit 829bfe031e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1000 additions and 31 deletions

View File

@ -12,7 +12,7 @@
@include text-overflow; @include text-overflow;
} }
@mixin grid-left-top-pos{ @mixin grid-right-top-pos{
position: absolute; position: absolute;
z-index: 100; z-index: 100;
right: 35px; right: 35px;

View File

@ -12,7 +12,7 @@ $size60:60px;
.toolbar { .toolbar {
overflow: hidden; overflow: hidden;
.rightPos { .rightPos {
@include grid-left-top-pos; @include grid-right-top-pos;
margin-top: 20px; margin-top: 20px;
.filter-divider { .filter-divider {
display: inline-block; display: inline-block;

View File

@ -16,7 +16,7 @@
.toolbar { .toolbar {
overflow: hidden; overflow: hidden;
.rightPos { .rightPos {
@include grid-left-top-pos; @include grid-right-top-pos;
.filter-divider { .filter-divider {
display: inline-block; display: inline-block;
height: 16px; height: 16px;

View File

@ -1,7 +1,7 @@
@import '../mixin'; @import '../mixin';
.rightPos{ .rightPos{
@include grid-left-top-pos; @include grid-right-top-pos;
} }
.toolbar { .toolbar {

View File

@ -33,6 +33,7 @@ export const enum ConfirmationTargets {
PROJECT, PROJECT,
PROJECT_MEMBER, PROJECT_MEMBER,
USER, USER,
ROBOT_ACCOUNT,
POLICY, POLICY,
TOGGLE_CONFIRM, TOGGLE_CONFIRM,
TARGET, TARGET,

View File

@ -11,27 +11,24 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { CoreModule } from '../core/core.module'; import { CoreModule } from "../core/core.module";
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from "../shared/shared.module";
import { ConfigurationComponent } from './config.component';
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 { GcComponent } from './gc/gc.component';
import { GcRepoService } from './gc/gc.service';
import { GcApiRepository } from './gc/gc.api.repository';
import { GcViewModelFactory } from './gc/gc.viewmodel.factory';
import { GcUtility } from './gc/gc.utility';
import { ConfigurationComponent } from "./config.component";
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 { GcComponent } from "./gc/gc.component";
import { GcRepoService } from "./gc/gc.service";
import { GcApiRepository } from "./gc/gc.api.repository";
import { RobotApiRepository } from "../project/robot-account/robot.api.repository";
import { GcViewModelFactory } from "./gc/gc.viewmodel.factory";
import { GcUtility } from "./gc/gc.utility";
@NgModule({ @NgModule({
imports: [ imports: [CoreModule, SharedModule],
CoreModule,
SharedModule
],
declarations: [ declarations: [
ConfigurationComponent, ConfigurationComponent,
ConfigurationAuthComponent, ConfigurationAuthComponent,
@ -39,6 +36,14 @@ import { GcUtility } from './gc/gc.utility';
GcComponent GcComponent
], ],
exports: [ConfigurationComponent], exports: [ConfigurationComponent],
providers: [ConfigurationService, GcRepoService, GcApiRepository, GcViewModelFactory, GcUtility, ConfirmMessageHandler] providers: [
ConfigurationService,
GcRepoService,
GcApiRepository,
GcViewModelFactory,
GcUtility,
ConfirmMessageHandler,
RobotApiRepository
]
}) })
export class ConfigurationModule { } export class ConfigurationModule {}

View File

@ -44,6 +44,7 @@ import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-reposit
import { ProjectComponent } from './project/project.component'; import { ProjectComponent } from './project/project.component';
import { ProjectDetailComponent } from './project/project-detail/project-detail.component'; import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { MemberComponent } from './project/member/member.component'; import { MemberComponent } from './project/member/member.component';
import { RobotAccountComponent } from './project/robot-account/robot-account.component';
import { ProjectLabelComponent } from "./project/project-label/project-label.component"; import { ProjectLabelComponent } from "./project/project-label/project-label.component";
import { ProjectConfigComponent } from './project/project-config/project-config.component'; import { ProjectConfigComponent } from './project/project-config/project-config.component';
import { ProjectRoutingResolver } from './project/project-routing-resolver.service'; import { ProjectRoutingResolver } from './project/project-routing-resolver.service';
@ -178,6 +179,10 @@ const harborRoutes: Routes = [
{ {
path: 'configs', path: 'configs',
component: ProjectConfigComponent component: ProjectConfigComponent
},
{
path: 'robot-account',
component: RobotAccountComponent
} }
] ]
}, },

View File

@ -22,6 +22,9 @@
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a> <a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="robot-account" routerLinkActive="active">{{'PROJECT_DETAIL.ROBOT_ACCOUNTS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)"> <li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)">
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a> <a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
</li> </li>

View File

@ -30,6 +30,7 @@ import { AddGroupComponent } from './member/add-group/add-group.component';
import { ProjectService } from './project.service'; import { ProjectService } from './project.service';
import { MemberService } from './member/member.service'; import { MemberService } from './member/member.service';
import { RobotService } from './robot-account/robot-account.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive'; import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
@ -37,6 +38,8 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co
import { ListChartsComponent } from './list-charts/list-charts.component'; import { ListChartsComponent } from './list-charts/list-charts.component';
import { ListChartVersionsComponent } from './list-chart-versions/list-chart-versions.component'; import { ListChartVersionsComponent } from './list-chart-versions/list-chart-versions.component';
import { ChartDetailComponent } from './chart-detail/chart-detail.component'; import { ChartDetailComponent } from './chart-detail/chart-detail.component';
import { RobotAccountComponent } from './robot-account/robot-account.component';
import { AddRobotComponent } from './robot-account/add-robot/add-robot.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -58,10 +61,12 @@ import { ChartDetailComponent } from './chart-detail/chart-detail.component';
AddGroupComponent, AddGroupComponent,
ListChartsComponent, ListChartsComponent,
ListChartVersionsComponent, ListChartVersionsComponent,
ChartDetailComponent ChartDetailComponent,
RobotAccountComponent,
AddRobotComponent
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService] providers: [ProjectRoutingResolver, ProjectService, MemberService, RobotService]
}) })
export class ProjectModule { export class ProjectModule {

View File

@ -0,0 +1,102 @@
<clr-modal [(clrModalOpen)]="addRobotOpened"
[clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}</h3>
<div class="modal-body">
<form #robotForm="ngForm">
<section class="form-block">
<div class="form-group">
<label class="col-md-3
form-group-label-override required" for="robot_name">
{{'ROBOT_ACCOUNT.NAME' | translate}}
</label>
<label aria-haspopup="true" role="tooltip" class="tooltip
tooltip-validation
tooltip-md tooltip-bottom-left" for="robot_name"
[class.invalid]="!isRobotNameValid">
<input type="text"
[(ngModel)]="robot.name"
size="30" class="input-width"
name="robot_name"
id="robot_name"
#robotName="ngModel"
required
pattern='[^" ~#$%]+'
maxLengthExt="255"
autocomplete="off"
(keyup)='handleValidation()'>
<span class="tooltip-content">
{{ nameTooltipText | translate }}
</span>
</label>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>
<div class="form-group">
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' |
translate}}</label>
<input type="text" size="255" class="input-width"
[(ngModel)]="robot.description"
name="robot_desc" id="robot_desc">
</div>
<div class="form-group clr-form-control rule-width">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="true"
[(ngModel)]="robot.access.isPull" name="isPull"
id="permission-pull" class="clr-checkbox">
<label for="permission-pull" class="clr-control-label">
{{'ROBOT_ACCOUNT.PULL_PERMISSION' | translate}}
</label>
</clr-checkbox-wrapper>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="true"
[(ngModel)]="robot.access.isPush" name="isPush"
id="permission-push" class="clr-checkbox">
<label for="permission-push" class="clr-control-label">
{{'ROBOT_ACCOUNT.PUSH_PERMISSION' | translate}}
</label>
</clr-checkbox-wrapper>
</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>
</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>
</section>
</div>
</clr-modal>

View File

@ -0,0 +1,31 @@
.rule-width {
width: 100%;
}
.input-width {
width: 200px;
}
.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;
}
}
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddRobotComponent } from './add-robot.component';
describe('AddRobotComponent', () => {
let component: AddRobotComponent;
let fixture: ComponentFixture<AddRobotComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddRobotComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddRobotComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,191 @@
import {
Component,
OnInit,
Input,
ViewChild,
OnDestroy,
Output,
EventEmitter,
ChangeDetectorRef
} 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 { ErrorHandler } from "@harbor/ui";
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
@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;
isSubmitOnGoing = false;
closable: boolean = false;
staticBackdrop: boolean = true;
isPull: boolean;
isPush: boolean;
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;
@Input() projectId: number;
@Input() projectName: string;
@Output() create = new EventEmitter<boolean>();
@ViewChild("robotForm") currentForm: NgForm;
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
constructor(
private robotService: RobotService,
private translate: TranslateService,
private errorHandler: ErrorHandler,
private cdr: ChangeDetectorRef,
private messageHandlerService: MessageHandlerService
) {}
ngOnInit(): void {
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";
}
}
});
}
openAddRobotModal(): void {
if (this.isSubmitOnGoing) {
return;
}
this.robot.name = "";
this.robot.description = "";
this.addRobotOpened = true;
this.isRobotNameValid = true;
this.robot = new Robot();
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
}
onCancel(): void {
this.addRobotOpened = false;
}
ngOnDestroy(): void {
this.robotNameChecker.unsubscribe();
}
onSubmit(): void {
if (this.isSubmitOnGoing) {
return;
}
this.isSubmitOnGoing = true;
this.robotService
.addRobotAccount(
this.projectId,
this.robot.name,
this.robot.description,
this.projectName,
this.robot.access.isPull,
this.robot.access.isPush
)
.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;
},
error => {
this.isSubmitOnGoing = false;
this.errorHandler.error(error);
}
);
}
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.isPush && !this.robot.access.isPull)
);
}
}
// Handle the form validation
handleValidation(): void {
let cont = this.currentForm.controls["robot_name"];
if (cont) {
this.robotNameChecker.next(cont.value);
}
}
onCpError($event: any): void {
if (this.copyAlert) {
this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR");
}
}
onCpSuccess($event: any): void {
this.copyToken = false;
this.translate
.get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount })
.subscribe((res: string) => {
this.messageHandlerService.showSuccess(res);
});
}
}

View File

@ -0,0 +1,74 @@
<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}}'
(filterEvt)="doSearch($event)" [currentValue]="searchRobot"></hbr-filter>
<span class="refresh-btn" (click)="retrieve()">
<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 class="btn btn-sm btn-secondary"
(click)="openAddRobotModal()">
<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-sm
btn-link"
clrDropdownTrigger>
<span>{{'MEMBER.ACTION' | translate}}<clr-icon shape="caret
down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<button clrDropdownItem [disabled]="!(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>
<div class="dropdown-divider"></div>
<button clrDropdownItem
(click)="openDeleteRobotsDialog(selectedRow)"
[disabled]="!selectedRow.length">{{'ROBOT_ACCOUNT.DELETE'
| translate}}</button>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-datagrid [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading">
<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-row *clrDgItems="let r of robots" [clrDgItem]="r">
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="r.disabled">
<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-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}}
-
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<add-robot [projectId]="projectId" [projectName]="projectName"
(create)="createAccount($event)"></add-robot>
</div>

View File

@ -0,0 +1,28 @@
@import "../../../../lib/src/mixin";
.robot-space {
margin-top: 12px;
position: relative;
clr-icon.red-position {
margin-left: 2px;
}
.rightPos {
@include grid-right-top-pos;
.option-left {
padding-left: 16px;
position: relative;
top: 10px;
}
.option-right {
padding-right: 16px;
.refresh-btn {
cursor: pointer;
}
}
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RobotAccountComponent } from './robot-account.component';
describe('RobotAccountComponent', () => {
let component: RobotAccountComponent;
let fixture: ComponentFixture<RobotAccountComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RobotAccountComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RobotAccountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,201 @@
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 } 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 {
ConfirmationTargets,
ConfirmationState,
ConfirmationButtons
} from "../../shared/shared.const";
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import {
operateChanges,
OperateInfo,
OperationService,
OperationState
} from "@harbor/ui";
@Component({
selector: "app-robot-account",
templateUrl: "./robot-account.component.html",
styleUrls: ["./robot-account.component.scss"]
})
export class RobotAccountComponent implements OnInit, OnDestroy {
@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;
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private robotService: RobotService,
private OperateDialogService: ConfirmationDialogService,
private operationService: OperationService,
private translate: TranslateService,
private ref: ChangeDetectorRef,
private messageHandlerService: MessageHandlerService
) {
this.subscription = OperateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.ROBOT_ACCOUNT
) {
this.delRobots(message.data);
}
}
);
this.forceRefreshView(2000);
}
ngOnInit(): void {
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();
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = 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;
}
let robotsDelete$ = robots.map(robot => this.delOperate(robot));
forkJoin(robotsDelete$)
.pipe(
catchError(err => throwError(err)),
finalize(() => {
this.retrieve();
this.selectedRow = [];
})
)
.subscribe(() => {});
}
delOperate(robot: Robot) {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = "OPERATION.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),
err => operateChanges(operMessage, OperationState.failure, err)
)
);
}
createAccount(created: boolean): void {
if (created) {
this.retrieve();
}
}
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();
});
}
}

View File

@ -0,0 +1,64 @@
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 { Http } from "@angular/http";
import { RobotApiRepository } from "./robot.api.repository";
@Injectable()
export class RobotService {
constructor(
private http: Http,
private robotApiRepository: RobotApiRepository
) {}
public addRobotAccount(projecId, name, description, projectName, isPull, isPush): Observable<any> {
let access = [];
if ( isPull ) {
access.push({"resource": "/project/" + projecId + "/repository", "action": "pull"});
access.push({"resource": "/project/" + projectName + "/repository", "action": "pull"});
}
if ( isPush ) {
access.push({"resource": "/project/" + projecId + "/repository", "action": "push"});
access.push({"resource": "/project/" + projectName + "/repository", "action": "push"});
}
let param = {
name: name,
description: description,
access: access
};
return this.robotApiRepository.postRobot(projecId, param);
}
public deleteRobotAccount(projecId, id): Observable<any> {
return this.robotApiRepository.deleteRobot(projecId, id);
}
public listRobotAccount(projecId): Observable<any> {
return this.robotApiRepository.listRobot(projecId);
}
public getRobotAccount(projecId, id): Observable<any> {
return this.robotApiRepository.getRobot(projecId, id);
}
public toggleDisabledAccount(projecId, id, isDisabled): Observable<any> {
let data = {
Disabled: isDisabled
};
return this.robotApiRepository.toggleDisabledAccount(projecId, id, data);
}
}

View File

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

View File

@ -0,0 +1,20 @@
export class Robot {
project_id: number;
id: number;
name: string;
description: string;
disabled: boolean;
access: {
isPull: boolean;
isPush: boolean;
};
constructor () {
this.access = <any>{};
// this.access[0].action = true;
this.access.isPull = true;
this.access.isPush = true;
}
}

View File

@ -17,6 +17,7 @@
vertical-align: middle; vertical-align: middle;
width: 80%; width: 80%;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all;
} }
.batchInfoUl{ .batchInfoUl{
padding: 20px; list-style-type: none; padding: 20px; list-style-type: none;

View File

@ -35,6 +35,7 @@ export const enum ConfirmationTargets {
EMPTY, EMPTY,
PROJECT, PROJECT,
PROJECT_MEMBER, PROJECT_MEMBER,
ROBOT_ACCOUNT,
USER, USER,
POLICY, POLICY,
TOGGLE_CONFIRM, TOGGLE_CONFIRM,

View File

@ -194,7 +194,8 @@
"LABELS": "Labels", "LABELS": "Labels",
"PROJECTS": "Projects", "PROJECTS": "Projects",
"CONFIG": "Configuration", "CONFIG": "Configuration",
"HELMCHART": "Helm Charts" "HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts"
}, },
"PROJECT_CONFIG": { "PROJECT_CONFIG": {
"REGISTRY": "Project registry", "REGISTRY": "Project registry",
@ -258,6 +259,31 @@
"SET_ROLE": "SET ROLE", "SET_ROLE": "SET ROLE",
"REMOVE": "Remove" "REMOVE": "Remove"
}, },
"ROBOT_ACCOUNT": {
"NAME": "Name",
"TOKEN": "Token",
"NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT",
"ENABLED_STATE": "Enabled state",
"DESCRIPTION": "Description",
"ACTION": "Action",
"EDIT": "Edit",
"ITEMS": "items",
"OF": "of",
"DISABLE_ACCOUNT": "Disable Account",
"ENABLE_ACCOUNT": "Enable Account",
"DELETE": "Delete",
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
"PULL_PERMISSION": "Permission for Pull",
"PUSH_PERMISSION": "Permission for Push",
"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",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
},
"GROUP": { "GROUP": {
"GROUP": "Group", "GROUP": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
@ -802,6 +828,7 @@
"DELETE_REPO": "Delete repository", "DELETE_REPO": "Delete repository",
"DELETE_TAG": "Delete tag", "DELETE_TAG": "Delete tag",
"DELETE_USER": "Delete user", "DELETE_USER": "Delete user",
"DELETE_ROBOT": "Delete robot",
"DELETE_REGISTRY": "Delete registry", "DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication", "DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete user member", "DELETE_MEMBER": "Delete user member",

View File

@ -194,7 +194,8 @@
"LABELS": "Labels", "LABELS": "Labels",
"PROJECTS": "Proyectos", "PROJECTS": "Proyectos",
"CONFIG": "Configuración", "CONFIG": "Configuración",
"HELMCHART": "Helm Charts" "HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts"
}, },
"PROJECT_CONFIG": { "PROJECT_CONFIG": {
"REGISTRY": "Registro de proyectos", "REGISTRY": "Registro de proyectos",
@ -258,6 +259,31 @@
"SET_ROLE": "SET ROLE", "SET_ROLE": "SET ROLE",
"REMOVE": "Remove" "REMOVE": "Remove"
}, },
"ROBOT_ACCOUNT": {
"NAME": "Name",
"TOKEN": "Token",
"NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT",
"ENABLED_STATE": "Enabled state",
"DESCRIPTION": "Description",
"ACTION": "Action",
"EDIT": "Edit",
"ITEMS": "items",
"OF": "of",
"DISABLE_ACCOUNT": "Disable Account",
"ENABLE_ACCOUNT": "Enable Account",
"DELETE": "Delete",
"CREAT_ROBOT_ACCOUNT": "Creat Robot Account",
"PULL_PERMISSION": "Permission for Pull",
"PUSH_PERMISSION": "Permission for Push",
"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",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
},
"GROUP": { "GROUP": {
"GROUP": "Group", "GROUP": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
@ -802,6 +828,7 @@
"DELETE_REPO": "Delete repository", "DELETE_REPO": "Delete repository",
"DELETE_TAG": "Delete tag", "DELETE_TAG": "Delete tag",
"DELETE_USER": "Delete user", "DELETE_USER": "Delete user",
"DELETE_ROBOT": "Delete robot",
"DELETE_REGISTRY": "Delete registry", "DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication", "DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete user member", "DELETE_MEMBER": "Delete user member",

View File

@ -180,7 +180,8 @@
"LABELS": "Labels", "LABELS": "Labels",
"PROJECTS": "Projets", "PROJECTS": "Projets",
"CONFIG": "Configuration", "CONFIG": "Configuration",
"HELMCHART": "Helm Charts" "HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts"
}, },
"PROJECT_CONFIG": { "PROJECT_CONFIG": {
"REGISTRY": "Dépôt du Projet", "REGISTRY": "Dépôt du Projet",
@ -242,6 +243,31 @@
"SET_ROLE": "SET ROLE", "SET_ROLE": "SET ROLE",
"REMOVE": "Remove" "REMOVE": "Remove"
}, },
"ROBOT_ACCOUNT": {
"NAME": "Nom",
"TOKEN": "gage ",
"NEW_ROBOT_ACCOUNT": "nouveau robot compte ",
"ENABLED_STATE": "état d 'activation",
"DESCRIPTION": "Description",
"ACTION": "Action",
"EDIT": "Edit",
"ITEMS": "items",
"OF": "of",
"DISABLE_ACCOUNT": "désactiver le compte ",
"ENABLE_ACCOUNT": "permettre à compte ",
"DELETE": "Supprimer",
"CREAT_ROBOT_ACCOUNT": "créat robot compte ",
"PULL_PERMISSION": "Permission for Pull",
"PUSH_PERMISSION": "Permission for Push",
"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",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?"
},
"GROUP": { "GROUP": {
"Group": "Group", "Group": "Group",
"GROUPS": "Groups", "GROUPS": "Groups",
@ -765,6 +791,7 @@
"DELETE_REPO": "Delete repository", "DELETE_REPO": "Delete repository",
"DELETE_TAG": "Delete tag", "DELETE_TAG": "Delete tag",
"DELETE_USER": "Delete user", "DELETE_USER": "Delete user",
"DELETE_ROBOT": "Delete robot",
"DELETE_REGISTRY": "Delete registry", "DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication", "DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete member", "DELETE_MEMBER": "Delete member",

View File

@ -192,7 +192,8 @@
"LABELS": "Etiquetas", "LABELS": "Etiquetas",
"PROJECTS": "Projetos", "PROJECTS": "Projetos",
"CONFIG": "Configuração", "CONFIG": "Configuração",
"HELMCHART": "Helm Charts" "HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts"
}, },
"PROJECT_CONFIG": { "PROJECT_CONFIG": {
"REGISTRY": "Registro do Projeto", "REGISTRY": "Registro do Projeto",
@ -256,6 +257,31 @@
"SET_ROLE": "DEFINIR FUNÇÃO", "SET_ROLE": "DEFINIR FUNÇÃO",
"REMOVE": "Remover" "REMOVE": "Remover"
}, },
"ROBOT_ACCOUNT": {
"NAME": "Nome",
"TOKEN": "Token",
"NEW_ROBOT_ACCOUNT": "Novo robô conta",
"ENABLED_STATE": "Enabled state",
"DESCRIPTION": "Descrição",
"ACTION": "AÇÃO",
"EDIT": "Editar",
"ITEMS": "itens",
"OF": "de",
"DISABLE_ACCOUNT": "Desactivar a conta",
"ENABLE_ACCOUNT": "Ativar conta",
"DELETE": "Remover",
"CREAT_ROBOT_ACCOUNT": "CRIA robô conta",
"PULL_PERMISSION": "Permission for Pull",
"PUSH_PERMISSION": "Permission for Push",
"FILTER_PLACEHOLDER": "Filtro robot accounts",
"ROBOT_NAME": "Não Pode conter caracteres especiais(~#$%) e comprimento máximo deveria ser 255 caracteres.",
"ACCOUNT_EXISTING": "Robô conta já existe.",
"ALERT_TEXT": "É só copiar o token de acesso Pessoal não VAI ter outra oportunidade.",
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirmar a remoção do robô Contas",
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?"
},
"GROUP": { "GROUP": {
"GROUP": "Grupo", "GROUP": "Grupo",
"GROUPS": "Grupos", "GROUPS": "Grupos",
@ -792,6 +818,7 @@
"DELETE_REPO": "Remover repositório", "DELETE_REPO": "Remover repositório",
"DELETE_TAG": "Remover tag", "DELETE_TAG": "Remover tag",
"DELETE_USER": "Remover usuário", "DELETE_USER": "Remover usuário",
"DELETE_ROBOT": "Delete robot",
"DELETE_REGISTRY": "Remover registry", "DELETE_REGISTRY": "Remover registry",
"DELETE_REPLICATION": "Remover replicação", "DELETE_REPLICATION": "Remover replicação",
"DELETE_MEMBER": "Remover usuário membro", "DELETE_MEMBER": "Remover usuário membro",

View File

@ -193,7 +193,8 @@
"LABELS": "标签", "LABELS": "标签",
"PROJECTS": "项目", "PROJECTS": "项目",
"CONFIG": "配置管理", "CONFIG": "配置管理",
"HELMCHART": "Helm Charts" "HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "机器人账户"
}, },
"PROJECT_CONFIG": { "PROJECT_CONFIG": {
"REGISTRY": "项目仓库", "REGISTRY": "项目仓库",
@ -257,6 +258,31 @@
"SET_ROLE": "设置角色", "SET_ROLE": "设置角色",
"REMOVE": "移除成员" "REMOVE": "移除成员"
}, },
"ROBOT_ACCOUNT": {
"NAME": "姓名",
"TOKEN": "令牌",
"NEW_ROBOT_ACCOUNT": "添加机器人账户",
"ENABLED_STATE": "启用状态",
"DESCRIPTION": "描述",
"ACTION": "操作",
"EDIT": "编辑",
"OF": "共计",
"ITEMS": "条记录",
"DISABLE_ACCOUNT": "禁用账户",
"ENABLE_ACCOUNT": "启用账户",
"DELETE": "删除",
"CREAT_ROBOT_ACCOUNT": "创建机器人账户",
"PULL_PERMISSION": "Pull 权限",
"PUSH_PERMISSION": "Push 权限",
"FILTER_PLACEHOLDER": "过滤机器人账户",
"ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255.",
"ACCOUNT_EXISTING": "机器人账户已经存在.",
"ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会",
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功.",
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认",
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?"
},
"GROUP": { "GROUP": {
"GROUP": "组", "GROUP": "组",
"GROUPS": "组", "GROUPS": "组",
@ -800,6 +826,7 @@
"DELETE_REPO": "删除仓库", "DELETE_REPO": "删除仓库",
"DELETE_TAG": "删除镜像标签", "DELETE_TAG": "删除镜像标签",
"DELETE_USER": "删除用户", "DELETE_USER": "删除用户",
"DELETE_ROBOT": "删除账户",
"DELETE_REGISTRY": "删除Registry", "DELETE_REGISTRY": "删除Registry",
"DELETE_REPLICATION": "删除复制", "DELETE_REPLICATION": "删除复制",
"DELETE_MEMBER": "删除用户成员", "DELETE_MEMBER": "删除用户成员",

View File

@ -79,3 +79,11 @@ body {
.datagrid-header{ .datagrid-header{
z-index: 1 !important; z-index: 1 !important;
} }
.color-green {
color: #1D5100;
}
.color-red {
color: red;
}