Revamp Copy Pull Command (#21155)

* update copy pull command in artifact tags page

* This commit moves "Copy Pull Command" button inside the table
* and add a separate column for better usability

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add user preferences component

* This Commit adds Preferences in navbar
* Updates the navbar

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add container runtime to preference settings

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix: lint & rebase

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update pull cmd for tag

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update copy pull command for digest

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix tests

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add toast message on copy pull command

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add top copy button

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add test for preference settings component

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix lint

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update comments and nits

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update pull cmd prefix name

* Updates title of preference settings
* Updates container runtime to pull cmd prefix

Signed-off-by: bupd <bupdprasanth@gmail.com>

* extend copy pull command with custom prefix

* This commit adds custom as dropdown option
* add custom_runtime localstorage variable for the pull prefix
* fix artifact list tab styles
* align copy icon in artifact tag list tab

Signed-off-by: bupd <bupdprasanth@gmail.com>

* minor fix

* allow only lowercase alphabets

Signed-off-by: bupd <bupdprasanth@gmail.com>

* remove unused copy pull command in i18n

* removes unused in copy_pull_command in i18n in all languages

Signed-off-by: bupd <bupdprasanth@gmail.com>

* remove commented line

Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>

* fix es-es-lang

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
This commit is contained in:
Prasanth Baskar 2025-03-03 18:20:05 +05:30 committed by GitHub
parent b9528d8deb
commit 8419bb6beb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 876 additions and 307 deletions

View File

@ -201,6 +201,7 @@
</div>
</clr-main-container>
<account-settings-modal></account-settings-modal>
<preference-settings></preference-settings>
<password-setting></password-setting>
<global-confirmation-dialog></global-confirmation-dialog>
<about-dialog></about-dialog>

View File

@ -18,70 +18,80 @@ import { SkinableConfig } from '../../services/skinable-config.service';
import { AppConfigService } from '../../services/app-config.service';
import { ErrorHandler } from '../../shared/units/error-handler';
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
import { PreferenceSettingsComponent } from '../preference-settings/preference-settings.component';
import { InlineAlertComponent } from '../../shared/components/inline-alert/inline-alert.component';
import { ScannerService } from '../../../../ng-swagger-gen/services/scanner.service';
import { UserService } from '../../../../ng-swagger-gen/services/user.service';
// Mocks
const fakeSessionService = {
getCurrentUser: function () {
return { has_admin_role: true };
},
};
const fakeSearchTriggerService = {
searchTriggerChan$: of('null'),
searchCloseChan$: of(null),
};
const mockMessageHandlerService = null;
const mockPasswordSettingService = null;
const mockSkinableConfig = {
getSkinConfig: function () {
return {
headerBgColor: {
darkMode: '',
lightMode: '',
},
loginBgImg: '',
loginTitle: '',
product: {
name: '',
logo: '',
introduction: '',
},
};
},
};
const fakeAppConfigService = {
isLdapMode: function () {
return true;
},
isHttpAuthMode: function () {
return false;
},
isOidcMode: function () {
return false;
},
getConfig: function () {
return {
with_trivy: true,
};
},
};
const fakedUserService = {
getCurrentUserInfo() {
return of({});
},
setCliSecret() {
return of(null);
},
};
describe('HarborShellComponent', () => {
let component: HarborShellComponent;
let fixture: ComponentFixture<HarborShellComponent>;
let fakeSessionService = {
getCurrentUser: function () {
return { has_admin_role: true };
},
};
let fakeSearchTriggerService = {
searchTriggerChan$: of('null'),
searchCloseChan$: of(null),
};
let mockMessageHandlerService = null;
let mockPasswordSettingService = null;
let mockSkinableConfig = {
getSkinConfig: function () {
return {
headerBgColor: {
darkMode: '',
lightMode: '',
},
loginBgImg: '',
loginTitle: '',
product: {
name: '',
logo: '',
introduction: '',
},
};
},
};
let fakeAppConfigService = {
isLdapMode: function () {
return true;
},
isHttpAuthMode: function () {
return false;
},
isOidcMode: function () {
return false;
},
getConfig: function () {
return {
with_trivy: true,
};
},
};
const fakedUserService = {
getCurrentUserInfo() {
return of({});
},
setCliSecret() {
return of(null);
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot(),
TranslateModule.forRoot(), // Ensure TranslateModule is imported
ClarityModule,
BrowserAnimationsModule,
FormsModule,
@ -89,6 +99,7 @@ describe('HarborShellComponent', () => {
declarations: [
HarborShellComponent,
AccountSettingsModalComponent,
PreferenceSettingsComponent,
PasswordSettingComponent,
AboutDialogComponent,
InlineAlertComponent,
@ -105,10 +116,7 @@ describe('HarborShellComponent', () => {
provide: MessageHandlerService,
useValue: mockMessageHandlerService,
},
{
provide: UserService,
useValue: fakedUserService,
},
{ provide: UserService, useValue: fakedUserService },
{
provide: PasswordSettingService,
useValue: mockPasswordSettingService,
@ -128,6 +136,9 @@ describe('HarborShellComponent', () => {
).componentInstance;
component.accountSettingsModal.inlineAlert =
TestBed.createComponent(InlineAlertComponent).componentInstance;
component.prefSetting = TestBed.createComponent(
PreferenceSettingsComponent
).componentInstance;
component.pwdSetting = TestBed.createComponent(
PasswordSettingComponent
).componentInstance;
@ -139,6 +150,7 @@ describe('HarborShellComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should open users profile', async () => {
component.openModal({
modalName: modalEvents.USER_PROFILE,
@ -149,7 +161,18 @@ describe('HarborShellComponent', () => {
fixture.nativeElement.querySelector('#account_settings_username');
expect(accountSettingsUsernameInput).toBeTruthy();
});
it('should open users changPwd', async () => {
it('should open users preferences', async () => {
component.openModal({
modalName: modalEvents.PREFERENCES,
modalFlag: false,
});
await fixture.whenStable();
const dropdowns = fixture.nativeElement.querySelector('.dropdowns');
expect(dropdowns).toBeTruthy();
});
it('should open users changePwd', async () => {
component.openModal({
modalName: modalEvents.CHANGE_PWD,
modalFlag: false,
@ -159,6 +182,7 @@ describe('HarborShellComponent', () => {
fixture.nativeElement.querySelector('#oldPassword');
expect(oldPasswordInput).toBeTruthy();
});
it('should open users about-dialog', async () => {
component.openModal({ modalName: modalEvents.ABOUT, modalFlag: false });
await fixture.whenStable();

View File

@ -37,6 +37,7 @@ import { THEME_ARRAY, ThemeInterface } from '../../services/theme';
import { clone } from '../../shared/units/utils';
import { ThemeService } from '../../services/theme.service';
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
import { PreferenceSettingsComponent } from '../preference-settings/preference-settings.component';
import {
EventService,
HarborEvent,
@ -53,6 +54,9 @@ export class HarborShellComponent implements OnInit, OnDestroy {
@ViewChild(AccountSettingsModalComponent)
accountSettingsModal: AccountSettingsModalComponent;
@ViewChild(PreferenceSettingsComponent)
prefSetting: PreferenceSettingsComponent;
@ViewChild(PasswordSettingComponent)
pwdSetting: PasswordSettingComponent;
@ -176,6 +180,9 @@ export class HarborShellComponent implements OnInit, OnDestroy {
case modalEvents.USER_PROFILE:
this.accountSettingsModal.open();
break;
case modalEvents.PREFERENCES:
this.prefSetting.open();
break;
case modalEvents.CHANGE_PWD:
this.pwdSetting.open();
break;

View File

@ -13,6 +13,7 @@
// limitations under the License.
export const modalEvents = {
USER_PROFILE: 'USER_PROFILE',
PREFERENCES: 'PREFERENCES',
CHANGE_PWD: 'CHANGE_PWD',
ABOUT: 'ABOUT',
};

View File

@ -0,0 +1,157 @@
<clr-modal
[(clrModalOpen)]="opened"
[clrModalClosable]="false"
[clrModalStaticBackdrop]="false">
<h3 class="modal-title">{{ 'CHANGE_PREF.TITLE' | translate }}</h3>
<div class="modal-body body-format dialog-body">
<div class="dropdowns content-container">
<div class="content-area centered-content-area">
<div class="clr-control-label">
{{ 'CHANGE_PREF.LANGUAGE' | translate }}
</div>
<clr-dropdown class="dropdown-lang dropdown bottom-left">
<button class="nav-icon nav-icon-width" clrDropdownToggle>
<clr-icon shape="world" class="icon-left"></clr-icon>
<span class="currentLocale">{{ currentLang }}</span>
<clr-icon size="10" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<a
*ngFor="let lang of guiLanguages"
href="javascript:void(0)"
clrDropdownItem
(click)="switchLanguage(lang[0])"
[class.lang-selected]="matchLang(lang[0])"
>{{ lang[1][0] }}</a
>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="content-area centered-content-area">
<div class="clr-control-label">
{{ 'CHANGE_PREF.DATE_TIME_FORMAT' | translate }}
</div>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon nav-icon-width" clrDropdownToggle>
<clr-icon shape="date" class="icon-left"></clr-icon>
<span class="currentLocale">{{
currentDatetimeRendering | translate
}}</span>
<clr-icon size="10" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<a
*ngFor="let rendering of guiDatetimeRenderings"
href="javascript:void(0)"
clrDropdownItem
(click)="switchDatetimeRendering(rendering[0])"
[class.locale-selected]="
matchDatetimeRendering(rendering[0])
"
>{{ rendering[1] | translate }}</a
>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="content-area centered-content-area">
<div class="clr-control-label">
{{ 'CHANGE_PREF.PULL_CMD_PREFIX' | translate }}
</div>
<clr-dropdown class="dropdown-lang dropdown bottom-left">
<button class="nav-icon nav-icon-width" clrDropdownToggle>
<clr-icon shape="bundle" class="icon-left"></clr-icon>
<span class="currentLocale">{{ currentRuntime }}</span>
<clr-icon size="10" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<clr-dropdown>
<button clrDropdownTrigger>custom</button>
<clr-dropdown-menu *clrIfOpen>
<form
#customruntimeForm="ngForm"
class="clr-form">
<div class="clr-form-control">
<label
for="customPrefix"
class="clr-control-label"
>Custom</label
>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input
type="text"
id="customPrefix"
name="customPrefix"
placeholder="Enter custom prefix"
[(ngModel)]="customRuntime"
#customPrefix="ngModel"
[ngClass]="{
'is-invalid':
customPrefix.invalid &&
customPrefix.touched
}"
pattern="^[a-z]+$"
minlength="2"
class="clr-input" />
<cds-icon
class="clr-validate-icon"
shape="exclamation-circle"></cds-icon>
</div>
<div
*ngIf="
customPrefix.invalid &&
customPrefix.touched
">
<div
*ngIf="customPrefix.errors?.['required']">
Prefix is required
</div>
<div
*ngIf="customPrefix.errors?.['minlength']">
Prefix must be at least 2
characters
</div>
<div
*ngIf="customPrefix.errors?.['pattern']">
Prefix must only contain
lowercase alphabets (a-z)
</div>
</div>
</div>
<div class="modal-footer">
<button
clrDropdownItem
type="button"
class="btn btn-primary"
id="ok-btn"
[disabled]="!isValid"
(click)="addCustomRuntime()">
{{ 'BUTTON.OK' | translate }}
</button>
</div>
</div>
</form>
</clr-dropdown-menu>
</clr-dropdown>
<ng-container *ngFor="let runtime of guiRuntimes">
<a
href="javascript:void(0)"
clrDropdownItem
(click)="switchRuntime(runtime[0])"
[class.lang-selected]="matchRuntime(runtime[0])"
>{{ runtime[1] }}</a
>
</ng-container>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</div>
</div>
<div class="modal-footer margin-left-override">
<button type="button" class="btn btn-primary" (click)="close()">
{{ 'BUTTON.CLOSE' | translate }}
</button>
</div>
</clr-modal>

View File

@ -0,0 +1,32 @@
.locale-selected {
font-weight: bold;
}
.nav-icon-width {
width: auto !important;
padding-left: 18px !important;
.icon-left {
left: -8px;
}
/* stylelint-disable */
.currentLocale {
padding-right: 40px;
padding-left: 10px;
}
}
.centered-content-area {
display: flex;
justify-content: space-between;
align-items: center;
width: 70%;
}
.dropdowns {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: start;
gap: 20px;
}

View File

@ -0,0 +1,24 @@
import { PreferenceSettingsComponent } from './preference-settings.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedTestingModule } from 'src/app/shared/shared.module';
describe('PreferenceSettingsComponent', () => {
let component: PreferenceSettingsComponent;
let fixture: ComponentFixture<PreferenceSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PreferenceSettingsComponent],
imports: [SharedTestingModule],
}).compileComponents();
fixture = TestBed.createComponent(PreferenceSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,195 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
getContainerRuntime,
getCustomContainerRuntime,
getDatetimeRendering,
} from 'src/app/shared/units/shared.utils';
import { registerLocaleData } from '@angular/common';
import { forkJoin, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ClrCommonStrings } from '@clr/angular/utils/i18n/common-strings.interface';
import { ClrCommonStringsService } from '@clr/angular';
import {
CUSTOM_RUNTIME_LOCALSTORAGE_KEY,
DATETIME_RENDERINGS,
DatetimeRendering,
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY,
DEFAULT_LANG_LOCALSTORAGE_KEY,
DEFAULT_RUNTIME_LOCALSTORAGE_KEY,
DefaultDatetimeRendering,
DeFaultLang,
DeFaultRuntime,
LANGUAGES,
RUNTIMES,
stringsForClarity,
SupportedLanguage,
SupportedRuntime,
} from '../../shared/entities/shared.const';
import { NgForm } from '@angular/forms';
import { InlineAlertComponent } from 'src/app/shared/components/inline-alert/inline-alert.component';
@Component({
selector: 'preference-settings',
templateUrl: 'preference-settings.component.html',
styleUrls: ['preference-settings.component.scss'],
})
export class PreferenceSettingsComponent implements OnInit {
readonly guiLanguages = Object.entries(LANGUAGES);
readonly guiRuntimes = Object.entries(RUNTIMES).filter(
([_, value]) => value !== RUNTIMES.custom
);
readonly guiDatetimeRenderings = Object.entries(DATETIME_RENDERINGS);
selectedLang: SupportedLanguage = DeFaultLang;
selectedRuntime: SupportedRuntime = DeFaultRuntime;
selectedDatetimeRendering: DatetimeRendering = DefaultDatetimeRendering;
opened: boolean = false;
error: any = null;
customRuntime: string = '';
@ViewChild('customruntimeForm', { static: false })
customRuntimeForm: NgForm;
@ViewChild(InlineAlertComponent, { static: true })
inlineAlert: InlineAlertComponent;
constructor(
private translate: TranslateService,
private commonStrings: ClrCommonStringsService
) {}
ngOnInit(): void {
this.selectedLang = this.translate.currentLang as SupportedLanguage;
if (this.selectedLang) {
registerLocaleData(
LANGUAGES[this.selectedLang][1],
this.selectedLang
);
this.translateClarityComponents();
}
this.selectedDatetimeRendering = getDatetimeRendering();
this.selectedRuntime = getContainerRuntime();
this.customRuntime = getCustomContainerRuntime();
}
// Check If form is valid
public get isValid(): boolean {
const customPrefixControl =
this.customRuntimeForm?.form.get('customPrefix');
return (
customPrefixControl?.valid &&
customPrefixControl?.value?.trim() !== '' &&
this.error === null
);
}
addCustomRuntime() {
if (this.customRuntime.trim()) {
const customRuntimeValue = this.customRuntime.trim();
this.switchRuntime('custom');
this.switchCustomRuntime(customRuntimeValue);
}
}
//Internationalization for Clarity components, refer to https://clarity.design/documentation/internationalization
translateClarityComponents() {
const translatedObservables: Observable<string | any>[] = [];
const translatedStringsForClarity: Partial<ClrCommonStrings> = {};
for (let key in stringsForClarity) {
translatedObservables.push(
this.translate.get(stringsForClarity[key]).pipe(
map(res => {
return [key, res];
})
)
);
}
forkJoin(translatedObservables).subscribe(res => {
if (res?.length) {
res.forEach(item => {
translatedStringsForClarity[item[0]] = item[1];
});
this.commonStrings.localize(translatedStringsForClarity);
}
});
}
public get currentRuntime(): string {
if (this.selectedRuntime) {
return RUNTIMES[this.selectedRuntime] as string;
}
return null;
}
public get currentLang(): string {
if (this.selectedLang) {
return LANGUAGES[this.selectedLang][0] as string;
}
return null;
}
public get currentDatetimeRendering(): string {
return DATETIME_RENDERINGS[this.selectedDatetimeRendering];
}
matchLang(lang: SupportedLanguage): boolean {
return lang === this.selectedLang;
}
matchRuntime(runtime: SupportedRuntime): boolean {
return runtime === this.selectedRuntime;
}
matchDatetimeRendering(datetime: DatetimeRendering): boolean {
return datetime === this.selectedDatetimeRendering;
}
// Switch languages
switchLanguage(lang: SupportedLanguage): void {
this.selectedLang = lang;
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang);
// due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module
// have to reload
this.translate.use(lang).subscribe(() => window.location.reload());
}
switchRuntime(runtime: SupportedRuntime): void {
this.selectedRuntime = runtime;
localStorage.setItem(DEFAULT_RUNTIME_LOCALSTORAGE_KEY, runtime);
}
switchCustomRuntime(runtime: SupportedRuntime): void {
localStorage.setItem(CUSTOM_RUNTIME_LOCALSTORAGE_KEY, runtime);
}
switchDatetimeRendering(datetime: DatetimeRendering): void {
this.selectedDatetimeRendering = datetime;
localStorage.setItem(
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY,
datetime
);
// have to reload,as HarborDatetimePipe is pure pipe
window.location.reload();
}
public open(): void {
this.opened = true;
}
public close(): void {
this.opened = false;
}
}

View File

@ -184,8 +184,9 @@
[registryUrl]="registryUrl"
[projectName]="projectName"
[repoName]="repoName"
[selectedRow]="selectedRow"
[isTopModel]="true"
class="mr-1"></app-pull-command>
<app-artifact-filter
[withDivider]="true"
(filterEvent)="filterEvent($event)"
@ -196,7 +197,7 @@
</div>
</clr-dg-action-bar>
<clr-dg-column class="flex-max-width" [clrDgSortBy]="'digest'"
<clr-dg-column [clrDgSortBy]="'digest'"
><ng-template
[clrDgHideableColumn]="{ hidden: hiddenArray[0] }">
{{ 'REPOSITORY.ARTIFACTS_COUNT' | translate }}
@ -259,7 +260,7 @@
<clr-dg-row
*ngFor="let artifact of artifactList; let i = index"
[clrDgItem]="artifact">
<clr-dg-cell class="flex-max-width truncated">
<clr-dg-cell>
<div class="cell white-normal">
<div
class="artifact-icon clr-display-inline-block"
@ -279,6 +280,7 @@
{{ artifact.digest | slice : 0 : 15 }}</a
>
<clr-tooltip
class="fix-width"
*ngIf="
artifact?.references &&
artifact?.references?.length
@ -302,6 +304,12 @@
{{ 'REPOSITORY.ARTIFACT_TOOTIP' | translate }}
</clr-tooltip-content>
</clr-tooltip>
<app-pull-command
[registryUrl]="registryUrl"
[projectName]="projectName"
[repoName]="repoName"
[artifact]="artifact"
class="fix-width"></app-pull-command>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="depth">

View File

@ -11,11 +11,16 @@
height: 0;
}
.fix-width {
width: 40px;
}
.truncated {
width: 100px;
line-height: 20px;
height: 20px;
display: inline-block;
display: flex;
align-items: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@ -198,6 +203,8 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.signpost-item {

View File

@ -103,6 +103,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
repoName: string;
registryUrl: string;
artifactList: ArtifactFront[] = [];
artifact: ArtifactFront;
availableTime = AVAILABLE_TIME;
inprogress: boolean;
pullComparator: Comparator<Artifact> = new CustomComparator<Artifact>(

View File

@ -1,131 +1,68 @@
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="mr-1" *ngIf="!isTagMode">
<button
[disabled]="
selectedRow?.length !== 1 || !hasPullCommand(selectedRow[0])
<div *ngIf="isTopModel" class="form-group">
<hbr-copy-input
#copyInputComponent
(onCopySuccess)="onCpSuccess(getPullCommandForTopModel())"
inputSize="55"
headerTitle=""
defaultValue="{{ getPullCommandForTopModel() }}"></hbr-copy-input>
</div>
<clr-dropdown *ngIf="!isTopModel && !isTagMode">
<hbr-copy-input
*ngIf="isImage(artifact)"
[title]="getPullCommandForRuntimeByDigest(artifact)"
[iconMode]="true"
(onCopySuccess)="
onCpSuccess(getPullCommandForRuntimeByDigest(artifact))
"
class="btn btn-link copy-pull-command"
clrDropdownTrigger>
{{ 'PUSH_IMAGE.COPY_PULL_COMMAND' | translate }}
<cds-icon shape="angle" direction="down"></cds-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
<ng-container *ngIf="isImage(selectedRow[0])">
<div
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForDocker(selectedRow[0])"
[iconMode]="true"
[defaultValue]="
getPullCommandForDocker(selectedRow[0])
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.DOCKER' | translate }}</span>
</div>
<div
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForPadMan(selectedRow[0])"
[iconMode]="true"
[defaultValue]="
getPullCommandForPadMan(selectedRow[0])
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.PODMAN' | translate }}</span>
</div>
</ng-container>
<div
*ngIf="isCNAB(selectedRow[0])"
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForCNAB(selectedRow[0])"
[iconMode]="true"
[defaultValue]="
getPullCommandForCNAB(selectedRow[0])
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.CNAB' | translate }}</span>
</div>
<div
*ngIf="isChart(selectedRow[0])"
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForChart(selectedRow[0])"
[iconMode]="true"
[defaultValue]="
getPullCommandForChart(selectedRow[0])
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.HELM' | translate }}</span>
</div>
</clr-dropdown-menu>
[defaultValue]="
getPullCommandForRuntimeByDigest(artifact)
"></hbr-copy-input>
<div
*ngIf="isCNAB(artifact)"
class="flex"
aria-label="Dropdown header Action">
<hbr-copy-input
[title]="getPullCommandForCNAB(artifact)"
[iconMode]="true"
(onCopySuccess)="onCpSuccess(getPullCommandForCNAB(artifact))"
[defaultValue]="getPullCommandForCNAB(artifact)"></hbr-copy-input>
</div>
<div
*ngIf="isChart(artifact)"
class="flex"
aria-label="Dropdown header Action">
<hbr-copy-input
[title]="getPullCommandForChart(artifact)"
[iconMode]="true"
(onCopySuccess)="onCpSuccess(getPullCommandForChart(artifact))"
[defaultValue]="getPullCommandForChart(artifact)"></hbr-copy-input>
</div>
</clr-dropdown>
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="mr-1" *ngIf="isTagMode">
<button
[disabled]="
selectedTags?.length !== 1 || !hasPullCommandForTag(artifact)
"
class="btn btn-link copy-pull-command"
clrDropdownTrigger>
{{ 'PUSH_IMAGE.COPY_PULL_COMMAND' | translate }}
<cds-icon shape="angle" direction="down"></cds-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
<ng-container *ngIf="isImage(artifact)">
<div
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForDockerByTag(artifact)"
[iconMode]="true"
[defaultValue]="
getPullCommandForDockerByTag(artifact)
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.DOCKER' | translate }}</span>
</div>
<div
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForPadManByTag(artifact)"
[iconMode]="true"
[defaultValue]="
getPullCommandForPadManByTag(artifact)
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.PODMAN' | translate }}</span>
</div>
</ng-container>
<div
*ngIf="isCNAB(artifact)"
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForCNABByTag(artifact)"
[iconMode]="true"
[defaultValue]="
getPullCommandForCNABByTag(artifact)
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.CNAB' | translate }}</span>
</div>
<div
*ngIf="isChart(artifact)"
class="flex"
aria-label="Dropdown header Action"
clrDropdownItem>
<hbr-copy-input
[title]="getPullCommandForChartByTag(artifact)"
[iconMode]="true"
[defaultValue]="
getPullCommandForChartByTag(artifact)
"></hbr-copy-input>
<span>{{ 'PUSH_IMAGE.HELM' | translate }}</span>
</div>
</clr-dropdown-menu>
<clr-dropdown
class="mr-1"
*ngIf="isTagMode && !isTopModel"
[disabled]="!hasPullCommandForTag(artifact)">
<hbr-copy-input
*ngIf="isImage(artifact)"
[title]="getPullCommandForRuntimeByTag(artifact)"
[iconMode]="true"
(onCopySuccess)="onCpSuccess(getPullCommandForRuntimeByTag(artifact))"
[defaultValue]="
getPullCommandForRuntimeByTag(artifact)
"></hbr-copy-input>
<hbr-copy-input
*ngIf="isCNAB(artifact)"
[title]="getPullCommandForCNABByTag(artifact)"
[iconMode]="true"
(onCopySuccess)="onCpSuccess(getPullCommandForCNABByTag(artifact))"
[defaultValue]="getPullCommandForCNABByTag(artifact)"></hbr-copy-input>
<hbr-copy-input
*ngIf="isChart(artifact)"
[title]="getPullCommandForChartByTag(artifact)"
[iconMode]="true"
(onCopySuccess)="onCpSuccess(getPullCommandForChartByTag(artifact))"
[defaultValue]="getPullCommandForChartByTag(artifact)"></hbr-copy-input>
</clr-dropdown>

View File

@ -1,10 +1,12 @@
import { PullCommandComponent } from './pull-command.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedTestingModule } from '../../../../../../../../shared/shared.module';
import { ArtifactType } from '../../../../artifact'; // Import the necessary type
describe('PullCommandComponent', () => {
let component: PullCommandComponent;
let fixture: ComponentFixture<PullCommandComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PullCommandComponent],
@ -13,6 +15,14 @@ describe('PullCommandComponent', () => {
fixture = TestBed.createComponent(PullCommandComponent);
component = fixture.componentInstance;
// Mock the artifact input with a valid value
component.artifact = {
type: ArtifactType.IMAGE,
digest: 'sampleDigest',
tags: [{ name: 'latest' }],
};
fixture.detectChanges();
});

View File

@ -6,9 +6,12 @@ import {
Clients,
getPullCommandByDigest,
getPullCommandByTag,
getPullCommandForTop,
hasPullCommand,
} from '../../../../artifact';
import { Tag } from '../../../../../../../../../../ng-swagger-gen/models/tag';
import { getContainerRuntime } from 'src/app/shared/units/shared.utils';
import { MessageHandlerService } from 'src/app/shared/services/message-handler.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-pull-command',
@ -16,6 +19,8 @@ import { Tag } from '../../../../../../../../../../ng-swagger-gen/models/tag';
styleUrls: ['./pull-command.component.scss'],
})
export class PullCommandComponent {
@Input()
isTopModel: boolean = false; // TopModel is for tab top component,
@Input()
isTagMode: boolean = false; // tagMode is for tag list datagrid,
@Input()
@ -24,17 +29,20 @@ export class PullCommandComponent {
registryUrl: string;
@Input()
repoName: string;
@Input()
selectedRow: Artifact[];
// for tagMode
@Input()
selectedTags: Tag[];
selectedTag: string;
@Input()
artifact: Artifact;
@Input()
accessoryType: string;
constructor(
private msgHandler: MessageHandlerService,
private translate: TranslateService
) {}
hasPullCommand(artifact: Artifact): boolean {
return hasPullCommand(artifact);
}
@ -51,25 +59,31 @@ export class PullCommandComponent {
return artifact.type === ArtifactType.CHART;
}
getPullCommandForDocker(artifact: Artifact): string {
return getPullCommandByDigest(
artifact.type,
// get client based on the selected container runtime
getSelectedClient(): Clients {
const runtime = getContainerRuntime();
const client = Object.values(Clients).find(client => client == runtime);
// return client if match found otherwise return (DOCKER)
return client ? client : Clients.DOCKER;
}
getPullCommandForTopModel(): string {
return getPullCommandForTop(
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
artifact.digest,
Clients.DOCKER
this.getSelectedClient()
);
}
getPullCommandForPadMan(artifact: Artifact): string {
getPullCommandForRuntimeByDigest(artifact: Artifact): string {
return getPullCommandByDigest(
artifact.type,
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
artifact.digest,
Clients.PODMAN
this.getSelectedClient()
);
}
@ -107,25 +121,14 @@ export class PullCommandComponent {
);
}
getPullCommandForDockerByTag(artifact: Artifact): string {
getPullCommandForRuntimeByTag(artifact: Artifact): string {
return getPullCommandByTag(
artifact.type,
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
this.selectedTags[0].name,
Clients.DOCKER
);
}
getPullCommandForPadManByTag(artifact: Artifact): string {
return getPullCommandByTag(
artifact.type,
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
this.selectedTags[0].name,
Clients.PODMAN
this.selectedTag,
this.getSelectedClient()
);
}
@ -135,7 +138,7 @@ export class PullCommandComponent {
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
this.selectedTags[0].name,
this.selectedTag,
Clients.CNAB
);
}
@ -146,8 +149,19 @@ export class PullCommandComponent {
`${this.registryUrl ? this.registryUrl : location.hostname}/${
this.projectName
}/${this.repoName}`,
this.selectedTags[0].name,
this.selectedTag,
Clients.CHART
);
}
onCpSuccess(copied: string): void {
// $event is the defaultValue emitted from CopyInputComponent
this.translate
.get('REPOSITORY.COPY_SUCCESS', {
param: copied,
})
.subscribe((res: string) => {
this.msgHandler.showSuccess(res);
});
}
}

View File

@ -36,15 +36,6 @@
</button>
</div>
<div class="right-pos">
<app-pull-command
[isTagMode]="true"
[artifact]="artifactDetails"
[accessoryType]="accessoryType"
[registryUrl]="registryUrl"
[projectName]="projectName"
[repoName]="repositoryName"
[selectedTags]="selectedRow"
class="mr-1"></app-pull-command>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
@ -129,16 +120,25 @@
</clr-dg-placeholder>
<clr-dg-row *ngFor="let tag of currentTags" [clrDgItem]="tag">
<clr-dg-cell>
<div class="cell white-normal" [class.immutable]="tag.immutable">
<div class="pull" [class.immutable]="tag.immutable">
<span
href="javascript:void(0)"
class="max-width-100"
class="max-width-150"
title="{{ tag.name }}"
>{{ tag.name }}</span
>
<span *ngIf="tag.immutable" class="label label-info ml-8">{{
'REPOSITORY.IMMUTABLE' | translate
}}</span>
<app-pull-command
class="pull-btn"
[isTagMode]="true"
[artifact]="artifactDetails"
[accessoryType]="accessoryType"
[registryUrl]="registryUrl"
[projectName]="projectName"
[repoName]="repositoryName"
[selectedTag]="tag.name"></app-pull-command>
</div>
</clr-dg-cell>
<clr-dg-cell>{{

View File

@ -1,61 +1,73 @@
.label-form {
max-width: 100% !important;
max-width: 100% !important;
}
.clr-control-container {
margin-bottom: 1rem;
position: relative;
margin-bottom: 1rem;
position: relative;
}
.btn.remove-btn {
border: none;
height: 0.6rem;
line-height: 1;
border: none;
height: 0.6rem;
line-height: 1;
}
.immutable {
display: flex;
align-items: center;
position: relative;
display: flex;
align-items: center;
position: relative;
.label {
position: absolute;
right: 0;
margin-right: 0;
}
.label {
position: absolute;
right: 0;
margin-right: 0;
}
}
.datagrid-action-bar {
margin-top: 0.5rem;
margin-top: 0.5rem;
}
.position-ab {
position: absolute;
position: absolute;
}
.white-space-nowrap {
white-space: nowrap;
white-space: nowrap;
}
.pull {
display: flex;
flex-flow: row wrap;
align-content: center;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
cursor: pointer;
cursor: pointer;
}
.spinner-tag {
position: absolute;
right: -.7rem;
top: 1.2rem;
position: absolute;
right: -0.7rem;
top: 1.2rem;
}
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
display: flex;
align-items: center;
justify-content: space-between;
}
.right-pos {
margin-right: 35px;
display: flex;
align-items: center;
margin-right: 35px;
display: flex;
align-items: center;
}
.pull-btn {
float: right;
margin-right: -20px;
}

View File

@ -2,6 +2,7 @@ import { Accessory } from 'ng-swagger-gen/models/accessory';
import { Artifact } from '../../../../../../ng-swagger-gen/models/artifact';
import { Platform } from '../../../../../../ng-swagger-gen/models/platform';
import { Label } from '../../../../../../ng-swagger-gen/models/label';
import { getCustomContainerRuntime } from 'src/app/shared/units/shared.utils';
export interface ArtifactFront extends Artifact {
platform?: Platform;
@ -101,6 +102,18 @@ export function hasPullCommand(artifact: Artifact): boolean {
);
}
export function getPullCommandForTop(url: string, client: Clients): string {
if (url) {
if (Object.values(Clients).includes(client)) {
if (client == 'custom') {
return `${getCustomContainerRuntime()} pull ${url}`;
}
return `${client} pull ${url}`;
}
}
return null;
}
export function getPullCommandByDigest(
artifactType: string,
url: string,
@ -109,13 +122,14 @@ export function getPullCommandByDigest(
): string {
if (artifactType && url && digest) {
if (artifactType === ArtifactType.IMAGE) {
if (client === Clients.DOCKER) {
return `${Clients.DOCKER} pull ${url}@${digest}`;
}
if (client === Clients.PODMAN) {
return `${Clients.PODMAN} pull ${url}@${digest}`;
if (Object.values(Clients).includes(client)) {
if (client == 'custom') {
return `${getCustomContainerRuntime()} pull ${url}@${digest}`;
}
return `${client} pull ${url}@${digest}`;
}
}
if (artifactType === ArtifactType.CNAB) {
return `${Clients.CNAB} pull ${url}@${digest}`;
}
@ -131,11 +145,12 @@ export function getPullCommandByTag(
): string {
if (artifactType && url && tag) {
if (artifactType === ArtifactType.IMAGE) {
if (client === Clients.DOCKER) {
return `${Clients.DOCKER} pull ${url}:${tag}`;
}
if (client === Clients.PODMAN) {
return `${Clients.PODMAN} pull ${url}:${tag}`;
if (Object.values(Clients).includes(client)) {
if (client == 'custom') {
return `${getCustomContainerRuntime()} pull ${url}:${tag}`;
}
return `${client} pull ${url}:${tag}`;
}
}
if (artifactType === ArtifactType.CNAB) {
@ -159,17 +174,14 @@ export interface ArtifactFilterEvent {
export enum Clients {
DOCKER = 'docker',
PODMAN = 'podman',
NERDCTL = 'nerdctl',
CONTAINERD = 'ctr',
CRI_O = 'crictl',
CUSTOM = 'custom',
CHART = 'helm',
CNAB = 'cnab-to-oci',
}
export enum ClientNames {
DOCKER = 'Docker',
PODMAN = 'Podman',
CHART = 'Helm',
CNAB = 'CNAB',
}
export enum ArtifactSbomType {
SPDX = 'SPDX',
}

View File

@ -18,7 +18,9 @@
<global-search></global-search>
<div class="header-actions">
<clr-dropdown class="dropdown-lang dropdown bottom-left">
<clr-dropdown
class="dropdown-lang dropdown bottom-left"
*ngIf="!isSessionValid">
<button class="nav-icon nav-icon-width" clrDropdownToggle>
<clr-icon shape="world" class="icon-left"></clr-icon>
<span class="currentLocale">{{ currentLang }}</span>
@ -35,7 +37,9 @@
>
</clr-dropdown-menu>
</clr-dropdown>
<clr-dropdown class="dropdown-locale dropdown bottom-left">
<clr-dropdown
class="dropdown-locale dropdown bottom-left"
*ngIf="!isSessionValid">
<button class="nav-icon nav-icon-width" clrDropdownToggle>
<clr-icon shape="date" class="icon-left"></clr-icon>
<span class="currentLocale">{{
@ -76,6 +80,12 @@
(click)="openAccountSettingsModal()"
>{{ 'ACCOUNT_SETTINGS.PROFILE' | translate }}</a
>
<a
href="javascript:void(0)"
clrDropdownItem
(click)="openPreferencesModal()"
>{{ 'ACCOUNT_SETTINGS.PREFERENCES' | translate }}</a
>
<a
*ngIf="canChangePassword"
href="javascript:void(0)"

View File

@ -173,6 +173,14 @@ export class NavigatorComponent implements OnInit {
});
}
// Open change preferences dialog
openPreferencesModal(): void {
this.showDialogModalAction.emit({
modalName: modalEvents.PREFERENCES,
modalFlag: true,
});
}
// Open change password dialog
openChangePwdModal(): void {
this.showDialogModalAction.emit({

View File

@ -1,7 +1,5 @@
<div>
<div class="command-title" *ngIf="!iconMode">
{{ headerTitle }}
</div>
<div class="command-title" *ngIf="!iconMode">{{ headerTitle }}</div>
<div>
<span [class.hide]="iconMode">
<input

View File

@ -231,7 +231,24 @@ export enum GroupType {
export const REFRESH_TIME_DIFFERENCE = 10000;
//
export const DeFaultRuntime = 'default';
export type SupportedRuntime = string;
export const RUNTIMES = {
default: 'docker',
podman: 'podman',
nerdctl: 'nerdctl',
ctr: 'containerd',
crictl: 'cri-o',
custom: 'custom',
} as const;
export const supportedRuntimes = Object.keys(RUNTIMES) as SupportedRuntime[];
/**
* The default cookie key used to store current used container runtime preference.
*/
export const DEFAULT_RUNTIME_LOCALSTORAGE_KEY = 'harbor-runtime';
export const CUSTOM_RUNTIME_LOCALSTORAGE_KEY = 'harbor-custom-runtime';
//
export const DeFaultLang = 'en-us';
export type SupportedLanguage = string;
export const LANGUAGES = {

View File

@ -92,6 +92,7 @@ import {
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { RobotPermissionsPanelComponent } from './components/robot-permissions-panel/robot-permissions-panel.component';
import { PreferenceSettingsComponent } from '../base/preference-settings/preference-settings.component';
// register necessary components
echarts.use([
@ -168,6 +169,7 @@ ClarityIcons.add({
InlineAlertComponent,
NewUserFormComponent,
MessageComponent,
PreferenceSettingsComponent,
NavigatorComponent,
SearchResultComponent,
GlobalSearchComponent,
@ -211,6 +213,7 @@ ClarityIcons.add({
InlineAlertComponent,
NewUserFormComponent,
MessageComponent,
PreferenceSettingsComponent,
NavigatorComponent,
SearchResultComponent,
GlobalSearchComponent,

View File

@ -22,6 +22,11 @@ import {
httpStatusCode,
SupportedLanguage,
LANGUAGES,
SupportedRuntime,
DEFAULT_RUNTIME_LOCALSTORAGE_KEY,
RUNTIMES,
DeFaultRuntime,
CUSTOM_RUNTIME_LOCALSTORAGE_KEY,
} from '../entities/shared.const';
/**
@ -208,6 +213,35 @@ export const errorHandler = function (error: any): string {
}
};
/**
* Gets the container runtime saved by the user, or the default runtime if no valid saved value is found.
*/
export function getContainerRuntime(): SupportedRuntime {
const savedContainerRuntime = localStorage.getItem(
DEFAULT_RUNTIME_LOCALSTORAGE_KEY
);
if (savedContainerRuntime && isSupportedRuntime(savedContainerRuntime)) {
return savedContainerRuntime;
}
return DeFaultRuntime;
}
function isSupportedRuntime(x: unknown): x is SupportedRuntime {
return Object.keys(RUNTIMES).some(k => k === x);
}
/**
* Gets the custom container runtime saved by the user
*/
export function getCustomContainerRuntime(): SupportedRuntime {
const savedContainerRuntime = localStorage.getItem(
CUSTOM_RUNTIME_LOCALSTORAGE_KEY
);
if (savedContainerRuntime) {
return savedContainerRuntime;
}
return DeFaultRuntime;
}
/**
* Gets the datetime rendering setting saved by the user, or the default setting if no valid saved value is found.
*/

View File

@ -103,7 +103,7 @@
}
.go-link {
color: $mode-link-color1 !important;
color: $mode-link-color1 !important;
border-bottom: 1px solid $mode-link-color1 !important;
}
@ -137,7 +137,7 @@ clr-dg-action-overflow {
background: $select-back-color;
color: $select-option-color;
}
}
}
}
hbr-tag {
@ -252,7 +252,9 @@ clr-header {
hbr-copy-input {
.command-input {
color: $command-input-color;
padding-left: 8px;
background-color: $command-input-bg-color;
border-radius: 4px;
}
}

View File

@ -151,8 +151,15 @@
"SAVE_SUCCESS": "Nutzerpasswort erfolgreich geändert.",
"PASS_TIPS": "8-128 Zeichen mit einem Groß-, einem Kleinbuchstaben und einer Ziffer"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "Nutzerprofil",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Passwort ändern",
"ABOUT": "Info",
"LOGOUT": "Ausloggen"
@ -773,7 +780,7 @@
"ARTIFACT_TOOTIP": "Klicken um die Artefakt-Liste des OCI index zu sehen",
"ARTIFACTS_COUNT": "Artefakte",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Pull Befehl",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "Pull Zeit",
"PUSH_TIME": "Push Zeit",
"IMMUTABLE": "Immutable",
@ -1146,8 +1153,7 @@
"TOOLTIP": "Befehlreferenz um ein Artefakt in das Projekt zu pushen.",
"TAG_COMMAND": "Tag ein Image für dieses Projekt:",
"PUSH_COMMAND": "Push ein Image für dieses Projekt:",
"COPY_ERROR": "Kopieren fehlgeschlagen, bitte die Befehlsreferenz manuell kopieren.",
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
"COPY_ERROR": "Kopieren fehlgeschlagen, bitte die Befehlsreferenz manuell kopieren."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artefakte",

View File

@ -151,8 +151,15 @@
"SAVE_SUCCESS": "User password changed successfully.",
"PASS_TIPS": "8-128 characters long with at least 1 uppercase, 1 lowercase and 1 number"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "User Profile",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Change Password",
"ABOUT": "About",
"LOGOUT": "Log Out"
@ -773,7 +780,7 @@
"ARTIFACT_TOOTIP": "Click to view this OCI index's artifact list",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Pull Command",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
@ -1148,8 +1155,7 @@
"TOOLTIP": "Command references for pushing an artifact to this project.",
"TAG_COMMAND": "Tag an image for this project:",
"PUSH_COMMAND": "Push an image to this project:",
"COPY_ERROR": "Copy failed, please try to manually copy the command references.",
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
"COPY_ERROR": "Copy failed, please try to manually copy the command references."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",

View File

@ -151,8 +151,15 @@
"SAVE_SUCCESS": "Contraseña de usuario guardada satisfactoriamente.",
"PASS_TIPS": "8-128 caracteres con 1 letra mayúscula, 1 minúscula y 1 número"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "Perfil de usuario",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Cambiar contraseña",
"ABOUT": "Acerca de",
"LOGOUT": "Cerrar sesión"
@ -774,7 +781,7 @@
"ARTIFACT_TOOTIP": "Haga clic para ver la lista de artefactos de este índice OCI",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Comando Pull",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "Tiempo de Pull",
"PUSH_TIME": "Tiempo de Push",
"IMMUTABLE": "Immutable",
@ -1145,8 +1152,7 @@
"TOOLTIP": "Referencias de comandos para enviar un artefacto a este proyecto.",
"TAG_COMMAND": "Tag a imagen para este proyecto:",
"PUSH_COMMAND": "Push a imagen para este proyecto:",
"COPY_ERROR": "Copiar fallido, por favor intenta copiar manualmente las referencias del comando.",
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
"COPY_ERROR": "Copiar fallido, por favor intenta copiar manualmente las referencias del comando."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filtrar Artifacts",
@ -2009,4 +2015,4 @@
"INVALID_VALUE": "El CVSS3 score debe estar entre el rango de 0 y 10",
"PAGE_TITLE_TOOLTIP": "El recuento completo de artefactos comprende el total acumulado de artefactos individuales, incluidos los accesorios de los artefactos, así como los artefactos secundarios asociados con el índice de imagen y los artefactos CNAB"
}
}
}

View File

@ -151,8 +151,15 @@
"SAVE_SUCCESS": "Mot de passe utilisateur modifié avec succès.",
"PASS_TIPS": "8-128 caractères avec au moins 1 majuscule, 1 minuscule et 1 chiffre"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "Profil Utilisateur",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Modifier le mot de passe",
"ABOUT": "À propos",
"LOGOUT": "Se déconnecter"
@ -773,7 +780,7 @@
"ARTIFACT_TOOTIP": "Cliquez pour voir la liste des index des artefacts de cet OCI",
"ARTIFACTS_COUNT": "Artefacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Commande de pull",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "Date/Heure de pull",
"PUSH_TIME": "Date/Heure de push",
"IMMUTABLE": "Immutable",
@ -1148,8 +1155,7 @@
"TOOLTIP": "Commandes pour push un artefact dans ce projet.",
"TAG_COMMAND": "Taguer une image pour ce projet :",
"PUSH_COMMAND": "Push une image dans ce projet :",
"COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement les commandes de référence.",
"COPY_PULL_COMMAND": "COMMANDE COPY PULL"
"COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement les commandes de référence."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filtrer les artefacts",

View File

@ -143,6 +143,12 @@
"CONFIRM_TITLE_CLI_GENERATE": "시크릿을 다시 생성할 수 있습니까?",
"CONFIRM_BODY_CLI_GENERATE": "Cli 시크릿을 재생성하면 이전 Cli 시크릿이 삭제됩니다"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"CHANGE_PWD": {
"TITLE": "비밀번호 변경",
"CURRENT_PWD": "현재 비밀번호",
@ -153,6 +159,7 @@
},
"ACCOUNT_SETTINGS": {
"PROFILE": "사용자 프로필",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "비밀번호 변경",
"ABOUT": "About",
"LOGOUT": "로그아웃"
@ -770,7 +777,7 @@
"ARTIFACT_TOOTIP": "이 OCI 인덱스의 아티팩트 목록을 보려면 클릭하세요.",
"ARTIFACTS_COUNT": "아티팩트",
"PULL_COUNT": "풀(Pull) 수",
"PULL_COMMAND": "풀(Pull) 명령어",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "풀(Pull) 시간",
"PUSH_TIME": "푸시 시간",
"IMMUTABLE": "Immutable",
@ -1143,8 +1150,7 @@
"TOOLTIP": "이 프로젝트에 아티팩트를 푸시하기 위한 명령 참조입니다.",
"TAG_COMMAND": "이 프로젝트의 이미지에 태그를 지정:",
"PUSH_COMMAND": "이 프로젝트에 이미지 푸시:",
"COPY_ERROR": "복사에 실패했습니다. 명령 참조를 수동으로 복사해 보세요.",
"COPY_PULL_COMMAND": "풀(PULL) 명령어 복사"
"COPY_ERROR": "복사에 실패했습니다. 명령 참조를 수동으로 복사해 보세요."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "아티팩트 필터",

View File

@ -142,6 +142,12 @@
"CONFIRM_TITLE_CLI_GENERATE": "Gostaria de redefinir o segredo?",
"CONFIRM_BODY_CLI_GENERATE": "Ao fazer isso, o segredo atual não poderá ser recuperado"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"CHANGE_PWD": {
"TITLE": "Alterar Senha",
"CURRENT_PWD": "Senha atual",
@ -152,6 +158,7 @@
},
"ACCOUNT_SETTINGS": {
"PROFILE": "Perfil do usuário",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Alterar senha",
"ABOUT": "Informativo",
"LOGOUT": "Sair"
@ -772,7 +779,7 @@
"ARTIFACT_TOOTIP": "Clique para ver a lista de artefatos OCI",
"ARTIFACTS_COUNT": "Artefatos",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Comando de Pull",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "Horário de Envio",
"PUSH_TIME": "Horário de Recebimento",
"IMMUTABLE": "Imutável",
@ -1143,8 +1150,7 @@
"TOOLTIP": "Referência de comandos para enviar artefatos a este projeto.",
"TAG_COMMAND": "Colocar tag em uma imagem deste projeto:",
"PUSH_COMMAND": "Envia uma imagem a esse projeto:",
"COPY_ERROR": "Cópia falhou, por favor tente copiar o comando de referência manualmente.",
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
"COPY_ERROR": "Cópia falhou, por favor tente copiar o comando de referência manualmente."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filtrar",

View File

@ -143,6 +143,12 @@
"CONFIRM_TITLE_CLI_GENERATE": "Şifreyi yeniden oluşturabileceğine emin misin?",
"CONFIRM_BODY_CLI_GENERATE": "Eğer cli şifresini yeniden oluşturursanız, eski cli şifresi atılır"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"CHANGE_PWD": {
"TITLE": "Şifreyi değiştir",
"CURRENT_PWD": "Mevcut Şifre",
@ -151,8 +157,10 @@
"SAVE_SUCCESS": "Kullanıcı şifresi başarıyla değiştirildi.",
"PASS_TIPS": "1 büyük harf, 1 küçük harf ve 1 sayı ile 8-128 karakter"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "Kullanıcı Profili",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "Şifreyi Değiştir",
"ABOUT": "Hakkında",
"LOGOUT": ıkış"
@ -773,7 +781,7 @@
"ARTIFACT_TOOTIP": "Click to view this OCI index's artifact list",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "İndirmeler",
"PULL_COMMAND": "İndirme Komutu",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "İndirme Zamanı",
"PUSH_TIME": "Yükleme Zamanı",
"IMMUTABLE": "Immutable",
@ -1146,8 +1154,7 @@
"TOOLTIP": "Command references for pushing an artifact to this project.",
"TAG_COMMAND": "Bu proje için bir imaj etiketleyin:",
"PUSH_COMMAND": "Bu projeye bir imaj gönder:",
"COPY_ERROR": "Kopyalama başarısız oldu, lütfen komut referanslarını el ile kopyalamayı deneyin.",
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
"COPY_ERROR": "Kopyalama başarısız oldu, lütfen komut referanslarını el ile kopyalamayı deneyin."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",

View File

@ -142,6 +142,12 @@
"CONFIRM_TITLE_CLI_GENERATE": "您确定需要重新生成cli secret吗?",
"CONFIRM_BODY_CLI_GENERATE": "如果您重新生成cli secret那么旧的cli secret将会被弃用"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"CHANGE_PWD": {
"TITLE": "修改密码",
"CURRENT_PWD": "当前密码",
@ -152,6 +158,7 @@
},
"ACCOUNT_SETTINGS": {
"PROFILE": "用户设置",
"PREFERENCES": "Preferences",
"CHANGE_PWD": "修改密码",
"ABOUT": "关于",
"LOGOUT": "退出"
@ -772,7 +779,7 @@
"ARTIFACT_TOOTIP": "点击查看此 OCI 索引的 Artifact 列表",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "下载数",
"PULL_COMMAND": "拉取命令",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "拉取时间",
"PUSH_TIME": "推送时间",
"IMMUTABLE": "不可变的",
@ -1147,8 +1154,7 @@
"TOOLTIP": "推送一个 artifact 到当前项目的参考命令。",
"TAG_COMMAND": "在项目中标记镜像:",
"PUSH_COMMAND": "推送镜像到当前项目:",
"COPY_ERROR": "拷贝失败,请尝试手动拷贝参考命令。",
"COPY_PULL_COMMAND": "复制拉取命令"
"COPY_ERROR": "拷贝失败,请尝试手动拷贝参考命令。"
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",

View File

@ -150,9 +150,16 @@
"SAVE_SUCCESS": "成功更改使用者密碼。",
"PASS_TIPS": "密碼長度需介於 8 到 128 個字元之間,且至少包含一個大寫字母、小寫字母或數字。"
},
"CHANGE_PREF": {
"TITLE": "Preferences",
"LANGUAGE": "Language",
"DATE_TIME_FORMAT": "Date/Time Format",
"PULL_CMD_PREFIX": "Pull Command Prefix"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "使用者設定",
"CHANGE_PWD": "修改密碼",
"PREFERENCES": "Preferences",
"ABOUT": "關於",
"LOGOUT": "登出"
},
@ -772,7 +779,7 @@
"ARTIFACT_TOOTIP": "點選此圖示進入引用的 Artifact 列表",
"ARTIFACTS_COUNT": "Artifact 數量",
"PULL_COUNT": "下載數",
"PULL_COMMAND": "Pull 命令",
"COPY_SUCCESS": "{{ param }} copied to Clipboard",
"PULL_TIME": "拉取時間",
"PUSH_TIME": "推送時間",
"IMMUTABLE": "不可變的",
@ -1144,8 +1151,7 @@
"TOOLTIP": "推送映像擋至此專案的參考命令。",
"TAG_COMMAND": "為此專案標記映像檔:",
"PUSH_COMMAND": "將映像檔推送至此專案:",
"COPY_ERROR": "複製失敗,請嘗試手動複製參考命令。",
"COPY_PULL_COMMAND": "複製拉取命令"
"COPY_ERROR": "複製失敗,請嘗試手動複製參考命令。"
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "篩選 Artifact(s)",