Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Tan Jiang 2017-03-23 21:02:05 +08:00
commit 6b55bf488a
36 changed files with 598 additions and 178 deletions

51
docs/use_make.md Normal file
View File

@ -0,0 +1,51 @@
### Variables
Variable | Description
-------------------|-------------
BASEIMAGE | Container base image, default: photon
DEVFLAG | Build model flag, default: dev
COMPILETAG | Compile model flag, default: compile_normal (local golang build)
GOBUILDIMAGE | Golang image to compile harbor go source code.
CLARITYIMAGE | Clarity image that based on Node to compile UI.
NOTARYFLAG | Whether to enable notary in harbor, default:false
HTTPPROXY | Clarity proxy to build UI.
### Targets
Target | Description
--------------------|-------------
all | prepare env, compile binaries, build images and install images
prepare | prepare env
compile | compile ui and jobservice code
compile_ui | compile ui binary
compile_jobservice | compile jobservice binary
compile_clarity | compile clarity ui binary
compile_adminserver | compile admin server binary
build | build Harbor docker images (default: using build_photon)
build_photon | build Harbor docker images from Photon OS base image
install | compile binaries, build images, prepare specific version of compose file and startup Harbor instance
start | startup Harbor instance
down | shutdown Harbor instance
package_online | prepare online install package
package_offline | prepare offline install package
pushimage | push Harbor images to specific registry server
clean all | remove binary, Harbor images, specific version docker-compose file, specific version tag and online/offline install package
cleanbinary | remove ui and jobservice binary
cleanimage | remove Harbor images
cleandockercomposefile | remove specific version docker-compose
cleanversiontag | remove specific version tag
cleanpackage | remove online/offline install package
version | set harbor version
#### EXAMPLE:
#### Build and run harbor from source code.
make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true HTTPPROXY=http://proxy.vmware.com:3128
### Package offline installer
make package_offline GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true HTTPPROXY=http://proxy.vmware.com:3128
### Start harbor with notary
make -e NOTARYFLAG=true start
### Stop harbor with notary
make -e NOTARYFLAG=true down

View File

