From 404ee307f35125bffa2322d2efa98fc42464395a Mon Sep 17 00:00:00 2001 From: FangyuanCheng Date: Thu, 17 Jan 2019 17:16:07 +0800 Subject: [PATCH] Support Robot account in Harbor Signed-off-by: FangyuanCheng --- src/portal/lib/src/_mixin.scss | 2 +- .../src/helm-chart/helm-chart.component.scss | 2 +- .../helm-chart-version.component.scss | 2 +- .../repository-gridview.component.scss | 2 +- src/portal/lib/src/shared/shared.const.ts | 1 + src/portal/src/app/config/config.module.ts | 45 ++-- src/portal/src/app/harbor-routing.module.ts | 5 + .../project-detail.component.html | 3 + src/portal/src/app/project/project.module.ts | 9 +- .../add-robot/add-robot.component.html | 102 +++++++++ .../add-robot/add-robot.component.scss | 31 +++ .../add-robot/add-robot.component.spec.ts | 25 +++ .../add-robot/add-robot.component.ts | 191 +++++++++++++++++ .../robot-account.component.html | 74 +++++++ .../robot-account.component.scss | 28 +++ .../robot-account.component.spec.ts | 25 +++ .../robot-account/robot-account.component.ts | 201 ++++++++++++++++++ .../robot-account/robot-account.service.ts | 64 ++++++ .../robot-account/robot.api.repository.ts | 44 ++++ .../src/app/project/robot-account/robot.ts | 20 ++ .../confirmation-dialog.component.scss | 1 + src/portal/src/app/shared/shared.const.ts | 1 + src/portal/src/i18n/lang/en-us-lang.json | 29 ++- src/portal/src/i18n/lang/es-es-lang.json | 29 ++- src/portal/src/i18n/lang/fr-fr-lang.json | 29 ++- src/portal/src/i18n/lang/pt-br-lang.json | 29 ++- src/portal/src/i18n/lang/zh-cn-lang.json | 29 ++- src/portal/src/styles.css | 8 + 28 files changed, 1000 insertions(+), 31 deletions(-) create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.html create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.html create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.scss create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.spec.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.service.ts create mode 100644 src/portal/src/app/project/robot-account/robot.api.repository.ts create mode 100644 src/portal/src/app/project/robot-account/robot.ts diff --git a/src/portal/lib/src/_mixin.scss b/src/portal/lib/src/_mixin.scss index ea15edcc6..a29a9c325 100644 --- a/src/portal/lib/src/_mixin.scss +++ b/src/portal/lib/src/_mixin.scss @@ -12,7 +12,7 @@ @include text-overflow; } -@mixin grid-left-top-pos{ +@mixin grid-right-top-pos{ position: absolute; z-index: 100; right: 35px; diff --git a/src/portal/lib/src/helm-chart/helm-chart.component.scss b/src/portal/lib/src/helm-chart/helm-chart.component.scss index cbbb50b7e..80a7590f9 100644 --- a/src/portal/lib/src/helm-chart/helm-chart.component.scss +++ b/src/portal/lib/src/helm-chart/helm-chart.component.scss @@ -12,7 +12,7 @@ $size60:60px; .toolbar { overflow: hidden; .rightPos { - @include grid-left-top-pos; + @include grid-right-top-pos; margin-top: 20px; .filter-divider { display: inline-block; diff --git a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss index 27a21a5d1..2459e977b 100644 --- a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss +++ b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss @@ -16,7 +16,7 @@ .toolbar { overflow: hidden; .rightPos { - @include grid-left-top-pos; + @include grid-right-top-pos; .filter-divider { display: inline-block; height: 16px; diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.scss b/src/portal/lib/src/repository-gridview/repository-gridview.component.scss index f345da744..8ee3cbb31 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.scss +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.scss @@ -1,7 +1,7 @@ @import '../mixin'; .rightPos{ - @include grid-left-top-pos; + @include grid-right-top-pos; } .toolbar { diff --git a/src/portal/lib/src/shared/shared.const.ts b/src/portal/lib/src/shared/shared.const.ts index bb91dbc1c..3c6c83fee 100644 --- a/src/portal/lib/src/shared/shared.const.ts +++ b/src/portal/lib/src/shared/shared.const.ts @@ -33,6 +33,7 @@ export const enum ConfirmationTargets { PROJECT, PROJECT_MEMBER, USER, + ROBOT_ACCOUNT, POLICY, TOGGLE_CONFIRM, TARGET, diff --git a/src/portal/src/app/config/config.module.ts b/src/portal/src/app/config/config.module.ts index cecf8f6fa..7556251fc 100644 --- a/src/portal/src/app/config/config.module.ts +++ b/src/portal/src/app/config/config.module.ts @@ -11,27 +11,24 @@ // 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 { NgModule } from '@angular/core'; -import { CoreModule } from '../core/core.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 { NgModule } from "@angular/core"; +import { CoreModule } from "../core/core.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 { RobotApiRepository } from "../project/robot-account/robot.api.repository"; +import { GcViewModelFactory } from "./gc/gc.viewmodel.factory"; +import { GcUtility } from "./gc/gc.utility"; @NgModule({ - imports: [ - CoreModule, - SharedModule - ], + imports: [CoreModule, SharedModule], declarations: [ ConfigurationComponent, ConfigurationAuthComponent, @@ -39,6 +36,14 @@ import { GcUtility } from './gc/gc.utility'; GcComponent ], exports: [ConfigurationComponent], - providers: [ConfigurationService, GcRepoService, GcApiRepository, GcViewModelFactory, GcUtility, ConfirmMessageHandler] + providers: [ + ConfigurationService, + GcRepoService, + GcApiRepository, + GcViewModelFactory, + GcUtility, + ConfirmMessageHandler, + RobotApiRepository + ] }) -export class ConfigurationModule { } +export class ConfigurationModule {} diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index 5eab2b3c1..4ec78ac7e 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -44,6 +44,7 @@ import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-reposit import { ProjectComponent } from './project/project.component'; import { ProjectDetailComponent } from './project/project-detail/project-detail.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 { ProjectConfigComponent } from './project/project-config/project-config.component'; import { ProjectRoutingResolver } from './project/project-routing-resolver.service'; @@ -178,6 +179,10 @@ const harborRoutes: Routes = [ { path: 'configs', component: ProjectConfigComponent + }, + { + path: 'robot-account', + component: RobotAccountComponent } ] }, diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 0b6853a70..7b43be2ca 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -22,6 +22,9 @@ + diff --git a/src/portal/src/app/project/project.module.ts b/src/portal/src/app/project/project.module.ts index cacb0b4e9..96ef8aa88 100644 --- a/src/portal/src/app/project/project.module.ts +++ b/src/portal/src/app/project/project.module.ts @@ -30,6 +30,7 @@ import { AddGroupComponent } from './member/add-group/add-group.component'; import { ProjectService } from './project.service'; import { MemberService } from './member/member.service'; +import { RobotService } from './robot-account/robot-account.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service'; 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 { ListChartVersionsComponent } from './list-chart-versions/list-chart-versions.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({ imports: [ @@ -58,10 +61,12 @@ import { ChartDetailComponent } from './chart-detail/chart-detail.component'; AddGroupComponent, ListChartsComponent, ListChartVersionsComponent, - ChartDetailComponent + ChartDetailComponent, + RobotAccountComponent, + AddRobotComponent ], exports: [ProjectComponent, ListProjectComponent], - providers: [ProjectRoutingResolver, ProjectService, MemberService] + providers: [ProjectRoutingResolver, ProjectService, MemberService, RobotService] }) export class ProjectModule { diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html new file mode 100644 index 000000000..87bc66d20 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html @@ -0,0 +1,102 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss new file mode 100644 index 000000000..7271c7727 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss @@ -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; + } + } + } +} diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts new file mode 100644 index 000000000..0f45bd6c2 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddRobotComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddRobotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts new file mode 100644 index 000000000..61922c4e1 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts @@ -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 = new Subject(); + nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME"; + robotForm: NgForm; + @Input() projectId: number; + @Input() projectName: string; + @Output() create = new EventEmitter(); + @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); + }); + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.component.html b/src/portal/src/app/project/robot-account/robot-account.component.html new file mode 100644 index 000000000..a1a55a440 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.html @@ -0,0 +1,74 @@ +
+
+
+
+
+
+ + + + +
+
+
+
+ + + + {{'MEMBER.ACTION' | translate}} + + + + + + + + + {{'ROBOT_ACCOUNT.NAME' | translate}} + {{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}} + {{'ROBOT_ACCOUNT.DESCRIPTION' | translate}} + + {{r.name}} + + + + + {{r.description}} + + + {{pagination.firstItem + 1}} + - + {{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' | + translate}} + {{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}} + + + +
+ +
diff --git a/src/portal/src/app/project/robot-account/robot-account.component.scss b/src/portal/src/app/project/robot-account/robot-account.component.scss new file mode 100644 index 000000000..9278019c0 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.scss @@ -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; + } + } + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.component.spec.ts b/src/portal/src/app/project/robot-account/robot-account.component.spec.ts new file mode 100644 index 000000000..2a1d3c303 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RobotAccountComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RobotAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/project/robot-account/robot-account.component.ts b/src/portal/src/app/project/robot-account/robot-account.component.ts new file mode 100644 index 000000000..006a59d81 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.ts @@ -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 = 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(); + }); + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.service.ts b/src/portal/src/app/project/robot-account/robot-account.service.ts new file mode 100644 index 000000000..0edd89390 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.service.ts @@ -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 { + 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 { + return this.robotApiRepository.deleteRobot(projecId, id); + } + + public listRobotAccount(projecId): Observable { + return this.robotApiRepository.listRobot(projecId); + } + + public getRobotAccount(projecId, id): Observable { + return this.robotApiRepository.getRobot(projecId, id); + } + + public toggleDisabledAccount(projecId, id, isDisabled): Observable { + let data = { + Disabled: isDisabled + }; + return this.robotApiRepository.toggleDisabledAccount(projecId, id, data); + } +} diff --git a/src/portal/src/app/project/robot-account/robot.api.repository.ts b/src/portal/src/app/project/robot-account/robot.api.repository.ts new file mode 100644 index 000000000..308e658cb --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot.api.repository.ts @@ -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 { + return this.http + .post(`/api/projects/${projectId}/robots`, param) + .pipe(map(response => response.json())) + .pipe(catchError(error => observableThrowError(error))); + } + + public deleteRobot(projectId, id): Observable { + return this.http + .delete(`/api/projects/${projectId}/robots/${id}`) + .pipe(catchError(error => observableThrowError(error))); + } + + public listRobot(projectId): Observable { + 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 { + 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 { + return this.http + .put(`/api/projects/${projectId}/robots/${id}`, data) + .pipe(catchError(error => observableThrowError(error))); + } +} diff --git a/src/portal/src/app/project/robot-account/robot.ts b/src/portal/src/app/project/robot-account/robot.ts new file mode 100644 index 000000000..f3ff1ffef --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot.ts @@ -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 = {}; + // this.access[0].action = true; + this.access.isPull = true; + this.access.isPush = true; + } +} + diff --git a/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss b/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss index da03240da..4ed1cbdbe 100644 --- a/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss +++ b/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss @@ -17,6 +17,7 @@ vertical-align: middle; width: 80%; white-space: pre-wrap; + word-break: break-all; } .batchInfoUl{ padding: 20px; list-style-type: none; diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index ac263e44d..7cefb9dec 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -35,6 +35,7 @@ export const enum ConfirmationTargets { EMPTY, PROJECT, PROJECT_MEMBER, + ROBOT_ACCOUNT, USER, POLICY, TOGGLE_CONFIRM, diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 907c94bd4..5ea73b524 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -194,7 +194,8 @@ "LABELS": "Labels", "PROJECTS": "Projects", "CONFIG": "Configuration", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Project registry", @@ -258,6 +259,31 @@ "SET_ROLE": "SET ROLE", "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", "GROUPS": "Groups", @@ -802,6 +828,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete user member", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 62a5f5061..b286dd804 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -194,7 +194,8 @@ "LABELS": "Labels", "PROJECTS": "Proyectos", "CONFIG": "Configuración", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Registro de proyectos", @@ -258,6 +259,31 @@ "SET_ROLE": "SET ROLE", "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", "GROUPS": "Groups", @@ -802,6 +828,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete user member", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 0efefb39a..13c03ec0b 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -180,7 +180,8 @@ "LABELS": "Labels", "PROJECTS": "Projets", "CONFIG": "Configuration", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Dépôt du Projet", @@ -242,6 +243,31 @@ "SET_ROLE": "SET ROLE", "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", "GROUPS": "Groups", @@ -765,6 +791,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete member", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 072781b5f..7cd47fef4 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -192,7 +192,8 @@ "LABELS": "Etiquetas", "PROJECTS": "Projetos", "CONFIG": "Configuração", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Registro do Projeto", @@ -256,6 +257,31 @@ "SET_ROLE": "DEFINIR FUNÇÃO", "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": "Grupo", "GROUPS": "Grupos", @@ -792,6 +818,7 @@ "DELETE_REPO": "Remover repositório", "DELETE_TAG": "Remover tag", "DELETE_USER": "Remover usuário", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Remover registry", "DELETE_REPLICATION": "Remover replicação", "DELETE_MEMBER": "Remover usuário membro", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 57c8bb19a..8766b43c4 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -193,7 +193,8 @@ "LABELS": "标签", "PROJECTS": "项目", "CONFIG": "配置管理", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "机器人账户" }, "PROJECT_CONFIG": { "REGISTRY": "项目仓库", @@ -257,6 +258,31 @@ "SET_ROLE": "设置角色", "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": "组", "GROUPS": "组", @@ -800,6 +826,7 @@ "DELETE_REPO": "删除仓库", "DELETE_TAG": "删除镜像标签", "DELETE_USER": "删除用户", + "DELETE_ROBOT": "删除账户", "DELETE_REGISTRY": "删除Registry", "DELETE_REPLICATION": "删除复制", "DELETE_MEMBER": "删除用户成员", diff --git a/src/portal/src/styles.css b/src/portal/src/styles.css index 431c0710b..ec72024cf 100644 --- a/src/portal/src/styles.css +++ b/src/portal/src/styles.css @@ -78,4 +78,12 @@ body { .datagrid-header{ z-index: 1 !important; +} + +.color-green { + color: #1D5100; +} + +.color-red { + color: red; } \ No newline at end of file