Commit 404ee307 authored by FangyuanCheng's avatar FangyuanCheng
Browse files

Support Robot account in Harbor


Signed-off-by: default avatarFangyuanCheng <fangyuanc@vmware.com>
parent daf81e24
......@@ -12,7 +12,7 @@
@include text-overflow;
}
@mixin grid-left-top-pos{
@mixin grid-right-top-pos{
position: absolute;
z-index: 100;
right: 35px;
......
......@@ -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;
......
......@@ -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;
......
@import '../mixin';
.rightPos{
@include grid-left-top-pos;
@include grid-right-top-pos;
}
.toolbar {
......
......@@ -33,6 +33,7 @@ export const enum ConfirmationTargets {
PROJECT,
PROJECT_MEMBER,
USER,
ROBOT_ACCOUNT,
POLICY,
TOGGLE_CONFIRM,
TARGET,
......
......@@ -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 {}
......@@ -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
}
]
},
......
......@@ -22,6 +22,9 @@
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</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)">
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
</li>
......
......@@ -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 {
......
<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>
\ No newline at end of file
.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;
}
}
}
}
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();
});
});
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);
});
}
}
<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">