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 @@
{{'PROJECT_DETAIL.LOGS' | translate}}
+
+ {{'PROJECT_DETAIL.ROBOT_ACCOUNTS' | translate}}
+
{{'PROJECT_DETAIL.CONFIG' | translate}}
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 @@
+
+ {{'ROBOT_ACCOUNT.CREAT_ROBOT_ACCOUNT' | translate}}
+
+
+
+
+
+
+
+ {{ createSuccess | translate}}
+
+
+
+
+
+
+
{{'ROBOT_ACCOUNT.ALERT_TEXT' | translate}}
+
+
+
+
+
+
\ 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