diff --git a/src/ui_ng/lib/README.md b/src/ui_ng/lib/README.md index 8ca86d1d9..36cf2cfbe 100644 --- a/src/ui_ng/lib/README.md +++ b/src/ui_ng/lib/README.md @@ -116,11 +116,58 @@ page contains `hbr-repository`. ... +watchRepoClickEvent(repo: RepositoryItem): void { + //Process repo + ... +} +``` + + +**hbr-repository-gridview Directive** + +**projectId** is used to specify which projects the repositories are from. + +**projectName** is used to generate the related commands for pushing images. + +**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user. + +**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property. + +**withVIC** is integrated with VIC + +**repoClickEvent** is an @output event emitter for you to catch the repository click events. + +**repoProvisionEvent** is an @output event emitter for you to catch the deploy button click event. + +**addInfoEvent** is an @output event emitter for you to catch the add additional info button event. + + @Output() repoProvisionEvent = new EventEmitter(); + @Output() addInfoEvent = new EventEmitter(); + + +``` + + +... + watchRepoClickEvent(repo: RepositoryItem): void { //Process repo ... } + +watchRepoProvisionEvent(repo: RepositoryItem): void { + //Process repo + ... +} + +watchAddInfoEvent(repo: RepositoryItem): void { + //Process repo + ... +} ``` diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 53cb7df2e..dac2b4339 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.52", + "version": "0.6.61", "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", diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index 2cea500a7..f5ecf6cf4 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.52", + "version": "0.6.61", "description": "Harbor shared UI components based on Clarity and Angular4", "author": "VMware", "module": "index.js", diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.css.ts b/src/ui_ng/lib/src/gridview/grid-view.component.css.ts new file mode 100644 index 000000000..e97f73bf3 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.css.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved. +// This software is released under MIT license. +// The full license information can be found in LICENSE in the root directory of this project. + +// @import 'node_modules/admiral-ui-common/css/mixins'; + +export const GRIDVIEW_STYLE = ` +.grid-content { + position: relative; + top: 36px; + left: 0; + right: 0; + bottom: 0; + overflow: auto; + max-height: 65vh; +} + +.card-item { + display: block; + max-width: 400px; + min-width: 300px; + position: absolute; + margin-right: 40px; + transition: width 0.4s, transform 0.4s; +} + +.content-empty { + text-align: center; + display: block; + margin-top: 100px; +} + +.central-block-loading { + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + @include animation(fadein 0.4s); + text-align: center; + background-color: rgba(255, 255, 255, 0.5); +} +.central-block-loading-more { + position: relative; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + @include animation(fadein 0.4s); + text-align: center; + background-color: rgba(255, 255, 255, 0.5); +} +.vertical-helper { + display: inline-block; + height: 100%; + vertical-align: middle; +} + +.spinner { + width: 100px; + height: 100px; + vertical-align: middle; +} + +` \ No newline at end of file diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.html.ts b/src/ui_ng/lib/src/gridview/grid-view.component.html.ts new file mode 100644 index 000000000..fdc9937aa --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.html.ts @@ -0,0 +1,18 @@ +export const GRIDVIEW_TEMPLATE = ` +
+
+ + + + + + {{'REPOSITORY.NO_ITEMS' | translate}} + +
+
+ +
+
+
+` \ No newline at end of file diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts b/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts new file mode 100644 index 000000000..5c0c63445 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridViewComponent } from './grid-view.component'; +import { SharedModule } from '../shared/shared.module'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; + + +describe('GridViewComponent', () => { + let component: GridViewComponent; + let fixture: ComponentFixture; + + let config: IServiceConfig = { + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + ], + declarations: [ + GridViewComponent, + ], + providers: [{ + provide: SERVICE_CONFIG, useValue: config }] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.ts b/src/ui_ng/lib/src/gridview/grid-view.component.ts new file mode 100644 index 000000000..d34f08f94 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.ts @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +import { Component, Input, Output, SimpleChanges, ContentChild, ViewChild, ViewChildren, + TemplateRef, HostListener, ViewEncapsulation, EventEmitter, AfterViewInit } from '@angular/core'; +import { CancelablePromise } from '../shared/shared.utils'; +import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; + +import { TranslateService } from '@ngx-translate/core'; +import { GRIDVIEW_TEMPLATE } from './grid-view.component.html'; +import { GRIDVIEW_STYLE } from './grid-view.component.css'; +import { ScrollPosition } from '../service/interface' + +@Component({ + selector: 'hbr-gridview', + template: GRIDVIEW_TEMPLATE, + styles: [GRIDVIEW_STYLE], + encapsulation: ViewEncapsulation.None +}) +/** + * Grid view general component. + */ +export class GridViewComponent implements AfterViewInit { + @Input() loading: boolean; + @Input() totalCount: number; + @Input() currentPage: number; + @Input() pageSize: number; + @Input() expectScrollPercent = 70; + @Input() withAdmiral: boolean; + @Input() + set items(value: any[]) { + let newCardStyles = value.map((d, index) => { + if (index < this.cardStyles.length) { + return this.cardStyles[index]; + } + return { + opacity: '0', + overflow: 'hidden' + }; + }); + this.cardStyles = newCardStyles; + this._items = value; + } + + @Output() loadNextPageEvent = new EventEmitter(); + + @ViewChildren('cardItem') cards: any; + @ViewChild('itemsHolder') itemsHolder: any; + @ContentChild(TemplateRef) gridItemTmpl: any; + + _items: any[] = []; + + cardStyles: any = []; + itemsHolderStyle: any = {}; + layoutTimeout: any; + + querySub: Subscription; + routerSub: Subscription; + + totalItemsCount: number; + loadedPages = 0; + nextPageLink: string; + hidePartialRows = false; + loadPagesTimeout: any; + + CurrentScrollPosition: ScrollPosition = { + sH: 0, + sT: 0, + cH: 0 + }; + + preScrollPosition: ScrollPosition = null; + + constructor(private translate: TranslateService) { } + + ngAfterViewInit() { + this.cards.changes.subscribe(() => { + this.throttleLayout(); + }); + this.throttleLayout(); + } + + get items() { + return this._items; + } + + @HostListener('scroll', ['$event']) + onScroll(event: any) { + + this.preScrollPosition = this.CurrentScrollPosition; + this.CurrentScrollPosition = { + sH: event.target.scrollHeight, + sT: event.target.scrollTop, + cH: event.target.clientHeight + } + if (!this.loading + && this.isScrollDown() + && this.isScrollExpectPercent() + && (this.currentPage * this.pageSize < this.totalCount)) { + this.loadNextPageEvent.emit(); + } + } + + isScrollDown(): boolean { + return this.preScrollPosition.sT < this.CurrentScrollPosition.sT; + } + + isScrollExpectPercent(): boolean { + return ((this.CurrentScrollPosition.sT + this.CurrentScrollPosition.cH) / this.CurrentScrollPosition.sH) > (this.expectScrollPercent / 100); + } + + @HostListener('window:resize') + onResize(event: any) { + this.throttleLayout(); + } + + throttleLayout() { + clearTimeout(this.layoutTimeout); + this.layoutTimeout = setTimeout(() => { + this.layout.call(this); + }, 40); + } + + get isFirstPage() { + return this.currentPage <= 1; + } + + layout() { + let el = this.itemsHolder.nativeElement; + + let width = el.offsetWidth; + let items = el.querySelectorAll('.card-item'); + let items_count = items.length; + if (items_count === 0) { + el.height = 0; + return; + } + + let itemsHeight = []; + for (let i = 0; i < items_count; i++) { + itemsHeight[i] = items[i].offsetHeight; + } + + let height = Math.max.apply(null, itemsHeight); + let itemsStyle: CSSStyleDeclaration = window.getComputedStyle(items[0]); + + let minWidthStyle: string = itemsStyle.minWidth; + let maxWidthStyle: string = itemsStyle.maxWidth; + + let minWidth = parseInt(minWidthStyle, 10); + let maxWidth = parseInt(maxWidthStyle, 10); + + let marginHeight: number = + parseInt(itemsStyle.marginTop, 10) + parseInt(itemsStyle.marginBottom, 10); + let marginWidth: number = + parseInt(itemsStyle.marginLeft, 10) + parseInt(itemsStyle.marginRight, 10); + + let columns = Math.floor(width / (minWidth + marginWidth)); + + let columnsToUse = Math.max(Math.min(columns, items_count), 1); + let rows = Math.floor(items_count / columnsToUse); + let itemWidth = Math.min(Math.floor(width / columnsToUse) - marginWidth, maxWidth); + let itemSpacing = columnsToUse === 1 || columns > items_count ? marginWidth : + (width - marginWidth - columnsToUse * itemWidth) / (columnsToUse - 1); + if (!this.withAdmiral) { + // Fixed spacing and margin on standalone mode + itemSpacing = marginWidth; + itemWidth = minWidth; + } + + let visible = items_count; + if (this.hidePartialRows && this.totalItemsCount && items_count !== this.totalItemsCount) { + visible = rows * columnsToUse; + } + + let count = 0; + for (let i = 0; i < visible; i++) { + let item = items[i]; + let itemStyle = window.getComputedStyle(item); + + let left = (i % columnsToUse) * (itemWidth + itemSpacing); + let top = Math.floor(count / columnsToUse) * (height + marginHeight); + + // trick to show nice apear animation, where the item is already positioned, + // but it will pop out + let oldTransform = itemStyle.transform; + if (!oldTransform || oldTransform === 'none') { + this.cardStyles[i] = { + transform: 'translate(' + left + 'px,' + top + 'px) scale(0)', + width: itemWidth + 'px', + transition: 'none', + overflow: 'hidden' + }; + this.throttleLayout(); + } else { + this.cardStyles[i] = { + transform: 'translate(' + left + 'px,' + top + 'px) scale(1)', + width: itemWidth + 'px', + transition: null, + overflow: 'hidden' + }; + this.throttleLayout(); + } + + if (!item.classList.contains('context-selected')) { + let itemHeight = itemsHeight[i]; + if (itemStyle.display === 'none' && itemHeight !== 0) { + this.cardStyles[i].display = null; + } + if (itemHeight !== 0) { + count++; + } + } + } + + for (let i = visible; i < items_count; i++) { + this.cardStyles[i] = { + display: 'none' + }; + } + this.itemsHolderStyle = { + height: Math.ceil(count / columnsToUse) * (height + marginHeight) + 'px' + }; + } + + onCardEnter(i: number) { + this.cardStyles[i].overflow = 'visible'; + } + + onCardLeave(i: number) { + this.cardStyles[i].overflow = 'hidden'; + } + + trackByFn(index: number, item: any) { + return index; + } +} diff --git a/src/ui_ng/lib/src/gridview/index.ts b/src/ui_ng/lib/src/gridview/index.ts new file mode 100644 index 000000000..3f5e40836 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/index.ts @@ -0,0 +1,8 @@ +import { Type } from "@angular/core"; +import { GridViewComponent } from './grid-view.component'; + +export * from "./grid-view.component"; + +export const HBR_GRIDVIEW_DIRECTIVES: Type[] = [ + GridViewComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 17c87e7fa..8584dcbf5 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -25,6 +25,8 @@ import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index'; import { CONFIGURATION_DIRECTIVES } from './config/index'; import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index'; import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/index'; +import { HBR_GRIDVIEW_DIRECTIVES } from './gridview/index'; +import { REPOSITORY_GRIDVIEW_DIRECTIVES } from './repository-gridview'; import { SystemInfoService, @@ -182,7 +184,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co PROJECT_POLICY_CONFIG_DIRECTIVES, LABEL_DIRECTIVES, CREATE_EDIT_LABEL_DIRECTIVES, - LABEL_PIECE_DIRECTIVES + LABEL_PIECE_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + REPOSITORY_GRIDVIEW_DIRECTIVES, ], exports: [ LOG_DIRECTIVES, @@ -207,7 +211,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co PROJECT_POLICY_CONFIG_DIRECTIVES, LABEL_DIRECTIVES, CREATE_EDIT_LABEL_DIRECTIVES, - LABEL_PIECE_DIRECTIVES + LABEL_PIECE_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + REPOSITORY_GRIDVIEW_DIRECTIVES, ], providers: [] }) diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 2e2d0e05f..501097b29 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -2,7 +2,7 @@ export * from './harbor-library.module'; export * from './service.config'; export * from './service/index'; export * from './error-handler/index'; -//export * from './utils'; +// export * from './utils'; export * from './log/index'; export * from './filter/index'; export * from './endpoint/index'; @@ -23,3 +23,5 @@ export * from './channel/index'; export * from './project-policy-config/index'; export * from './label/index'; export * from './create-edit-label'; +export * from './gridview/index'; +export * from './repository-gridview/index'; diff --git a/src/ui_ng/lib/src/repository-gridview/index.ts b/src/ui_ng/lib/src/repository-gridview/index.ts new file mode 100644 index 000000000..aa9cd65f2 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/index.ts @@ -0,0 +1,8 @@ +import { Type } from "@angular/core"; +import { RepositoryGridviewComponent } from './repository-gridview.component'; + +export * from "./repository-gridview.component"; + +export const REPOSITORY_GRIDVIEW_DIRECTIVES: Type[] = [ + RepositoryGridviewComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts new file mode 100644 index 000000000..b0b8539e5 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts @@ -0,0 +1,76 @@ +export const REPOSITORY_GRIDVIEW_STYLE = ` +.rightPos{ + position: absolute; + z-index: 100; + right: 35px; + margin-top: 4px; +} + +.toolbar { + overflow: hidden; +} + +.filter-divider { + display: inline-block; + height: 16px; + width: 2px; + background-color: #cccccc; + padding-top: 12px; + padding-bottom: 12px; + position: relative; + top: 9px; + margin-right: 6px; + margin-left: 6px; +} + +.card-block { + margin-top: 24px; + min-height: 100px; +} + +.form-group { + display: flex; +} + +.form-group > label { + width: 100px; +} + + +.card-media-block { + margin-top: 12px; + margin-bottom: 12px; +} + +.card-media-block > img { + height: 45px; + width: 45px; +} +.card-media-description { + height: 45px; +} +.card-media-description > p { + margin-top: 0px; +} + +.card-text { + height: 45px; + overflow: hidden; + margin-bottom: 18px; +} + +.card-block { + margin-top: 0px; +} + +.card-footer { + padding-top: 6px; + padding-bottom: 6px; +} + +.list-img > img { + height: 24px; + width: 24px; + margin-right: 12px; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts new file mode 100644 index 000000000..90c8820a7 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts @@ -0,0 +1,92 @@ +export const REPOSITORY_GRIDVIEW_TEMPLATE = ` +
+
+
+
+
+ + + + + + + + + + +
+
+
+
+
+ + + + + {{'REPOSITORY.NAME' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.PULL_COUNT' | translate}} + {{'REPOSITORY.PLACEHOLDER' | translate }} + + {{r.name}} + {{r.tags_count}} + {{r.pull_count}} + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} + {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}} + + + +
+ + + +
+
+ +
+ + {{item.name}} + +

{{registryUrl}}

+
+
+
+
+
+ {{getRepoDescrition(item)}} +
+
+ +
{{item.tags_count}}
+
+
+ +
{{item.pull_count}}
+
+
+ +
+
+
+ +
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts new file mode 100644 index 000000000..438754bfb --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts @@ -0,0 +1,175 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { RouterTestingModule } from '@angular/router/testing'; + +import { SharedModule } from '../shared/shared.module'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { RepositoryGridviewComponent } from './repository-gridview.component'; +import { TagComponent } from '../tag/tag.component'; +import { FilterComponent } from '../filter/filter.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; +import { TagService, TagDefaultService } from '../service/tag.service'; +import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service'; +import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index'; +import { HBR_GRIDVIEW_DIRECTIVES } from '../gridview/index' +import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index'; +import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index'; +import { JobLogViewerComponent } from '../job-log-viewer/index'; +import {LabelPieceComponent} from "../label-piece/label-piece.component"; + +import { click } from '../utils'; + +describe('RepositoryComponentGridview (inline template)', () => { + + let compRepo: RepositoryGridviewComponent; + let fixtureRepo: ComponentFixture; + let repositoryService: RepositoryService; + let tagService: TagService; + let systemInfoService: SystemInfoService; + + let spyRepos: jasmine.Spy; + let spySystemInfo: jasmine.Spy; + + let mockSystemInfo: SystemInfo = { + "with_notary": true, + "with_admiral": false, + "admiral_endpoint": "NA", + "auth_mode": "db_auth", + "registry_url": "10.112.122.56", + "project_creation_restriction": "everyone", + "self_registration": true, + "has_ca_root": false, + "harbor_version": "v1.1.1-rc1-160-g565110d" + }; + + let mockRepoData: RepositoryItem[] = [ + { + "id": 1, + "name": "library/busybox", + "project_id": 1, + "description": "asdfsadf", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + }, + { + "id": 2, + "name": "library/nginx", + "project_id": 1, + "description": "asdf", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + } + ]; + + let mockRepo: Repository = { + metadata: {xTotalCount: 2}, + data: mockRepoData + }; + + let mockTagData: Tag[] = [ + { + "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", + "name": "1.11.5", + "size": "2049", + "architecture": "amd64", + "os": "linux", + "docker_version": "1.12.3", + "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", + "created": new Date("2016-11-08T22:41:15.912313785Z"), + "signature": null, + "labels": [] + } + ]; + + let config: IServiceConfig = { + repositoryBaseEndpoint: '/api/repository/testing', + systemInfoEndpoint: '/api/systeminfo/testing', + targetBaseEndpoint: '/api/tag/testing' + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule + ], + declarations: [ + RepositoryGridviewComponent, + TagComponent, + LabelPieceComponent, + ConfirmationDialogComponent, + FilterComponent, + VULNERABILITY_DIRECTIVES, + PUSH_IMAGE_BUTTON_DIRECTIVES, + INLINE_ALERT_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + JobLogViewerComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: RepositoryService, useClass: RepositoryDefaultService }, + { provide: TagService, useClass: TagDefaultService }, + { provide: SystemInfoService, useClass: SystemInfoDefaultService } + ] + }); + })); + + beforeEach(() => { + fixtureRepo = TestBed.createComponent(RepositoryGridviewComponent); + compRepo = fixtureRepo.componentInstance; + compRepo.projectId = 1; + compRepo.hasProjectAdminRole = true; + + repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService); + systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService); + + spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo)); + spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo)); + fixtureRepo.detectChanges(); + }); + + it('should create', () => { + expect(compRepo).toBeTruthy(); + }); + + it('should load and render data', async(() => { + fixtureRepo.detectChanges(); + + fixtureRepo.whenStable().then(() => { + fixtureRepo.detectChanges(); + + let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell')); + expect(deRepo).toBeTruthy(); + let elRepo: HTMLElement = deRepo.nativeElement; + expect(elRepo).toBeTruthy(); + expect(elRepo.textContent).toEqual('library/busybox'); + }); + })); + + it('should filter data by keyword', async(() => { + fixtureRepo.detectChanges(); + + fixtureRepo.whenStable().then(() => { + fixtureRepo.detectChanges(); + + compRepo.doSearchRepoNames('nginx'); + fixtureRepo.detectChanges(); + let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell')); + expect(de).toBeTruthy(); + expect(de.length).toEqual(1); + let el: HTMLElement = de[0].nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/nginx'); + }); + })); + +}); diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts new file mode 100644 index 000000000..5215eef0f --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts @@ -0,0 +1,404 @@ +import { Component, Input, Output, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; +import {Observable} from "rxjs/Observable"; +import { TranslateService } from '@ngx-translate/core'; +import { Comparator, State } from 'clarity-angular'; + +import { REPOSITORY_GRIDVIEW_TEMPLATE } from './repository-gridview.component.html'; +import { REPOSITORY_GRIDVIEW_STYLE } from './repository-gridview.component.css'; +import { Repository, SystemInfo, SystemInfoService, RepositoryService, RequestQueryParams, RepositoryItem, TagService } from '../service/index'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { toPromise, CustomComparator , DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting} from '../utils'; +import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; +import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; +import { Tag, CardItemEvent } from '../service/interface'; +import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message"; +import { GridViewComponent } from '../gridview/grid-view.component' + + +@Component({ + selector: 'hbr-repository-gridview', + template: REPOSITORY_GRIDVIEW_TEMPLATE, + styles: [REPOSITORY_GRIDVIEW_STYLE], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RepositoryGridviewComponent implements OnChanges, OnInit { + signedCon: {[key: string]: any | string[]} = {}; + @Input() projectId: number; + @Input() projectName = 'unknown'; + @Input() urlPrefix: string; + @Input() hasSignedIn: boolean; + @Input() hasProjectAdminRole: boolean; + @Output() repoClickEvent = new EventEmitter(); + @Output() repoProvisionEvent = new EventEmitter(); + @Output() addInfoEvent = new EventEmitter(); + + lastFilteredRepoName: string; + repositories: RepositoryItem[] = []; + repositoriesCopy: RepositoryItem[] = []; + systemInfo: SystemInfo; + selectedRow: RepositoryItem[] = []; + loading = true; + + isCardView: boolean; + cardHover = false; + listHover = false; + + batchDelectionInfos: BatchInfo[] = []; + pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); + tagsCountComparator: Comparator = new CustomComparator('tags_count', 'number'); + + pageSize: number = DEFAULT_PAGE_SIZE; + currentPage = 1; + totalCount = 0; + currentState: State; + + @ViewChild('confirmationDialog') + confirmationDialog: ConfirmationDialogComponent; + + @ViewChild('gridView') + gridView: GridViewComponent; + + + constructor( + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private repositoryService: RepositoryService, + private systemInfoService: SystemInfoService, + private tagService: TagService, + private ref: ChangeDetectorRef, + private router: Router) { } + + public get registryUrl(): string { + return this.systemInfo ? this.systemInfo.registry_url : ''; + } + + public get withAdmiral(): boolean { + return this.systemInfo ? this.systemInfo.with_admiral : false; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['projectId'] && changes['projectId'].currentValue) { + this.refresh(); + } + } + + ngOnInit(): void { + if (this.withAdmiral) { + this.isCardView = true; + } else { + this.isCardView = false; + } + // Get system info for tag views + toPromise(this.systemInfoService.getSystemInfo()) + .then(systemInfo => this.systemInfo = systemInfo) + .catch(error => this.errorHandler.error(error)); + + this.lastFilteredRepoName = ''; + } + + confirmDeletion(message: ConfirmationAcknowledgement) { + if (message && + message.source === ConfirmationTargets.REPOSITORY && + message.state === ConfirmationState.CONFIRMED) { + + let promiseLists: any[] = []; + let repoNames: string[] = message.data.split(','); + + repoNames.forEach(repoName => { + promiseLists.push(this.delOperate(repoName)); + }); + + Promise.all(promiseLists).then((item) => { + this.selectedRow = []; + this.refresh(); + let st: State = this.getStateAfterDeletion(); + if (!st) { + this.refresh(); + } else { + this.clrLoad(st); + } + }); + } + } + + delOperate(repoName: string) { + let findedList = this.batchDelectionInfos.find(data => data.name === repoName); + if (this.signedCon[repoName].length !== 0) { + Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).subscribe(res => { + findedList = BathInfoChanges(findedList, res[0], false, true, res[1]); + }); + } else { + return toPromise(this.repositoryService + .deleteRepository(repoName)) + .then( + response => { + this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => { + findedList = BathInfoChanges(findedList, res); + }); + }).catch(error => { + if (error.status === "412") { + Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => { + findedList = BathInfoChanges(findedList, res[0], false, true, res[1]); + }); + return; + } + this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { + findedList = BathInfoChanges(findedList, res, false, true); + }); + }); + } + } + + doSearchRepoNames(repoName: string) { + this.lastFilteredRepoName = repoName; + this.currentPage = 1; + let st: State = this.currentState; + if (!st) { + st = { page: {} }; + } + st.page.size = this.pageSize; + st.page.from = 0; + st.page.to = this.pageSize - 1; + this.clrLoad(st); + } + + saveSignatures(event: {[key: string]: string[]}): void { + Object.assign(this.signedCon, event); + } + + deleteRepos(repoLists: RepositoryItem[]) { + if (repoLists && repoLists.length) { + let repoNames: string[] = []; + this.batchDelectionInfos = []; + let repArr: any[] = []; + + repoLists.forEach(repo => { + repoNames.push(repo.name); + let initBatchMessage = new BatchInfo(); + initBatchMessage.name = repo.name; + this.batchDelectionInfos.push(initBatchMessage); + + if (!this.signedCon[repo.name]) { + repArr.push(this.getTagInfo(repo.name)); + } + }); + + Promise.all(repArr).then(() => { + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', '', repoNames.join(','), 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL); + }); + } + } + + getTagInfo(repoName: string): Promise { + this.signedCon[repoName] = []; + return toPromise(this.tagService + .getTags(repoName)) + .then(items => { + items.forEach((t: Tag) => { + if (t.signature !== null) { + this.signedCon[repoName].push(t.name); + } + }); + }) + .catch(error => this.errorHandler.error(error)); + } + + signedDataSet(repoName: string): void { + let signature = ''; + if (this.signedCon[repoName].length === 0) { + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL); + return; + } + signature = this.signedCon[repoName].join(','); + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO_SIGNED', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO_SIGNED', ConfirmationButtons.CLOSE); + } + + confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void { + this.translateService.get(summaryKey, + { + repoName: repoName, + signedImages: signature, + }) + .subscribe((res: string) => { + summaryKey = res; + let message = new ConfirmationMessage( + summaryTitle, + summaryKey, + repoName, + repoName, + ConfirmationTargets.REPOSITORY, + button); + this.confirmationDialog.open(message); + + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 5000); + }); + } + + provisionItemEvent(evt: any, repo: RepositoryItem): void { + evt.stopPropagation(); + this.repoProvisionEvent.emit(repo); + } + deleteItemEvent(evt: any, item: RepositoryItem): void { + evt.stopPropagation(); + this.deleteRepos([item]); + } + itemAddInfoEvent(evt: any, repo: RepositoryItem): void { + evt.stopPropagation(); + this.addInfoEvent.emit(repo); + } + selectedChange(): void { + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 2000); + } + refresh() { + this.doSearchRepoNames(''); + } + + loadNextPage() { + if (this.currentPage * this.pageSize >= this.totalCount) { + return + } + this.currentPage = this.currentPage + 1; + + // Pagination + let params: RequestQueryParams = new RequestQueryParams(); + params.set("page", '' + this.currentPage); + params.set("page_size", '' + this.pageSize); + + this.loading = true; + + toPromise(this.repositoryService.getRepositories( + this.projectId, + this.lastFilteredRepoName, + params)) + .then((repo: Repository) => { + this.totalCount = repo.metadata.xTotalCount; + this.repositoriesCopy = repo.data; + this.signedCon = {}; + // Do filtering and sorting + this.repositoriesCopy = doFiltering(this.repositoriesCopy, this.currentState); + this.repositoriesCopy = doSorting(this.repositoriesCopy, this.currentState); + this.repositories = this.repositories.concat(this.repositoriesCopy); + this.loading = false; + }) + .catch(error => { + this.loading = false; + this.errorHandler.error(error); + }); + } + + clrLoad(state: State): void { + this.selectedRow = []; + // Keep it for future filtering and sorting + this.currentState = state; + + let pageNumber: number = calculatePage(state); + if (pageNumber <= 0) { pageNumber = 1; } + + // Pagination + let params: RequestQueryParams = new RequestQueryParams(); + params.set("page", '' + pageNumber); + params.set("page_size", '' + this.pageSize); + + this.loading = true; + + toPromise(this.repositoryService.getRepositories( + this.projectId, + this.lastFilteredRepoName, + params)) + .then((repo: Repository) => { + this.totalCount = repo.metadata.xTotalCount; + this.repositories = repo.data; + + this.signedCon = {}; + // Do filtering and sorting + this.repositories = doFiltering(this.repositories, state); + this.repositories = doSorting(this.repositories, state); + + this.loading = false; + }) + .catch(error => { + this.loading = false; + this.errorHandler.error(error); + }); + + // Force refresh view + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 5000); + } + + getStateAfterDeletion(): State { + let total: number = this.totalCount - 1; + if (total <= 0) { return null; } + + let totalPages: number = Math.ceil(total / this.pageSize); + let targetPageNumber: number = this.currentPage; + + if (this.currentPage > totalPages) { + targetPageNumber = totalPages; // Should == currentPage -1 + } + + let st: State = this.currentState; + if (!st) { + st = { page: {} }; + } + st.page.size = this.pageSize; + st.page.from = (targetPageNumber - 1) * this.pageSize; + st.page.to = targetPageNumber * this.pageSize - 1; + + return st; + } + + watchRepoClickEvt(repo: RepositoryItem) { + this.repoClickEvent.emit(repo); + } + + getImgLink(repo: RepositoryItem): string { + return '/container-image-icons?container-image=' + repo.name + } + + getRepoDescrition(repo: RepositoryItem): string { + if (repo && repo.description) { + return repo.description; + } + return "No description for this repo. You can add it to this repository." + } + showCard(cardView: boolean) { + if (this.isCardView === cardView) { + return + } + this.isCardView = cardView; + this.refresh(); + } + + mouseEnter(itemName: string) { + if (itemName === 'card') { + this.cardHover = true; + } else { + this.listHover = true; + } + } + + mouseLeave(itemName: string) { + if (itemName === 'card') { + this.cardHover = false; + } else { + this.listHover = false; + } + } + + isHovering(itemName: string) { + if (itemName === 'card') { + return this.cardHover; + } else { + return this.listHover; + } + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts b/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts index 41940767f..4886db198 100644 --- a/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts +++ b/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts @@ -34,7 +34,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; import { Subscription } from 'rxjs/Subscription'; -import { Tag, TagClickEvent } from '../service/interface'; +import { Tag } from '../service/interface'; import { State } from "clarity-angular"; import { diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index dc5f4e7a9..6838e3a5a 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -250,7 +250,7 @@ export interface VulnerabilitySummary { job_id?: number; severity: VulnerabilitySeverity; components: VulnerabilityComponents; - update_time: Date; //Use as complete timestamp + update_time: Date; // Use as complete timestamp } export interface VulnerabilityComponents { @@ -277,3 +277,15 @@ export interface Label { scope: string; project_id: number; } + +export interface CardItemEvent { + event_type: string; + item: any; + additional_info?: any; +} + +export interface ScrollPosition { + sH: number; + sT: number; + cH: number; +}; diff --git a/src/ui_ng/lib/src/shared/shared.module.ts b/src/ui_ng/lib/src/shared/shared.module.ts index 63d85fa16..aea8eef57 100644 --- a/src/ui_ng/lib/src/shared/shared.module.ts +++ b/src/ui_ng/lib/src/shared/shared.module.ts @@ -55,7 +55,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { provide: MissingTranslationHandler, useClass: MyMissingTranslationHandler } - }) + }), ], exports: [ CommonModule, @@ -65,9 +65,8 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { CookieModule, ClipboardModule, ClarityModule, - TranslateModule + TranslateModule, ], providers: [CookieService] }) - export class SharedModule { } diff --git a/src/ui_ng/lib/src/shared/shared.utils.ts b/src/ui_ng/lib/src/shared/shared.utils.ts index fe0550ebe..c9096db9a 100644 --- a/src/ui_ng/lib/src/shared/shared.utils.ts +++ b/src/ui_ng/lib/src/shared/shared.utils.ts @@ -15,7 +15,7 @@ import { NgForm } from '@angular/forms'; import { httpStatusCode, AlertType } from './shared.const'; /** * To handle the error message body - * + * * @export * @returns {string} */ @@ -24,7 +24,7 @@ export const errorHandler = function (error: any): string { return "UNKNOWN_ERROR"; } if (!(error.statusCode || error.status)) { - //treat as string message + // treat as string message return '' + error; } else { switch (error.statusCode || error.status) { @@ -46,4 +46,28 @@ export const errorHandler = function (error: any): string { return "UNKNOWN_ERROR"; } } +} + +export class CancelablePromise { + + constructor(promise: Promise) { + this.wrappedPromise = new Promise((resolve, reject) => { + promise.then((val) => + this.isCanceled ? reject({isCanceled: true}) : resolve(val) + ); + promise.catch((error) => + this.isCanceled ? reject({isCanceled: true}) : reject(error) + ); + }); + } + + private wrappedPromise: Promise; + private isCanceled: boolean; + getPromise(): Promise { + return this.wrappedPromise; + } + + cancel() { + this.isCanceled = true; + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts index 127e5b52e..af9394f5e 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts @@ -58,7 +58,7 @@ export const TAG_DETAIL_HTML: string = ` -
+
{{'TAG.LABELS' | translate }}
diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index 381660994..36d3e5fac 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -17,8 +17,8 @@ export const TAG_TEMPLATE = `
- - + +
@@ -39,31 +39,29 @@ export const TAG_TEMPLATE = `
-
+
-
- - - -
- -
-
{{'LABEL.NO_LABELS' | translate }}
-
- -
-
-
-
+ + + +
+ +
+
{{'LABEL.NO_LABELS' | translate }}
+
+ +
+
+
+
-
{{'REPOSITORY.TAG' | translate}} {{'REPOSITORY.SIZE' | translate}} @@ -73,7 +71,7 @@ export const TAG_TEMPLATE = ` {{'REPOSITORY.AUTHOR' | translate}} {{'REPOSITORY.CREATED' | translate}} {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.LABELS' | translate}} + {{'REPOSITORY.LABELS' | translate}} {{'TAG.PLACEHOLDER' | translate }} @@ -98,7 +96,7 @@ export const TAG_TEMPLATE = ` {{t.author}} {{t.created | date: 'short'}} {{t.docker_version}} - +
@@ -114,7 +112,7 @@ export const TAG_TEMPLATE = `
- + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}     diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index d0e2dda27..ee0137069 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - this.getAllLabels(); + if (!this.withAdmiral) { + this.getAllLabels(); + } } public get filterLabelPieceWidth() { diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 52ae9f4cb..7940d6aa6 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -31,7 +31,7 @@ "clarity-icons": "^0.10.17", "clarity-ui": "^0.10.27", "core-js": "^2.4.1", - "harbor-ui": "0.6.58", + "harbor-ui": "0.6.61", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/src/app/repository/repository-page.component.html b/src/ui_ng/src/app/repository/repository-page.component.html index 13cc9c0c1..06f72277d 100644 --- a/src/ui_ng/src/app/repository/repository-page.component.html +++ b/src/ui_ng/src/app/repository/repository-page.component.html @@ -1,4 +1,5 @@
- +
\ No newline at end of file diff --git a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts index 9a10273d7..96708a18e 100644 --- a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts +++ b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts @@ -79,6 +79,7 @@ export class TagRepositoryComponent implements OnInit { hasChanges(): boolean { return this.repositoryComponent.hasChanges(); } + watchTagClickEvt(tagEvt: TagClickEvent): void { let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name]; this.router.navigate(linkUrl); diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index f776b61e3..525ed7bd8 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -404,6 +404,7 @@ "REPOSITORIES": "Repositories", "OF": "of", "ITEMS": "items", + "NO_ITEMS": "NO ITEMS", "POP_REPOS": "Popular Repositories", "DELETED_REPO_SUCCESS": "Deleted repositories successfully.", "DELETED_TAG_SUCCESS": "Deleted tags successfully.", @@ -416,7 +417,10 @@ "LABELS": ":labels", "ADD_TO_IMAGE": "Add labels to this image", "FILTER_BY_LABEL": "Filter projects by label", - "ADD_LABELS": "Add labels" + "ADD_LABELS": "Add labels", + "ACTION": "ACTION", + "DEPLOY": "DEPLOY", + "ADDITIONAL_INFO": "Add Additional Info" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?" diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index 70a19d087..9f3f9d00c 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -404,6 +404,7 @@ "REPOSITORIES": "Repositorios", "OF": "of", "ITEMS": "elementos", + "NO_ITEMS": "NO ITEMS", "POP_REPOS": "Repositorios Populares", "DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.", "DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.", @@ -415,7 +416,11 @@ "IMAGE": "Imágenes", "LABELS": "Labels", "ADD_TO_IMAGE": "Add labels to this image", - "ADD_LABELS": "Add labels" + "ADD_LABELS": "Add labels", + "FILTER_BY_LABEL": "Filter projects by label", + "ACTION": "ACTION", + "DEPLOY": "DEPLOY", + "ADDITIONAL_INFO": "Add Additional Info" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?" diff --git a/src/ui_ng/src/i18n/lang/fr-fr-lang.json b/src/ui_ng/src/i18n/lang/fr-fr-lang.json index 28784f9fe..0c965db34 100644 --- a/src/ui_ng/src/i18n/lang/fr-fr-lang.json +++ b/src/ui_ng/src/i18n/lang/fr-fr-lang.json @@ -364,7 +364,11 @@ "DELETED_TAG_SUCCESS": "Tag supprimé avec succés.", "COPY": "Copier", "NOTARY_IS_UNDETERMINED": "Ne peut pas déterminer la signature de ce tag.", - "PLACEHOLDER": "Nous ne trouvons aucun dépôt !" + "PLACEHOLDER": "Nous ne trouvons aucun dépôt !", + "IMAGE": "Images", + "ACTION": "ACTION", + "DEPLOY": "DEPLOY", + "ADDITIONAL_INFO": "Add Additional Info" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?" diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index ce2ff9b34..e61f2b90f 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -404,6 +404,7 @@ "REPOSITORIES": "镜像仓库", "OF": "共计", "ITEMS": "条记录", + "NO_ITEMS": "没有记录", "POP_REPOS": "受欢迎的镜像仓库", "DELETED_REPO_SUCCESS": "成功删除镜像仓库。", "DELETED_TAG_SUCCESS": "成功删除镜像标签。", @@ -415,7 +416,11 @@ "IMAGE": "镜像", "LABELS": "标签", "ADD_TO_IMAGE": "添加标签到此镜像", - "ADD_LABELS": "添加标签" + "ADD_LABELS": "添加标签", + "FILTER_BY_LABEL": "过滤标签", + "ACTION": "操作", + "DEPLOY": "部署", + "ADDITIONAL_INFO": "添加信息" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"