Merge remote-tracking branch 'upstream/master' into batchDelection

This commit is contained in:
Fuhui Peng (c) 2018-01-18 14:59:32 +08:00
commit 29e53817b9
22 changed files with 291 additions and 91 deletions

View File

@ -35,6 +35,7 @@ import (
const (
defaultJSONCfgStorePath string = "/etc/adminserver/config/config.json"
defaultKeyPath string = "/etc/adminserver/key"
ldapScopeKey string = "ldap_scope"
)
var (
@ -274,6 +275,11 @@ func initCfgStore() (err error) {
log.Errorf("Failed to read old configuration from %s", path)
return err
}
// Update LDAP Scope for migration
// only used when migrating harbor release before v1.3
// after v1.3 there is always a db configuration before migrate.
validLdapScope(jsonconfig, true)
err = CfgStore.Write(jsonconfig)
if err != nil {
log.Error("Failed to update old configuration to database")
@ -336,7 +342,7 @@ func LoadFromEnv(cfgs map[string]interface{}, all bool) error {
return fmt.Errorf("%v is not string or parse type", v)
}
validLdapScope(cfgs, false)
return nil
}
@ -356,3 +362,18 @@ func GetDatabaseFromCfg(cfg map[string]interface{}) *models.Database {
database.SQLite = sqlite
return database
}
// Valid LDAP Scope
func validLdapScope(cfg map[string]interface{}, isMigrate bool) {
ldapScope := cfg[ldapScopeKey].(int)
if isMigrate && ldapScope > 0 && ldapScope < 3 {
ldapScope = ldapScope - 1
}
if ldapScope >= 3 {
ldapScope = 2
}
if ldapScope < 0 {
ldapScope = 0
}
cfg[ldapScopeKey] = ldapScope
}

View File

@ -127,17 +127,54 @@ func TestLoadFromEnv(t *testing.T) {
}
func TestGetDatabaseFromCfg(t *testing.T) {
cfg :=map[string]interface{} {
common.DatabaseType:"mysql",
common.MySQLDatabase:"registry",
common.MySQLHost:"127.0.0.1",
common.MySQLPort:3306,
common.MySQLPassword:"1234",
common.MySQLUsername:"root",
common.SQLiteFile:"/tmp/sqlite.db",
cfg := map[string]interface{}{
common.DatabaseType: "mysql",
common.MySQLDatabase: "registry",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLPassword: "1234",
common.MySQLUsername: "root",
common.SQLiteFile: "/tmp/sqlite.db",
}
database := GetDatabaseFromCfg(cfg)
assert.Equal(t,"mysql",database.Type)
assert.Equal(t, "mysql", database.Type)
}
func TestValidLdapScope(t *testing.T) {
ldapScopeKey := "ldap_scope"
testCfgs := []struct {
config map[string]interface{}
migrate bool
ldapScopeResult int
}{
{map[string]interface{}{
ldapScopeKey: 1,
}, true, 0},
{map[string]interface{}{
ldapScopeKey: 2,
}, true, 1},
{map[string]interface{}{
ldapScopeKey: 3,
}, true, 2},
{map[string]interface{}{
ldapScopeKey: -1,
}, true, 0},
{map[string]interface{}{
ldapScopeKey: 100,
}, false, 2},
{map[string]interface{}{
ldapScopeKey: -100,
}, false, 0},
}
for i, item := range testCfgs {
validLdapScope(item.config, item.migrate)
if item.config[ldapScopeKey].(int) != item.ldapScopeResult {
t.Fatalf("Failed to update ldapScope expected %v, actual %v at index %v", item.ldapScopeResult, item.config[ldapScopeKey], i)
}
}
}

View File

@ -41,6 +41,8 @@ const (
UsersURLSuffix = "/Users"
)
var uaaTransport = &http.Transport{}
// Client provides funcs to interact with UAA.
type Client interface {
//PasswordAuth accepts username and password, return a token if it's valid.
@ -49,6 +51,8 @@ type Client interface {
GetUserInfo(token string) (*UserInfo, error)
//SearchUser searches a user based on user name.
SearchUser(name string) ([]*SearchUserEntry, error)
//UpdateConfig updates the config of the current client
UpdateConfig(cfg *ClientConfig) error
}
// ClientConfig values to initialize UAA Client
@ -169,13 +173,13 @@ func (dc *defaultClient) prepareCtx() context.Context {
return context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient)
}
// NewDefaultClient creates an instance of defaultClient.
func NewDefaultClient(cfg *ClientConfig) (Client, error) {
func (dc *defaultClient) UpdateConfig(cfg *ClientConfig) error {
url := cfg.Endpoint
if !strings.Contains(url, "://") {
url = "https://" + url
}
url = strings.TrimSuffix(url, "/")
dc.endpoint = url
tc := &tls.Config{
InsecureSkipVerify: cfg.SkipTLSVerify,
}
@ -183,7 +187,7 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) {
content, err := ioutil.ReadFile(cfg.CARootPath)
if err != nil {
return nil, err
return err
}
pool := x509.NewCertPool()
//Do not throw error if the certificate is malformed, so we can put a place holder.
@ -196,11 +200,9 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
log.Warningf("The root certificate file %s is not found, skip configuring root cert in UAA client.", cfg.CARootPath)
}
}
hc := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tc,
},
}
uaaTransport.TLSClientConfig = tc
dc.httpClient.Transport = uaaTransport
//dc.httpClient.Transport = transport.
oc := &oauth2.Config{
ClientID: cfg.ClientID,
@ -216,11 +218,17 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
ClientSecret: cfg.ClientSecret,
TokenURL: url + TokenURLSuffix,
}
return &defaultClient{
httpClient: hc,
oauth2Cfg: oc,
twoLegCfg: cc,
endpoint: url,
}, nil
dc.oauth2Cfg = oc
dc.twoLegCfg = cc
return nil
}
// NewDefaultClient creates an instance of defaultClient.
func NewDefaultClient(cfg *ClientConfig) (Client, error) {
hc := &http.Client{}
c := &defaultClient{httpClient: hc}
if err := c.UpdateConfig(cfg); err != nil {
return nil, err
}
return c, nil
}