@ -120,15 +120,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
//Handle the global search event and then let the result page to trigger api
doSearch(event: string): void {
if (event === "") {
if (!this.isSearchResultsOpened) {
//Will not open search result panel if term is empty
//Do nothing
return;
} else {
//If opened, then close the search result panel
this.isSearchResultsOpened = false;
this.searchResultComponet.close();
return;
}
}
//Once this method is called
//the search results page must be opened

View File

@ -0,0 +1,62 @@
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]='isCurrentTabLink("config-auth")'>{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'" [clrTabLinkActive]='isCurrentTabLink("config-replication")'>{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'" [clrTabLinkActive]='isCurrentTabLink("config-email")'>{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'" [clrTabLinkActive]='isCurrentTabLink("config-system")'>{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]='isCurrentTabContent("authentication")'>
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'" [clrTabContentActive]='isCurrentTabContent("replication")'>
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'" [clrTabContentActive]='isCurrentTabContent("email")'>
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'" [clrTabContentActive]='isCurrentTabContent("system_settings")'>
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
id="tokenExpiration"
size="40" [disabled]="disabled(allConfig.token_expiration)">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
</div>
</div>

View File

@ -1,16 +1,24 @@
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
<ul id="configTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item">
<button id="config-auth" class="btn btn-link nav-link active" aria-controls="authentication" [class.active]='isCurrentTabLink("config-auth")' type="button" (click)='tabLinkClick("config-auth")'>{{'CONFIG.AUTH' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-replication" class="btn btn-link nav-link" aria-controls="replication" [class.active]='isCurrentTabLink("config-replication")' type="button" (click)='tabLinkClick("config-replication")'>{{'CONFIG.REPLICATION' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-email" class="btn btn-link nav-link" aria-controls="email" [class.active]='isCurrentTabLink("config-email")' type="button" (click)='tabLinkClick("config-email")'>{{'CONFIG.EMAIL' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'">
</section>
<section id="replication" role="tabpanel" aria-labelledby="config-replication" [hidden]='!isCurrentTabContent("replication")'>
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
@ -24,11 +32,11 @@
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'">
</section>
<section id="email" role="tabpanel" aria-labelledby="config-email" [hidden]='!isCurrentTabContent("email")'>
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'">
</section>
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
@ -50,8 +58,7 @@
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>

View File

@ -16,8 +16,15 @@ import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
const fakePass = "fakepassword";
const TabLinkContentMap = {
"config-auth": "authentication",
"config-replication": "replication",
"config-email": "email",
"config-system": "system_settings"
};
@Component({
selector: 'config',
@ -27,7 +34,7 @@ const fakePass = "fakepassword";
export class ConfigurationComponent implements OnInit, OnDestroy {
private onGoing: boolean = false;
allConfig: Configuration = new Configuration();
private currentTabId: string = "";
private currentTabId: string = "config-auth";//default tab
private originalCopy: Configuration;
private confirmSub: Subscription;
private testingOnGoing: boolean = false;
@ -41,17 +48,76 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
private msgService: MessageService,
private configService: ConfigurationService,
private confirmService: ConfirmationDialogService,
private appConfigService: AppConfigService) { }
private appConfigService: AppConfigService,
private session: SessionService) { }
private isCurrentTabLink(tabId: string): boolean {
return this.currentTabId === tabId;
}
private isCurrentTabContent(contentId: string): boolean {
return TabLinkContentMap[this.currentTabId] === contentId;
}
private hasUnsavedChangesOfCurrentTab(): any {
let allChanges = this.getChanges();
if (this.isEmpty(allChanges)) {
return null;
}
let properties = [];
switch (this.currentTabId) {
case "config-auth":
for (let prop in allChanges) {
if (prop.startsWith("ldap_")) {
return allChanges;
}
}
properties = ["auth_mode", "project_creation_restriction", "self_registration"];
break;
case "config-email":
for (let prop in allChanges) {
if (prop.startsWith("email_")) {
return allChanges;
}
}
return null;
case "config-replication":
properties = ["verify_remote_cert"];
break;
case "config-system":
properties = ["token_expiration"];
break;
default:
return null;
}
for (let prop in allChanges) {
if (properties.indexOf(prop) != -1) {
return allChanges;
}
}
return null;
}
ngOnInit(): void {
//First load
//Double confirm the current use has admin role
let currentUser = this.session.getCurrentUser();
if (currentUser && currentUser.has_admin_role > 0) {
this.retrieveConfig();
}
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED &&
confirmation.source === ConfirmationTargets.CONFIG) {
confirmation.state === ConfirmationState.CONFIRMED) {
if (confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
} else if (confirmation.source === ConfirmationTargets.CONFIG_TAB) {
this.reset(confirmation.data["changes"]);
this.currentTabId = confirmation.data["tabId"];
}
}
});
}
@ -104,8 +170,15 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.authConfig && this.authConfig.isValid();
}
public tabLinkChanged(tabLink: any) {
this.currentTabId = tabLink.id;
public tabLinkClick(tabLink: string) {
//Whether has unsave changes in current tab
let changes = this.hasUnsavedChangesOfCurrentTab();
if (!changes) {
this.currentTabId = tabLink;
return;
}
this.confirmUnsavedTabChanges(changes, tabLink);
}
/**
@ -154,14 +227,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
this.confirmUnsavedChanges(changes);
} else {
//Inprop situation, should not come here
console.error("Nothing changed");
@ -218,6 +284,33 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
});
}
private confirmUnsavedChanges(changes: any) {
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
}
private confirmUnsavedTabChanges(changes: any, tabId: string){
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
{
"changes": changes,
"tabId": tabId
},
ConfirmationTargets.CONFIG_TAB
);
this.confirmService.openComfirmDialog(msg);
}
private retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()

View File

@ -18,6 +18,7 @@ export class MessageComponent implements OnInit {
globalMessage: Message = new Message();
globalMessageOpened: boolean;
messageText: string = "";
private timer: any = null;
constructor(
private messageService: MessageService,
@ -48,7 +49,7 @@ export class MessageComponent implements OnInit {
// Make the message alert bar dismiss after several intervals.
//Only for this case
setInterval(() => this.onClose(), dismissInterval);
this.timer = setTimeout(() => this.onClose(), dismissInterval);
}
);
}
@ -56,15 +57,9 @@ export class MessageComponent implements OnInit {
//Translate or refactor the message shown to user
translateMessage(msg: Message): void {
if (!msg) {
return;
}
let key = "";
if (!msg.message) {
key = "UNKNOWN_ERROR";
} else {
key = typeof msg.message === "string" ? msg.message.trim() : msg.message;
let key = "UNKNOWN_ERROR", param = "";
if (msg && msg.message) {
key = (typeof msg.message === "string" ? msg.message.trim() : msg.message);
if (key === "") {
key = "UNKNOWN_ERROR";
}
@ -73,13 +68,11 @@ export class MessageComponent implements OnInit {
//Override key for HTTP 401 and 403
if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) {
key = "UNAUTHORIZED_ERROR";
}
if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
} else if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
key = "FORBIDDEN_ERROR";
}
this.translate.get(key).subscribe((res: string) => this.messageText = res);
this.translate.get(key, { 'param': param }).subscribe((res: string) => this.messageText = res);
}
public get needAuth(): boolean {
@ -98,6 +91,9 @@ export class MessageComponent implements OnInit {
}
onClose() {
if (this.timer) {
clearTimeout(this.timer);
}
this.globalMessageOpened = false;
}
}

View File

@ -33,6 +33,8 @@ import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deactivate.service';
import { MemberGuard } from './shared/route/member-guard-activate.service';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'password-reset', component: ResetPasswordComponent },
@ -79,6 +81,7 @@ const harborRoutes: Routes = [
{
path: 'projects/:id',
component: ProjectDetailComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
@ -89,7 +92,8 @@ const harborRoutes: Routes = [
},
{
path: 'replication',
component: ReplicationComponent
component: ReplicationComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'member',

View File

@ -44,5 +44,4 @@ export class AuditLogService extends BaseService {
.map(response => response.json() as AuditLog[])
.catch(error => this.handleError(error));
}
}

View File

@ -2,25 +2,18 @@
margin-top: 0px !important;
}
.filter-log {
float: right;
margin-right: 24px;
position: relative;
top: 8px;
}
.action-head-pos {
position: relative;
top: 20px;
padding-right: 18px;
}
.refresh-btn {
position: absolute;
right: -4px;
top: 8px;
cursor: pointer;
}
.refresh-btn:hover {
color: #00bfff;
}
.custom-lines-button {
padding: 0px !important;
min-width: 25px !important;
@ -30,3 +23,20 @@
font-size: 16px;
text-decoration: underline;
}
.log-select {
width: 180px;
display: inline-block;
top: 1px;
}
.item-divider {
height: 24px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 12px;
top: 8px;
position: relative;
}

View File

@ -1,21 +1,25 @@
<div>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}
<span class="badge badge-info">{{logNumber}}</span>
</h2>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">
<span>
<label>{{'RECENT_LOG.SUB_TITLE' | translate}} </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 10" (click)="setLines(10)">10</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 25" (click)="setLines(25)">25</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 50" (click)="setLines(50)">50</button>
<label>{{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</label>
</span>
<grid-filter class="filter-log" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span class="refresh-btn" (click)="refresh()">
<div class="select log-select">
<select id="log_display_num" (change)="handleOnchange($event)">
<option value="10">{{'RECENT_LOG.SUB_TITLE' | translate}} 10 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="25">{{'RECENT_LOG.SUB_TITLE' | translate}} 25 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="50">{{'RECENT_LOG.SUB_TITLE' | translate}} 50 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
</select>
</div>
<div class="item-divider"></div>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span (click)="refresh()" class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</span>
</div>
</div>
<div>
<clr-datagrid>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>

View File

@ -34,18 +34,23 @@ export class RecentLogComponent implements OnInit {
this.retrieveLogs();
}
public get inProgress(): boolean {
return this.onGoing;
}
public setLines(lines: number): void {
this.lines = lines;
private handleOnchange($event: any) {
if (event && event.target && event.srcElement["value"]) {
this.lines = event.srcElement["value"];
if (this.lines < 10) {
this.lines = 10;
}
this.retrieveLogs();
}
}
public get logNumber(): number {
return this.recentLogs?this.recentLogs.length:0;
}
public get inProgress(): boolean {
return this.onGoing;
}
public doFilter(terms: string): void {
if (terms.trim() === "") {

View File

@ -13,7 +13,6 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-project',
templateUrl: 'create-project.component.html',
@ -50,6 +49,7 @@ export class CreateProjectComponent implements AfterViewChecked {
.subscribe(
status=>{
this.create.emit(true);
this.messageService.announceMessage(status, 'PROJECT.CREATED_SUCCESS', AlertType.SUCCESS);
this.createProjectOpened = false;
},
error=>{

View File

@ -5,8 +5,8 @@
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects" [clrDgItem]="p">
<clr-dg-action-overflow *ngIf="listFullMode">
<button class="action-item" (click)="newReplicationRule(p)">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<clr-dg-action-overflow [hidden]="!listFullMode || p.current_user_role_id !== 1">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-overflow>

View File

@ -43,6 +43,11 @@ export class ListProjectComponent implements OnInit {
return this.mode === ListMode.FULL && this.session.getCurrentUser() != null;
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(false);

View File

@ -19,15 +19,15 @@
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id">
<input type="radio" name="roleRadios" id="checkrads_project_admin" [value]="1" [(ngModel)]="member.role_id">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_developer" value="2" [(ngModel)]="member.role_id">
<input type="radio" name="roleRadios" id="checkrads_developer" [value]="2" [(ngModel)]="member.role_id">
<label for="checkrads_developer">{{'MEMBER.DEVELOPER' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_guest" value="3" [(ngModel)]="member.role_id">
<input type="radio" name="roleRadios" id="checkrads_guest" [value]="3" [(ngModel)]="member.role_id">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</div>
</div>
@ -36,6 +36,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!memberForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -51,6 +51,7 @@ export class AddMemberComponent implements AfterViewChecked {
.addMember(this.projectId, this.member.username, +this.member.role_id)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'MEMBER.ADDED_SUCCESS', AlertType.SUCCESS);
console.log('Added member successfully.');
this.added.emit(true);
this.addMemberOpened = false;
@ -112,9 +113,11 @@ export class AddMemberComponent implements AfterViewChecked {
}
openAddMemberModal(): void {
this.memberForm.reset();
this.member = new Member();
this.addMemberOpened = true;
this.hasChanged = false;
this.member.role_id = 1;
}
}

View File

@ -2,7 +2,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left">
<button class="btn btn-primary" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<button *ngIf="hasProjectAdminRole" class="btn btn-primary" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="flex-xs-middle option-right">
@ -18,7 +18,7 @@
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members">
<clr-dg-action-overflow [hidden]="u.user_id === currentUser.user_id">
<clr-dg-action-overflow [hidden]="u.user_id === currentUser.user_id || !hasProjectAdminRole">
<button class="action-item" (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</button>

View File

@ -40,12 +40,22 @@ export class MemberComponent implements OnInit, OnDestroy {
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
hasProjectAdminRole: boolean;
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: ConfirmationDialogService,
session: SessionService) {
//Get current user from registered resolver.
this.currentUser = session.getCurrentUser();
let projectMembers: Member[] = session.getProjectMembers();
if(this.currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === this.currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
this.delSub = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
@ -54,7 +64,8 @@ export class MemberComponent implements OnInit, OnDestroy {
.deleteMember(this.projectId, message.data)
.subscribe(
response => {
console.log('Successful change role with user ' + message.data);
this.messageService.announceMessage(response, 'MEMBER.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete member: ' + message.data);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + message.data, AlertType.DANGER)
@ -102,6 +113,7 @@ export class MemberComponent implements OnInit, OnDestroy {
.changeMemberRole(this.projectId, userId, roleId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'MEMBER.SWITCHED_SUCCESS', AlertType.SUCCESS);
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},

View File

@ -3,7 +3,7 @@
<h2 class="header-title">{{'PROJECT.PROJECTS' | translate}}</h2>
<div class="row flex-items-xs-between">
<div class="option-left">
<button class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<button *ngIf="projectCreationRestriction" class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="option-right">

View File

@ -23,6 +23,10 @@ import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
@Component({
@ -59,6 +63,8 @@ export class ProjectComponent implements OnInit, OnDestroy {
constructor(
private projectService: ProjectService,
private messageService: MessageService,
private appConfigService: AppConfigService,
private sessionService: SessionService,
private deletionDialogService: ConfirmationDialogService) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
@ -69,6 +75,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
.deleteProject(projectId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'PROJECT.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete project with ID:' + projectId);
this.retrieve();
},
@ -76,11 +83,13 @@ export class ProjectComponent implements OnInit, OnDestroy {
);
}
});
}
ngOnInit(): void {
this.projectName = '';
this.isPublic = 0;
}
ngOnDestroy(): void {
@ -89,6 +98,19 @@ export class ProjectComponent implements OnInit, OnDestroy {
}
}
get projectCreationRestriction(): boolean {
let account = this.sessionService.getCurrentUser();
if(account) {
switch(this.appConfigService.getConfig().project_creation_restriction) {
case 'adminonly':
return (account.has_admin_role === 1);
case 'everyone':
return true;
}
}
return false;
}
retrieve(state?: State): void {
if (state) {
this.page = state.page.to + 1;
@ -135,7 +157,10 @@ export class ProjectComponent implements OnInit, OnDestroy {
this.projectService
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response => console.log('Successful toggled project_id:' + p.project_id),
response => {
this.messageService.announceMessage(response, 'PROJECT.TOGGLED_SUCCESS', AlertType.SUCCESS);
console.log('Successful toggled project_id:' + p.project_id);
},
error => this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}

View File

@ -70,4 +70,11 @@ export class ProjectService {
.catch(error=>Observable.throw(error));
}
checkProjectMember(projectId: number): Observable<any> {
return this.http
.get(`/api/projects/${projectId}/members`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
}

View File

@ -108,6 +108,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.createTarget(this.target)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'DESTINATION.CREATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful added target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
@ -129,7 +130,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.get(errorMessageKey)
.subscribe(res=>{
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
this.inlineAlert.showInlineError(errorMessageKey);
this.inlineAlert.showInlineError(res);
});
}
);
@ -139,6 +140,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.updateTarget(this.target)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'DESTINATION.UPDATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful updated target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
@ -158,7 +160,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
this.inlineAlert.showInlineError(errorMessageKey);
this.inlineAlert.showInlineError(res);
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
});
}

View File

@ -43,14 +43,15 @@ export class DestinationComponent implements OnInit {
.deleteTarget(targetId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'DESTINATION.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful deleted target with ID:' + targetId);
this.reload();
},
error => this.messageService
.announceMessage(error.status,
'Failed to delete target with ID:' + targetId + ', error:' + error,
AlertType.DANGER)
);
error => {
this.messageService
.announceMessage(error.status,'DESTINATION.DELETED_FAILED', AlertType.DANGER);
console.log('Failed to delete target with ID:' + targetId + ', error:' + error);
});
}
});
}

View File

@ -3,7 +3,7 @@
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow *ngIf="listFullMode">
<clr-dg-action-overflow *ngIf="listFullMode && hasProjectAdminRole">
<button class="action-item">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>

View File

@ -7,6 +7,9 @@ import { SearchTriggerService } from '../../base/global-search/search-trigger.se
import { SessionService } from '../../shared/session.service';
import { ListMode } from '../../shared/shared.const';
import { SessionUser } from '../../shared/session-user';
import { Member } from '../../project/member/member';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html'
@ -25,10 +28,22 @@ export class ListRepositoryComponent {
pageOffset: number = 1;
hasProjectAdminRole: boolean;
constructor(
private router: Router,
private searchTrigger: SearchTriggerService,
private session: SessionService) { }
private session: SessionService) {
//Get current user from registered resolver.
let currentUser = session.getCurrentUser();
let projectMembers: Member[] = session.getProjectMembers();
if(currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
}
deleteRepo(repoName: string) {
this.delete.emit(repoName);

View File

@ -60,6 +60,7 @@ export class RepositoryComponent implements OnInit {
.subscribe(
response => {
this.refresh();
this.messageService.announceMessage(response, 'REPOSITORY.DELETED_REPO_SUCCESS', AlertType.SUCCESS);
console.log('Successful deleted repo:' + repoName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)

View File

@ -3,19 +3,19 @@
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<clr-dg-action-overflow *ngIf="hasProjectAdminRole">
<button class="action-item" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-dg-cell *ngIf="withNotary">
<clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell>

View File

@ -10,10 +10,14 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../tag';
import { TagView } from '../tag-view';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { Member } from '../../project/member/member';
@Component({
moduleId: module.id,
selector: 'tag-repository',
@ -25,8 +29,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
projectId: number;
repoName: string;
hasProjectAdminRole: boolean;
tags: TagView[];
registryUrl: string;
withNotary: boolean;
private subscription: Subscription;
@ -35,7 +42,18 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
private messageService: MessageService,
private deletionDialogService: ConfirmationDialogService,
private repositoryService: RepositoryService,
private appConfigService: AppConfigService) {
private appConfigService: AppConfigService,
private session: SessionService){
let currentUser = session.getCurrentUser();
let projectMembers: Member[] = session.getProjectMembers();
if(currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(
message => {
if (message &&
@ -52,6 +70,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
.subscribe(
response => {
this.retrieve();
this.messageService.announceMessage(response, 'REPOSITORY.DELETED_TAG_SUCCESS', AlertType.SUCCESS);
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
@ -68,6 +87,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve();
}
@ -79,11 +99,23 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
retrieve() {
this.tags = [];
if(this.withNotary) {
this.repositoryService
.listTagsWithVerifiedSignatures(this.repoName)
.subscribe(
items => {
items.forEach(t => {
items => this.listTags(items),
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
} else {
this.repositoryService
.listTags(this.repoName)
.subscribe(
items => this.listTags(items),
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
}
}
private listTags(tags: Tag[]): void {
tags.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
@ -96,8 +128,6 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
tag.os = data['os'];
this.tags.push(tag);
});
},
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
}
deleteTag(tag: TagView) {

View File

@ -183,6 +183,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
.createPolicy(this.getPolicyByForm())
.subscribe(
response=>{
this.messageService.announceMessage(response, 'REPLICATION.CREATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful created policy: ' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);
@ -199,6 +200,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
.createOrUpdatePolicyWithNewTarget(this.getPolicyByForm(), this.getTargetByForm())
.subscribe(
response=>{
this.messageService.announceMessage(response, 'REPLICATION.UPDATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful created policy and target:' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);

View File

@ -50,7 +50,10 @@ export class ListPolicyComponent implements OnDestroy {
this.replicationService
.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
response => {
this.messageService.announceMessage(response, 'REPLICATION.TOGGLED_SUCCESS', AlertType.SUCCESS);
console.log('Successful toggled policy status')
},
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
@ -67,10 +70,11 @@ export class ListPolicyComponent implements OnDestroy {
.deletePolicy(message.data)
.subscribe(
response => {
this.messageService.announceMessage(response, 'REPLICATION.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
error => this.messageService.announceMessage(error.status, 'REPLICATION.DELETED_FAILED', AlertType.DANGER)
);
}
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { ProjectService } from '../../project/project.service';
import { CommonRoutes } from '../../shared/shared.const';
@Injectable()
export class MemberGuard implements CanActivate, CanActivateChild {
constructor(
private sessionService: SessionService,
private projectService: ProjectService,
private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
let projectId: number = route.params['id'];
return new Promise((resolve, reject) => {
this.projectService.checkProjectMember(projectId)
.subscribe(
res=>{
this.sessionService.setProjectMembers(res);
return resolve(true)
},
error => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return resolve(false);
});
});
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -3,6 +3,8 @@ import { Headers, Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { SessionUser } from './session-user';
import { Member } from '../project/member/member';
import { SignInCredential } from './sign-in-credential';
import { enLang } from '../shared/shared.const'
@ -27,6 +29,8 @@ const langMap = {
export class SessionService {
currentUser: SessionUser = null;
projectMembers: Member[];
private headers = new Headers({
"Content-Type": 'application/json'
});
@ -143,4 +147,12 @@ export class SessionService {
})
.catch(error => this.handleError(error));
}
setProjectMembers(projectMembers: Member[]): void {
this.projectMembers = projectMembers;
}
getProjectMembers(): Member[] {
return this.projectMembers;
}
}

View File

@ -24,7 +24,8 @@ export const enum ConfirmationTargets {
REPOSITORY,
TAG,
CONFIG,
CONFIG_ROUTE
CONFIG_ROUTE,
CONFIG_TAB
};
export const enum ActionType {

View File

@ -32,6 +32,7 @@ import { StatisticsComponent } from './statictics/statistics.component';
import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
import { SignInGuard } from './route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './route/leaving-config-deactivate.service';
import { MemberGuard } from './route/member-guard-activate.service';
@NgModule({
imports: [
@ -79,7 +80,8 @@ import { LeavingConfigRouteDeactivate } from './route/leaving-config-deactivate.
SystemAdminGuard,
AuthCheckGuard,
SignInGuard,
LeavingConfigRouteDeactivate
LeavingConfigRouteDeactivate,
MemberGuard
]
})
export class SharedModule {

View File

@ -132,7 +132,10 @@
"DELETION_TITLE": "Confirm project deletion",
"DELETION_SUMMARY": "Do you want to delete project {{param}}?",
"FILTER_PLACEHOLDER": "Filter Projects",
"REPLICATION_RULE": "Replication Rule"
"REPLICATION_RULE": "Replication Rule",
"CREATED_SUCCESS": "Created project successfully.",
"DELETED_SUCCESS": "Deleted project successfully.",
"TOGGLED_SUCCESS": "Toggled project successfully."
},
"PROJECT_DETAIL": {
"REPOSITORIES": "Repositories",
@ -159,7 +162,10 @@
"UNKNOWN_ERROR": "Unknown error occurred while adding member.",
"FILTER_PLACEHOLDER": "Filter Members",
"DELETION_TITLE": "Confirm project member deletion",
"DELETION_SUMMARY": "Do you want to delete project member {{param}}?"
"DELETION_SUMMARY": "Do you want to delete project member {{param}}?",
"ADDED_SUCCESS": "Added member successfully.",
"DELETED_SUCCESS": "Deleted member successfully.",
"SWITCHED_SUCCESS": "Switched member role successfully."
},
"AUDIT_LOG": {
"USERNAME": "Username",
@ -235,7 +241,12 @@
"TOGGLE_ENABLE_TITLE": "Enable Policy",
"CONFIRM_TOGGLE_ENABLE_POLICY": "After enabling the replication policy, all repositories under the project will be replicated to the destination registry. Please confirm to continue.",
"TOGGLE_DISABLE_TITLE": "Disable Policy",
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue."
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue.",
"CREATED_SUCCESS": "Created policy successfully.",
"UPDATED_SUCCESS": "Updated policy successfully.",
"DELETED_SUCCESS": "Deleted policy successfully.",
"DELETED_FAILED": "Deleted policy failed.",
"TOGGLED_SUCCESS": "Toggled policy status successfully."
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",
@ -257,7 +268,11 @@
"INVALID_NAME": "Invalid destination name.",
"FAILED_TO_GET_TARGET": "Failed to get endpoint.",
"CREATION_TIME": "Creation Time",
"ITEMS": "item(s)"
"ITEMS": "item(s)",
"CREATED_SUCCESS": "Created destination successfully.",
"UPDATED_SUCCESS": "Updated destination successfully.",
"DELETED_SUCCESS": "Deleted destination successfully.",
"DELETED_FAILED": "Deleted destination failed."
},
"REPOSITORY": {
"COPY_ID": "Copy ID",
@ -286,7 +301,9 @@
"SHOW_DETAILS": "Show Details",
"REPOSITORIES": "Repositories",
"ITEMS": "item(s)",
"POP_REPOS": "Popular Repositories"
"POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repository successfully.",
"DELETED_TAG_SUCCESS": "Deleted tag successfully."
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet, do you really want to cancel?"
@ -310,7 +327,7 @@
"EMAIL": "Email",
"SYSTEM": "System Settings",
"CONFIRM_TITLE": "Confirm to cancel",
"CONFIRM_SUMMARY": "Some changes are not saved yet, do you really want to leave?",
"CONFIRM_SUMMARY": "Some changes are not saved yet, do you really want to discard?",
"SAVE_SUCCESS": "Configurations have been successfully saved",
"MAIL_SERVER": "Email Server",
"MAIL_SERVER_PORT": "Email Server Port",
@ -386,7 +403,8 @@
"IN_PROGRESS": "Search...",
"BACK": "Back"
},
"UNKNOWN_ERROR": "Some unknown errors HAVE occurred. Please try again later",
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later",
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue the operation",
"FORBIDDEN_ERROR": "You are not allowed to perform this operation"
"FORBIDDEN_ERROR": "You are not allowed to perform this operation",
"GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}"
}

View File

@ -132,7 +132,10 @@
"DELETION_TITLE": "删除项目确认",
"DELETION_SUMMARY": "你确认删除项目 {{param}}",
"FILTER_PLACEHOLDER": "过滤项目",
"REPLICATION_RULE": "复制策略"
"REPLICATION_RULE": "复制策略",
"CREATED_SUCCESS": "创建项目成功。",
"DELETED_SUCCESS": "删除项目成功。",
"TOGGLED_SUCCESS": "切换状态成功。"
},
"PROJECT_DETAIL": {
"REPOSITORIES": "镜像仓库",
@ -159,7 +162,10 @@
"UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认",
"DELETION_SUMMARY": "你确认删除项目成员 {{param}}?"
"DELETION_SUMMARY": "你确认删除项目成员 {{param}}?",
"ADDED_SUCCESS": "新增成员成功。",
"DELETED_SUCCESS": "删除成员成功",
"SWITCHED_SUCCESS": "切换角色成功"
},
"AUDIT_LOG": {
"USERNAME": "用户名",
@ -235,7 +241,12 @@
"TOGGLE_ENABLE_TITLE": "启用策略",
"CONFIRM_TOGGLE_ENABLE_POLICY": "启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。",
"TOGGLE_DISABLE_TITLE": "停用策略",
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。"
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。",
"CREATED_SUCCESS": "创建复制策略成功。",
"UPDATED_SUCCESS": "更新复制策略成功。",
"DELETED_SUCCESS": "删除复制策略成功。",
"DELETED_FAILED": "删除复制策略失败。",
"TOGGLED_SUCCESS": "切换复制策略状态成功。"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",
@ -257,7 +268,11 @@
"INVALID_NAME": "无效的目标名称。",
"FAILED_TO_GET_TARGET": "获取目标失败。",
"CREATION_TIME": "创建时间",
"ITEMS": "条记录"
"ITEMS": "条记录",
"CREATED_SUCCESS": "创建目标成功。",
"UPDATED_SUCCESS": "更新目标成功。",
"DELETED_SUCCESS": "删除目标成功。",
"DELETED_FAILED": "删除目标失败。"
},
"REPOSITORY": {
"COPY_ID": "复制ID",
@ -286,7 +301,9 @@
"SHOW_DETAILS": "显示详细",
"REPOSITORIES": "镜像仓库",
"ITEMS": "条记录",
"POP_REPOS": "受欢迎的镜像库"
"POP_REPOS": "受欢迎的镜像库",
"DELETED_REPO_SUCCESS": "删除镜像仓库成功。",
"DELETED_TAG_SUCCESS": "删除镜像标签成功。"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认取消?"
@ -388,5 +405,6 @@
},
"UNKNOWN_ERROR": "发生未知错误,请稍后再试",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限"
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限",
"GENERAL_ERROR": "调用后台服务时出现错误: {{param}}"
}