View File

@ -19,6 +19,8 @@ import (
"golang.org/x/oauth2"
)
const fakeToken = "The Fake Token"
// FakeClient is for test only
type FakeClient struct {
Username string
@ -28,14 +30,26 @@ type FakeClient struct {
// PasswordAuth ...
func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, error) {
if username == fc.Username && password == fc.Password {
return &oauth2.Token{}, nil
return &oauth2.Token{AccessToken: fakeToken}, nil
}
return nil, fmt.Errorf("Invalide username and password")
}
// GetUserInfo ...
func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) {
return nil, nil
if token != fakeToken {
return nil, fmt.Errorf("Unexpected token: %s, expected: %s", token, fakeToken)
}
info := &UserInfo{
Name: "fakeName",
Email: "fake@fake.com",
}
return info, nil
}
// UpdateConfig ...
func (fc *FakeClient) UpdateConfig(cfg *ClientConfig) error {
return nil
}
// SearchUser ...

View File

@ -110,7 +110,7 @@ func Login(m models.AuthModel) (*models.User, error) {
time.Sleep(frozenTime)
}
authenticator.PostAuthenticate(user)
err = authenticator.PostAuthenticate(user)
return user, err
}

View File

@ -23,27 +23,12 @@ import (
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/uaa"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
)
//CreateClient create a UAA Client instance based on system configuration.
func CreateClient() (uaa.Client, error) {
UAASettings, err := config.UAASettings()
if err != nil {
return nil, err
}
cfg := &uaa.ClientConfig{
ClientID: UAASettings.ClientID,
ClientSecret: UAASettings.ClientSecret,
Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert,
CARootPath: os.Getenv("UAA_CA_ROOT"),
}
return uaa.NewDefaultClient(cfg)
}
// Auth is the implementation of AuthenticateHelper to access uaa for authentication.
type Auth struct {
sync.Mutex
@ -58,12 +43,17 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
t, err := u.client.PasswordAuth(m.Principal, m.Password)
if t != nil && err == nil {
//TODO: See if it's possible to get more information from token.
user := &models.User{
Username: m.Principal,
}
err = u.OnBoardUser(user)
return user, err
info, err2 := u.client.GetUserInfo(t.AccessToken)
if err2 != nil {
log.Warningf("Failed to extract user info from UAA, error: %v", err2)
} else {
user.Email = info.Email
user.Realname = info.Name
}
return user, nil
}
return nil, err
}
@ -89,6 +79,28 @@ func (u *Auth) OnBoardUser(user *models.User) error {
return dao.OnBoardUser(user)
}
// PostAuthenticate will check if user exists in DB, if not on Board user, if he does, update the profile.
func (u *Auth) PostAuthenticate(user *models.User) error {
dbUser, err := dao.GetUser(models.User{Username: user.Username})
if err != nil {
return err
}
if dbUser == nil {
return u.OnBoardUser(user)
}
if user.Email != "" {
dbUser.Email = user.Email
}
if user.Realname != "" {
dbUser.Realname = user.Realname
}
if err2 := dao.ChangeUserProfile(*user, "Email", "Realname"); err2 != nil {
log.Warningf("Failed to update user profile, user: %s, error: %v", user.Username, err2)
}
return nil
}
// SearchUser search user on uaa server, transform it to Harbor's user model
func (u *Auth) SearchUser(username string) (*models.User, error) {
if err := u.ensureClient(); err != nil {
@ -116,13 +128,27 @@ func (u *Auth) SearchUser(username string) (*models.User, error) {
}
func (u *Auth) ensureClient() error {
if u.client != nil {
return nil
var cfg *uaa.ClientConfig
UAASettings, err := config.UAASettings()
// log.Debugf("Uaa settings: %+v", UAASettings)
if err != nil {
log.Warningf("Failed to get UAA setting from Admin Server, error: %v", err)
} else {
cfg = &uaa.ClientConfig{
ClientID: UAASettings.ClientID,
ClientSecret: UAASettings.ClientSecret,
Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert,
CARootPath: os.Getenv("UAA_CA_ROOT"),
}
}
if u.client != nil && cfg != nil {
return u.client.UpdateConfig(cfg)
}
u.Lock()
defer u.Unlock()
if u.client == nil {
c, err := CreateClient()
c, err := uaa.NewDefaultClient(cfg)
if err != nil {
return err
}

View File

@ -102,11 +102,12 @@ func TestMain(m *testing.M) {
os.Exit(rc)
}
func TestCreateClient(t *testing.T) {
func TestEnsureClient(t *testing.T) {
assert := assert.New(t)
c, err := CreateClient()
auth := Auth{client: nil}
err := auth.ensureClient()
assert.Nil(err)
assert.NotNil(c)
assert.NotNil(auth.client)
}
func TestAuthenticate(t *testing.T) {
@ -123,6 +124,7 @@ func TestAuthenticate(t *testing.T) {
u1, err1 := auth.Authenticate(m1)
assert.Nil(err1)
assert.NotNil(u1)
assert.Equal("fake@fake.com", u1.Email)
m2 := models.AuthModel{
Principal: "wrong",
Password: "wrong",
@ -153,6 +155,30 @@ func TestOnBoardUser(t *testing.T) {
assert.Equal("test", user.Realname)
assert.Equal("test", user.Username)
assert.Equal("test@uaa.placeholder", user.Email)
err3 := dao.ClearTable(models.UserTable)
assert.Nil(err3)
}
func TestPostAuthenticate(t *testing.T) {
assert := assert.New(t)
auth := Auth{}
um := &models.User{
Username: "test",
}
err := auth.PostAuthenticate(um)
assert.Nil(err)
user, _ := dao.GetUser(models.User{Username: "test"})
assert.Equal("test@uaa.placeholder", user.Email)
um.Email = "newEmail@new.com"
um.Realname = "newName"
err2 := auth.PostAuthenticate(um)
assert.Nil(err2)
user2, _ := dao.GetUser(models.User{Username: "test"})
assert.Equal("newEmail@new.com", user2.Email)
assert.Equal("newName", user2.Realname)
err3 := dao.ClearTable(models.UserTable)
assert.Nil(err3)
}
func TestSearchUser(t *testing.T) {

View File

@ -1,6 +1,6 @@
{
"name": "harbor-ui",
"version": "0.6.6",
"version": "0.6.22",
"description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",

View File

@ -1,6 +1,6 @@
{
"name": "harbor-ui",
"version": "0.6.6",
"version": "0.6.22",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware",
"module": "index.js",

View File

@ -20,7 +20,7 @@ import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../
import { CONFIRMATION_DIALOG_TEMPLATE } from './confirmation-dialog.component.html';
import { CONFIRMATION_DIALOG_STYLE } from './confirmation-dialog.component.css';
import {BatchInfo} from "./confirmation-batch-message";
import {BatchInfo} from './confirmation-batch-message';
@Component({
selector: 'confirmation-dialog',
@ -29,16 +29,16 @@ import {BatchInfo} from "./confirmation-batch-message";
})
export class ConfirmationDialogComponent {
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
opened = false;
dialogTitle = '';
dialogContent = '';
message: ConfirmationMessage;
buttons: ConfirmationButtons;
@Output() confirmAction = new EventEmitter<ConfirmationAcknowledgement>();
@Output() cancelAction = new EventEmitter<ConfirmationAcknowledgement>();
@Input() batchInfors: BatchInfo[] = [];
isDelete: boolean = false;
isDelete = false;
constructor(
private translate: TranslateService) {}
@ -49,7 +49,7 @@ export class ConfirmationDialogComponent {
this.message = msg;
this.translate.get(this.dialogTitle).subscribe((res: string) => this.dialogTitle = res);
this.translate.get(this.dialogContent, { 'param': msg.param }).subscribe((res: string) => this.dialogContent = res);
//Open dialog
// Open dialog
this.buttons = msg.buttons;
this.opened = true;
}
@ -81,7 +81,8 @@ export class ConfirmationDialogComponent {
}
cancel(): void {
if(!this.message){//Inproper condition
if (!this.message) {
// Inproper condition
this.close();
return;
}

View File

@ -1,7 +1,7 @@
export const REPOSITORY_LISTVIEW_TEMPLATE = `
<div>
<div class="row" style="position:relative;">
<div>
<div>
<div class="row flex-items-xs-right option-right rightPos">
<div class="flex-xs-middle">
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
@ -10,7 +10,7 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = `
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17",
"core-js": "^2.4.1",
"harbor-ui": "0.6.21",
"harbor-ui": "0.6.22",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -1,12 +1,20 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="false">
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<inline-alert class="modal-title" (confirmEvt)="confirm($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<form #accountSettingsFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group form-group-override">
<label for="account_settings_username" class="form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="account_settings_username" aria-haspopup="true" class="form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="33">
<clr-tooltip *ngIf="renamable">
<button (dblclick)="openRenameAlert()" class="btn btn-link">
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
</button>
<clr-tooltip-content clrPosition="bottom-left" clrSize="md" *clrIfOpen>
<span (click)="openRenameAlert()"> {{'PROFILE.ADMIN_RENAME_TIP'}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="form-group form-group-override">
<label for="account_settings_email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>

View File

@ -42,6 +42,8 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
formValueChanged: boolean = false;
checkOnGoing: boolean = false;
RenameOnGoing: boolean = false;
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm;
@ViewChild(InlineAlertComponent)
@ -133,6 +135,29 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
return this.checkOnGoing;
}
public get renamable(): boolean {
return this.account && this.account.has_admin_role && this.account.username === 'admin' && this.account.user_id === 1;
}
openRenameAlert(): void {
this.RenameOnGoing = true;
this.inlineAlert.showInlineConfirmation({
message: 'PROFILE.RENAME_CONFIRM_INFO'
});
}
confirmRename(): void {
if (this.renamable) {
this.session.renameAdmin(this.account)
.then(() => {
this.msgHandler.showSuccess('PROFILE.RENAME_SUCCESS');
})
.catch(error => {
this.msgHandler.handleError(error);
});
}
}
ngAfterViewChecked(): void {
if (this.accountFormRef != this.accountForm) {
this.accountFormRef = this.accountForm;
@ -215,7 +240,11 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
});
}
confirmCancel($event: any): void {
confirm($event: any): void {
if(this.RenameOnGoing) {
this.confirmRename();
this.RenameOnGoing = false;
}
this.inlineAlert.close();
this.opened = false;
}

View File

@ -13,7 +13,7 @@
<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>
<li role="presentation" class="nav-item" *ngIf="withClair">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'VULNERABILITY.SINGULAR' | translate}}</button>
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>

View File

@ -35,7 +35,7 @@ export class ProjectDetailComponent {
roleName: string;
constructor(
private route: ActivatedRoute,
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService,
private projectService: ProjectService) {

View File

@ -28,6 +28,7 @@ const signOffEndpoint = "/log_out";
const accountEndpoint = "/api/users/:id";
const langEndpoint = "/language";
const userExistsEndpoint = "/userExists";
const renameAdminEndpoint = 'api/internal/renameadmin';
const langMap = {
"zh": "zh-CN",
"en": "en-US"
@ -129,6 +130,25 @@ export class SessionService {
.catch(error => this.handleError(error))
}
/**
*
* Update accpunt settings
*
* @param {SessionUser} account
* @returns {Promise<any>}
*
* @memberOf SessionService
*/
renameAdmin(account: SessionUser): Promise<any> {
if (!account) {
return Promise.reject("Invalid account settings");
}
return this.http.post(renameAdminEndpoint, JSON.stringify({}), HTTP_JSON_OPTIONS)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
}
/**
* Switch the backend language profile
*/

View File

@ -22,7 +22,7 @@ const userMgmtEndpoint = '/api/users';
/**
* Define related methods to handle account and session corresponding things
*
*
* @export
* @class SessionService
*/
@ -31,26 +31,26 @@ export class UserService {
constructor(private http: Http) { }
//Handle the related exceptions
// Handle the related exceptions
handleError(error: any): Promise<any> {
return Promise.reject(error.message || error);
}
//Get the user list
// Get the user list
getUsers(): Promise<User[]> {
return this.http.get(userMgmtEndpoint, HTTP_GET_OPTIONS).toPromise()
.then(response => response.json() as User[])
.catch(error => this.handleError(error));
}
//Add new user
// Add new user
addUser(user: User): Promise<any> {
return this.http.post(userMgmtEndpoint, JSON.stringify(user), HTTP_JSON_OPTIONS).toPromise()
.then(() => null)
.catch(error => this.handleError(error));
}
//Delete the specified user
// Delete the specified user
deleteUser(userId: number): Promise<any> {
return this.http.delete(userMgmtEndpoint + "/" + userId, HTTP_JSON_OPTIONS)
.toPromise()
@ -58,7 +58,7 @@ export class UserService {
.catch(error => this.handleError(error));
}
//Update user to enable/disable the admin role
// Update user to enable/disable the admin role
updateUser(user: User): Promise<any> {
return this.http.put(userMgmtEndpoint + "/" + user.user_id, JSON.stringify(user), HTTP_JSON_OPTIONS)
.toPromise()
@ -66,7 +66,7 @@ export class UserService {
.catch(error => this.handleError(error));
}
//Set user admin role
// Set user admin role
updateUserRole(user: User): Promise<any> {
return this.http.put(userMgmtEndpoint + "/" + user.user_id + "/sysadmin", JSON.stringify(user), HTTP_JSON_OPTIONS)
.toPromise()

View File

@ -82,7 +82,10 @@
"FULL_NAME": "First and last name",
"COMMENT": "Comments",
"PASSWORD": "Password",
"SAVE_SUCCESS": "User profile saved successfully."
"SAVE_SUCCESS": "User profile saved successfully.",
"ADMIN_RENAME_TIP": "Double click to change your username to \"admin@harbor.local\", but this action can NOT redo.",
"RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "This action can not undo, Confirm To Rename?"
},
"CHANGE_PWD": {
"TITLE": "Change Password",
@ -217,7 +220,6 @@
"OF": "of",
"SWITCH_TITLE": "Confirm project members switch",
"SWITCH_SUMMARY": "Do you want to switch project members {{param}}?"
},
"AUDIT_LOG": {
"USERNAME": "Username",
@ -426,6 +428,7 @@
"REPLICATION": "Replication",
"EMAIL": "Email",
"SYSTEM": "System Settings",
"VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirm to cancel",
"CONFIRM_SUMMARY": "Some changes have not been saved. Do you want to discard them?",
"SAVE_SUCCESS": "Configuration has been successfully saved.",

View File

@ -82,7 +82,10 @@
"FULL_NAME": "Nombre y apellidos",
"COMMENT": "Comentarios",
"PASSWORD": "Contraseña",
"SAVE_SUCCESS": "Perfil de usuario guardado satisfactoriamente."
"SAVE_SUCCESS": "Perfil de usuario guardado satisfactoriamente.",
"ADMIN_RENAME_TIP": "Double click to change your username to \"admin@harbor.local\", but this action can NOT redo.",
"RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "This action can not undo, Confirm To Rename?"
},
"CHANGE_PWD": {
"TITLE": "Cambiar contraseña",
@ -364,8 +367,7 @@
"PLACEHOLDER": "We couldn't find any endpoints!"
},
"REPOSITORY": {
"COPY_ID": "Copiar ID",
"COPY_PARENT_ID": "Copiar ID padre",
"COPY_DIGEST_ID": "Copy Digest",
"DELETE": "Eliminar",
"NAME": "Nombre",
"TAGS_COUNT": "Etiquetas",
@ -426,6 +428,7 @@
"REPLICATION": "Replicación",
"EMAIL": "Email",
"SYSTEM": "Opciones del Sistema",
"VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirma cancelación",
"CONFIRM_SUMMARY": "Algunos cambios no han sido guardados aún. ¿Quiere descartarlos?",
"SAVE_SUCCESS": "La configuración ha sido guardada satisfactoriamente.",
@ -528,6 +531,8 @@
"PRO_ITEM": "PROYECTOS",
"REPO_ITEM": "REPOSITORIOS",
"INDEX_PRIVATE": "PRIVADO",
"INDEX_MY_PROJECTS": "MY PROJECTS",
"INDEX_MY_REPOSITORIES": "MY REPOSITORIES",
"INDEX_PUB": "PÚBLICO",
"INDEX_TOTAL": "TOTAL",
"STORAGE": "ALMACENAMIENTO",

View File

@ -82,7 +82,10 @@
"FULL_NAME": "全名",
"COMMENT": "注释",
"PASSWORD": "密码",
"SAVE_SUCCESS": "成功保存用户设置。"
"SAVE_SUCCESS": "成功保存用户设置。",
"ADMIN_RENAME_TIP": "双击将用户名改为 \"admin@harbor.local\", 注意这个操作是不可逆的",
"RENAME_SUCCESS": "用户名更改成功!",
"RENAME_CONFIRM_INFO": "更改用户名无法撤销, 你确定更改吗·?"
},
"CHANGE_PWD": {
"TITLE": "修改密码",
@ -202,7 +205,6 @@
"DEVELOPER": "开发人员",
"GUEST": "访客",
"DELETE": "删除",
"OF": "共计",
"ITEMS": "条记录",
"ACTIONS": "操作",
"USERNAME_IS_REQUIRED": "用户名为必填项。",
@ -215,6 +217,7 @@
"ADDED_SUCCESS": "成功新增成员。",
"DELETED_SUCCESS": "成功删除成员。",
"SWITCHED_SUCCESS": "切换角色成功。",
"OF": "共计",
"SWITCH_TITLE": "切换项目成员确认",
"SWITCH_SUMMARY": "你确认切换项目成员 {{param}}??"
},
@ -233,10 +236,10 @@
"OTHERS": "其他",
"ADVANCED": "高级检索",
"SIMPLE": "简单检索",
"OF": "共计",
"ITEMS": "条记录",
"FILTER_PLACEHOLDER": "过滤日志",
"INVALID_DATE": "无效日期。"
"INVALID_DATE": "无效日期。",
"OF": "共计"
},
"REPLICATION": {
"REPLICATION_RULE": "复制规则",
@ -425,6 +428,7 @@
"REPLICATION": "复制",
"EMAIL": "邮箱",
"SYSTEM": "系统设置",
"VULNERABILITY": "漏洞",
"CONFIRM_TITLE": "确认取消",
"CONFIRM_SUMMARY": "配置项有改动, 确定取消?",
"SAVE_SUCCESS": "变更的配置项成功保存。",

View File

@ -6,7 +6,5 @@ cd /harbor_src
mv /harbor_resources/node_modules ./
npm install
npm run test > ./npm-ut-test-results
npm install -q --no-progress
npm run test > ./npm-ut-test-results