Add costomized banner message UI (#18827)

1.Fixes #18719
2.Add Banner Message item to configuration
3.Add banner_message property to systeminfo API

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2023-07-03 15:58:37 +08:00 committed by GitHub
parent 8fe561865d
commit ef96c729c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 551 additions and 12 deletions

View File

@ -116,4 +116,4 @@ This project uses open source components which have additional licensing terms.
## Fossa Status ## Fossa Status
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor?ref=badge_large) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor?ref=badge_large)

View File

@ -7682,6 +7682,12 @@ definitions:
GeneralInfo: GeneralInfo:
type: object type: object
properties: properties:
banner_message:
type: string
x-nullable: true
x-omitempty: true
description: The banner message for the UI. It is the stringified result of the banner message object.
example: "{\"closable\":true,\"message\":\"your banner message content\",\"type\":\"warning\",\"fromDate\":\"06/19/2023\",\"toDate\":\"06/21/2023\"}"
current_time: current_time:
type: string type: string
format: date-time format: date-time
@ -8820,6 +8826,9 @@ definitions:
session_timeout: session_timeout:
$ref: '#/definitions/IntegerConfigItem' $ref: '#/definitions/IntegerConfigItem'
description: The session timeout in minutes description: The session timeout in minutes
banner_message:
$ref: '#/definitions/StringConfigItem'
description: The banner message for the UI.It is the stringified result of the banner message object
Configurations: Configurations:
type: object type: object
properties: properties:
@ -9088,6 +9097,11 @@ definitions:
description: Whether or not to skip update pull time for scanner description: Whether or not to skip update pull time for scanner
x-omitempty: true x-omitempty: true
x-isnullable: true x-isnullable: true
banner_message:
type: string
description: The banner message for the UI.It is the stringified result of the banner message object
x-omitempty: true
x-isnullable: true
StringConfigItem: StringConfigItem:
type: object type: object
properties: properties:

View File

@ -219,6 +219,9 @@ const (
// SessionTimeout defines the web session timeout // SessionTimeout defines the web session timeout
SessionTimeout = "session_timeout" SessionTimeout = "session_timeout"
// Customized banner message
BannerMessage = "banner_message"
// UIMaxLengthLimitedOfNumber is the max length that UI limited for type number // UIMaxLengthLimitedOfNumber is the max length that UI limited for type number
UIMaxLengthLimitedOfNumber = 10 UIMaxLengthLimitedOfNumber = 10
// ExecutionStatusRefreshIntervalSeconds is the interval seconds for refreshing execution status // ExecutionStatusRefreshIntervalSeconds is the interval seconds for refreshing execution status

View File

@ -47,6 +47,7 @@ type Data struct {
PrimaryAuthMode bool PrimaryAuthMode bool
SelfRegistration bool SelfRegistration bool
HarborVersion string HarborVersion string
BannerMessage string
AuthProxySettings *models.HTTPAuthProxy AuthProxySettings *models.HTTPAuthProxy
Protected *protectedData Protected *protectedData
} }
@ -90,11 +91,18 @@ func (c *controller) GetInfo(ctx context.Context, opt Options) (*Data, error) {
logger.Errorf("Error occurred getting config: %v", err) logger.Errorf("Error occurred getting config: %v", err)
return nil, err return nil, err
} }
mgr := config.GetCfgManager(ctx)
err = mgr.Load(ctx)
if err != nil {
logger.Errorf("Error occurred loading config: %v", err)
return nil, err
}
res := &Data{ res := &Data{
AuthMode: utils.SafeCastString(cfg[common.AUTHMode]), AuthMode: utils.SafeCastString(cfg[common.AUTHMode]),
PrimaryAuthMode: utils.SafeCastBool(cfg[common.PrimaryAuthMode]), PrimaryAuthMode: utils.SafeCastBool(cfg[common.PrimaryAuthMode]),
SelfRegistration: utils.SafeCastBool(cfg[common.SelfRegistration]), SelfRegistration: utils.SafeCastBool(cfg[common.SelfRegistration]),
HarborVersion: fmt.Sprintf("%s-%s", version.ReleaseVersion, version.GitCommit), HarborVersion: fmt.Sprintf("%s-%s", version.ReleaseVersion, version.GitCommit),
BannerMessage: utils.SafeCastString(mgr.Get(ctx, common.BannerMessage).GetString()),
} }
if res.AuthMode == common.HTTPAuth { if res.AuthMode == common.HTTPAuth {
if s, err := config.HTTPAuthProxySetting(ctx); err == nil { if s, err := config.HTTPAuthProxySetting(ctx); err == nil {

View File

@ -31,6 +31,7 @@ func (s *sysInfoCtlTestSuite) SetupTest() {
common.RegistryStorageProviderName: "filesystem", common.RegistryStorageProviderName: "filesystem",
common.ReadOnly: false, common.ReadOnly: false,
common.NotificationEnable: false, common.NotificationEnable: false,
common.BannerMessage: "{\"closable\":false,\"message\":\"Just for test\",\"type\":\" error\"}",
} }
config.InitWithSettings(conf) config.InitWithSettings(conf)
@ -58,6 +59,7 @@ func (s *sysInfoCtlTestSuite) TestGetInfo() {
AuthMode: "db_auth", AuthMode: "db_auth",
HarborVersion: "test-fakeid", HarborVersion: "test-fakeid",
SelfRegistration: true, SelfRegistration: true,
BannerMessage: "{\"closable\":false,\"message\":\"Just for test\",\"type\":\" error\"}",
}, },
}, },
{ {
@ -66,6 +68,7 @@ func (s *sysInfoCtlTestSuite) TestGetInfo() {
AuthMode: "db_auth", AuthMode: "db_auth",
HarborVersion: "test-fakeid", HarborVersion: "test-fakeid",
SelfRegistration: true, SelfRegistration: true,
BannerMessage: "{\"closable\":false,\"message\":\"Just for test\",\"type\":\" error\"}",
Protected: &protectedData{ Protected: &protectedData{
RegistryURL: "test.goharbor.io", RegistryURL: "test.goharbor.io",
ExtURL: "https://test.goharbor.io", ExtURL: "https://test.goharbor.io",

View File

@ -189,5 +189,7 @@ var (
{Name: common.SessionTimeout, Scope: UserScope, Group: BasicGroup, EnvKey: "SESSION_TIMEOUT", DefaultValue: "60", ItemType: &Int64Type{}, Editable: true, Description: `The session timeout in minutes`}, {Name: common.SessionTimeout, Scope: UserScope, Group: BasicGroup, EnvKey: "SESSION_TIMEOUT", DefaultValue: "60", ItemType: &Int64Type{}, Editable: true, Description: `The session timeout in minutes`},
{Name: common.ExecutionStatusRefreshIntervalSeconds, Scope: SystemScope, Group: BasicGroup, EnvKey: "EXECUTION_STATUS_REFRESH_INTERVAL_SECONDS", DefaultValue: "30", ItemType: &Int64Type{}, Editable: false, Description: `The interval seconds to refresh the execution status`}, {Name: common.ExecutionStatusRefreshIntervalSeconds, Scope: SystemScope, Group: BasicGroup, EnvKey: "EXECUTION_STATUS_REFRESH_INTERVAL_SECONDS", DefaultValue: "30", ItemType: &Int64Type{}, Editable: false, Description: `The interval seconds to refresh the execution status`},
{Name: common.BannerMessage, Scope: UserScope, Group: BasicGroup, EnvKey: "BANNER_MESSAGE", DefaultValue: "", ItemType: &StringType{}, Editable: true, Description: `The customized banner message for the UI`},
} }
) )

View File

@ -255,3 +255,8 @@ func ScannerSkipUpdatePullTime(ctx context.Context) bool {
log.Infof("skip_update_pull_time:%v", DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool()) log.Infof("skip_update_pull_time:%v", DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool())
return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool() return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool()
} }
// BannerMessage returns the customized banner message
func BannerMessage(ctx context.Context) string {
return DefaultMgr().Get(ctx, common.BannerMessage).GetString()
}

View File

@ -1,4 +1,13 @@
<clr-alerts> <clr-alerts>
<clr-alert
*ngIf="hasValidBannerMessage() && isLogin()"
[clrAlertType]="getBannerMessageType()"
[clrAlertAppLevel]="true"
[clrAlertClosable]="getBannerMessageClosable()">
<clr-alert-item>
<span class="alert-text">{{ getBannerMessage() }}</span>
</clr-alert-item>
</clr-alert>
<clr-alert <clr-alert
*ngIf="showReadOnly && isLogin()" *ngIf="showReadOnly && isLogin()"
[clrAlertType]="'warning'" [clrAlertType]="'warning'"

View File

@ -15,6 +15,10 @@ import { MessageService } from '../../../shared/components/global-message/messag
import { Message } from '../../../shared/components/global-message/message'; import { Message } from '../../../shared/components/global-message/message';
import { JobServiceDashboardHealthCheckService } from '../../left-side-nav/job-service-dashboard/job-service-dashboard-health-check.service'; import { JobServiceDashboardHealthCheckService } from '../../left-side-nav/job-service-dashboard/job-service-dashboard-health-check.service';
import { AppConfigService } from '../../../services/app-config.service'; import { AppConfigService } from '../../../services/app-config.service';
import {
BannerMessage,
BannerMessageType,
} from '../../left-side-nav/config/config';
const HAS_SHOWED_SCANNER_INFO: string = 'hasShowScannerInfo'; const HAS_SHOWED_SCANNER_INFO: string = 'hasShowScannerInfo';
const YES: string = 'yes'; const YES: string = 'yes';
@Component({ @Component({
@ -185,4 +189,79 @@ export class AppLevelAlertsComponent implements OnInit, OnDestroy {
isLogin(): boolean { isLogin(): boolean {
return !!this.session.getCurrentUser(); return !!this.session.getCurrentUser();
} }
hasValidBannerMessage(): boolean {
if (
this.appConfigService.getConfig()?.banner_message &&
this.appConfigService.getConfig()?.current_time
) {
const current = new Date(
this.appConfigService.getConfig()?.current_time
);
const bm = JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage;
if (bm?.fromDate && bm?.toDate) {
return (
new Date(current) <= new Date(bm.toDate) &&
new Date(current) >= new Date(bm.fromDate)
);
}
if (bm?.fromDate && !bm?.toDate) {
return new Date(current) >= new Date(bm.fromDate);
}
if (!bm?.fromDate && bm?.toDate) {
return new Date(current) <= new Date(bm.toDate);
}
}
return false;
}
getBannerMessage() {
if (
this.appConfigService.getConfig()?.banner_message &&
(
JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage
)?.message
) {
return (
JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage
)?.message;
}
return null;
}
getBannerMessageType() {
if (
this.appConfigService.getConfig()?.banner_message &&
(
JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage
)?.type
) {
return (
JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage
)?.type;
}
return BannerMessageType.WARNING;
}
getBannerMessageClosable(): boolean {
if (this.appConfigService.getConfig()?.banner_message) {
return (
JSON.parse(
this.appConfigService.getConfig()?.banner_message
) as BannerMessage
)?.closable;
}
return true;
}
} }

View File

@ -11,6 +11,10 @@ import { clone } from '../../../shared/units/utils';
import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import {
EventService,
HarborEvent,
} from '../../../services/event-service/event.service';
const fakePass = 'aWpLOSYkIzJTTU4wMDkx'; const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
@ -24,7 +28,8 @@ export class ConfigService {
constructor( constructor(
private confirmService: ConfirmationDialogService, private confirmService: ConfirmationDialogService,
private configureService: ConfigureService, private configureService: ConfigureService,
private msgHandler: MessageHandlerService private msgHandler: MessageHandlerService,
private event: EventService
) { ) {
this._confirmSub = this.confirmService.confirmationConfirm$.subscribe( this._confirmSub = this.confirmService.confirmationConfirm$.subscribe(
confirmation => { confirmation => {
@ -66,6 +71,7 @@ export class ConfigService {
.subscribe( .subscribe(
res => { res => {
this._currentConfig = res as Configuration; this._currentConfig = res as Configuration;
this.event.publish(HarborEvent.REFRESH_BANNER_MESSAGE);
// Add password fields // Add password fields
this._currentConfig.email_password = new StringValueItem( this._currentConfig.email_password = new StringValueItem(
fakePass, fakePass,

View File

@ -114,6 +114,7 @@ export class Configuration {
skip_audit_log_database: BoolValueItem; skip_audit_log_database: BoolValueItem;
session_timeout: NumberValueItem; session_timeout: NumberValueItem;
scanner_skip_update_pulltime: BoolValueItem; scanner_skip_update_pulltime: BoolValueItem;
banner_message: StringValueItem;
public constructor() { public constructor() {
this.auth_mode = new StringValueItem('db_auth', true); this.auth_mode = new StringValueItem('db_auth', true);
this.primary_auth_mode = new BoolValueItem(false, true); this.primary_auth_mode = new BoolValueItem(false, true);
@ -190,6 +191,10 @@ export class Configuration {
this.skip_audit_log_database = new BoolValueItem(false, true); this.skip_audit_log_database = new BoolValueItem(false, true);
this.session_timeout = new NumberValueItem(60, true); this.session_timeout = new NumberValueItem(60, true);
this.scanner_skip_update_pulltime = new BoolValueItem(false, true); this.scanner_skip_update_pulltime = new BoolValueItem(false, true);
this.banner_message = new StringValueItem(
JSON.stringify(new BannerMessage()),
true
);
} }
} }
@ -208,3 +213,28 @@ export enum Triggers {
SCHEDULE = 'Schedule', SCHEDULE = 'Schedule',
EVENT = 'Event', EVENT = 'Event',
} }
export class BannerMessage {
message: string;
closable: boolean;
type: string;
fromDate: Date;
toDate: Date;
constructor() {
this.closable = false;
}
}
export enum BannerMessageType {
SUCCESS = 'success',
INFO = 'info',
WARNING = 'warning',
ERROR = 'danger',
}
export const BannerMessageI18nMap = {
[BannerMessageType.SUCCESS]: 'BANNER_MESSAGE.SUCCESS',
[BannerMessageType.INFO]: 'BANNER_MESSAGE.INFO',
[BannerMessageType.WARNING]: 'BANNER_MESSAGE.WARNING',
[BannerMessageType.ERROR]: 'BANNER_MESSAGE.DANGER',
};

View File

@ -346,6 +346,127 @@
" /> " />
</clr-checkbox-wrapper> </clr-checkbox-wrapper>
</clr-checkbox-container> </clr-checkbox-container>
<div class="clr-form-control">
<label class="clr-control-label">{{
'BANNER_MESSAGE.BANNER_MESSAGE' | translate
}}</label>
<div class="clr-control-container flex-baseline">
<div class="clr-textarea-wrapper">
<textarea
id="banner-message"
placeholder="{{
'BANNER_MESSAGE.ENTER_MESSAGE' | translate
}}"
autocomplete="off"
class="clr-textarea"
[(ngModel)]="messageText"
[ngModelOptions]="{ standalone: true }"
[disabled]="
!currentConfig.banner_message.editable
"></textarea>
</div>
<div class="message-type">
<div
class="clr-select-wrapper"
[ngClass]="{
'clr-form-control-disable':
!currentConfig.banner_message.editable ||
!messageText
}">
<label class="message-label">{{
'BANNER_MESSAGE.MESSAGE_TYPE' | translate
}}</label>
<select
id="banner-message-type"
class="clr-select message-select"
[(ngModel)]="messageType"
[ngModelOptions]="{ standalone: true }"
[disabled]="
!currentConfig.banner_message.editable ||
!messageText
">
<option
*ngFor="let t of bannerMessageTypes"
value="{{ t }}">
{{ translateMessageType(t) | translate }}
</option>
</select>
</div>
</div>
<div
class="clr-checkbox-wrapper ml-1"
[ngClass]="{
'clr-form-control-disable':
!currentConfig.banner_message.editable ||
!messageText
}">
<input
class="clr-checkbox-inline"
type="checkbox"
[(ngModel)]="messageClosable"
[ngModelOptions]="{ standalone: true }"
[disabled]="
!currentConfig.banner_message.editable ||
!messageText
"
id="banner-message-closable" />
<label
class="clr-control-label"
for="banner-message-closable"
>{{ 'BANNER_MESSAGE.CLOSABLE' | translate }}</label
>
</div>
</div>
</div>
<div class="clr-form-control">
<label class="clr-control-label"></label>
<div class="clr-control-container flex-baseline">
<div class="clr-textarea-wrapper duration">
<label>{{ 'REPLICATION.DURATION' | translate }}</label>
</div>
<div
class="clr-input-wrapper flex message-type"
[ngClass]="{
'clr-form-control-disable':
!currentConfig.banner_message.editable ||
!messageText
}">
<label>{{ 'BANNER_MESSAGE.FROM' | translate }}</label>
<input
class="date"
type="date"
id="from"
clrDate
[(ngModel)]="messageFromDate"
[ngModelOptions]="{ standalone: true }"
[disabled]="
!currentConfig.banner_message.editable ||
!messageText
" />
</div>
<div
class="clr-checkbox-wrapper flex ml-1"
[ngClass]="{
'clr-form-control-disable':
!currentConfig.banner_message.editable ||
!messageText
}">
<label>{{ 'BANNER_MESSAGE.TO' | translate }}</label>
<input
class="date"
clrDate
type="date"
id="to"
[(ngModel)]="messageToDate"
[disabled]="
!currentConfig.banner_message.editable ||
!messageText
"
[ngModelOptions]="{ standalone: true }" />
</div>
</div>
</div>
</section> </section>
</form> </form>
<div> <div>

View File

@ -1,3 +1,6 @@
$input-width: 12rem;
.subtitle { .subtitle {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
@ -148,11 +151,11 @@
} }
.clr-input { .clr-input {
width: 12rem; width: $input-width;
} }
.pro-creation { .pro-creation {
width: 12rem; width: $input-width;
} }
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
@ -165,3 +168,47 @@ input::-webkit-inner-spin-button {
input[type=number] { input[type=number] {
appearance: textfield; appearance: textfield;
} }
.clr-textarea {
max-width: none;
width: $input-width;
}
.flex-baseline {
display: flex;
align-items: baseline;
}
$message-type-width: 12rem;
.message-type {
margin-left: 2rem;
width: $message-type-width;
}
.message-label {
margin-right: 0.25rem;
}
.duration {
width: $input-width;
display: flex;
justify-content: right;
}
.flex {
display: flex;
}
:host::ng-deep clr-date-container{
margin-top: 0;
margin-left: 0.5rem;
}
.message-select {
width: 6.75rem;
}
.date {
width: 6rem;
}

View File

@ -1,6 +1,11 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { Configuration } from '../config'; import {
BannerMessage,
BannerMessageI18nMap,
BannerMessageType,
Configuration,
} from '../config';
import { import {
CURRENT_BASE_HREF, CURRENT_BASE_HREF,
getChanges, getChanges,
@ -10,13 +15,19 @@ import { ConfigService } from '../config.service';
import { AppConfigService } from '../../../../services/app-config.service'; import { AppConfigService } from '../../../../services/app-config.service';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'system-settings', selector: 'system-settings',
templateUrl: './system-settings.component.html', templateUrl: './system-settings.component.html',
styleUrls: ['./system-settings.component.scss'], styleUrls: ['./system-settings.component.scss'],
}) })
export class SystemSettingsComponent implements OnInit { export class SystemSettingsComponent implements OnInit, OnDestroy {
bannerMessageTypes: string[] = Object.values(BannerMessageType);
onGoing = false; onGoing = false;
downloadLink: string; downloadLink: string;
get currentConfig(): Configuration { get currentConfig(): Configuration {
@ -26,18 +37,90 @@ export class SystemSettingsComponent implements OnInit {
set currentConfig(cfg: Configuration) { set currentConfig(cfg: Configuration) {
this.conf.setConfig(cfg); this.conf.setConfig(cfg);
} }
messageText: string;
messageType: string;
messageClosable: boolean;
messageFromDate: Date;
messageToDate: Date;
// the copy of bannerMessage
messageTextCopy: string;
messageTypeCopy: string;
messageClosableCopy: boolean;
messageFromDateCopy: Date;
messageToDateCopy: Date;
bannerRefreshSub: Subscription;
@ViewChild('systemConfigFrom') systemSettingsForm: NgForm; @ViewChild('systemConfigFrom') systemSettingsForm: NgForm;
constructor( constructor(
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private errorHandler: MessageHandlerService, private errorHandler: MessageHandlerService,
private conf: ConfigService private conf: ConfigService,
private event: EventService
) { ) {
this.downloadLink = CURRENT_BASE_HREF + '/systeminfo/getcert'; this.downloadLink = CURRENT_BASE_HREF + '/systeminfo/getcert';
} }
ngOnInit() { ngOnInit() {
this.conf.resetConfig(); this.conf.resetConfig();
if (!this.bannerRefreshSub) {
this.bannerRefreshSub = this.event.subscribe(
HarborEvent.REFRESH_BANNER_MESSAGE,
() => {
this.setValueForBannerMessage();
}
);
}
if (this.currentConfig.banner_message) {
this.setValueForBannerMessage();
}
}
ngOnDestroy() {
if (this.bannerRefreshSub) {
this.bannerRefreshSub.unsubscribe();
this.bannerRefreshSub = null;
}
}
setValueForBannerMessage() {
if (this.currentConfig.banner_message.value) {
this.messageText = (
JSON.parse(
this.currentConfig.banner_message.value
) as BannerMessage
).message;
this.messageType = (
JSON.parse(
this.currentConfig.banner_message.value
) as BannerMessage
).type;
this.messageClosable = (
JSON.parse(
this.currentConfig.banner_message.value
) as BannerMessage
).closable;
this.messageFromDate = (
JSON.parse(
this.currentConfig.banner_message.value
) as BannerMessage
).fromDate;
this.messageToDate = (
JSON.parse(
this.currentConfig.banner_message.value
) as BannerMessage
).toDate;
} else {
this.messageText = null;
this.messageType = BannerMessageType.WARNING;
this.messageClosable = false;
}
this.messageTextCopy = this.messageText;
this.messageTypeCopy = this.messageType;
this.messageClosableCopy = this.messageClosable;
this.messageFromDateCopy = this.messageFromDate;
this.messageToDateCopy = this.messageToDate;
} }
get editable(): boolean { get editable(): boolean {
@ -69,7 +152,17 @@ export class SystemSettingsComponent implements OnInit {
} }
public hasChanges(): boolean { public hasChanges(): boolean {
return !isEmpty(this.getChanges()); return !isEmpty(this.getChanges()) || this.hasBannerMessageChanged();
}
hasBannerMessageChanged() {
return (
this.messageTextCopy != this.messageText ||
this.messageTypeCopy != this.messageType ||
this.messageClosableCopy != this.messageClosable ||
this.messageFromDateCopy != this.messageFromDate ||
this.messageToDateCopy != this.messageToDate
);
} }
public getChanges() { public getChanges() {
@ -96,7 +189,8 @@ export class SystemSettingsComponent implements OnInit {
prop === 'audit_log_forward_endpoint' || prop === 'audit_log_forward_endpoint' ||
prop === 'skip_audit_log_database' || prop === 'skip_audit_log_database' ||
prop === 'session_timeout' || prop === 'session_timeout' ||
prop === 'scanner_skip_update_pulltime' prop === 'scanner_skip_update_pulltime' ||
prop === 'banner_message'
) { ) {
changes[prop] = allChanges[prop]; changes[prop] = allChanges[prop];
} }
@ -128,6 +222,19 @@ export class SystemSettingsComponent implements OnInit {
*/ */
public save(): void { public save(): void {
let changes = this.getChanges(); let changes = this.getChanges();
if (this.hasBannerMessageChanged()) {
const bm = new BannerMessage();
bm.message = this.messageText;
bm.type = this.messageType;
bm.closable = this.messageClosable;
bm.fromDate = this.messageFromDate;
bm.toDate = this.messageToDate;
if (bm.message) {
changes['banner_message'] = JSON.stringify(bm);
} else {
changes['banner_message'] = '';
}
}
if (!isEmpty(changes)) { if (!isEmpty(changes)) {
this.onGoing = true; this.onGoing = true;
this.conf this.conf
@ -184,4 +291,8 @@ export class SystemSettingsComponent implements OnInit {
this.currentConfig.skip_audit_log_database.value = false; this.currentConfig.skip_audit_log_database.value = false;
} }
} }
translateMessageType(type: string): string {
return BannerMessageI18nMap[type] || type;
}
} }

View File

@ -29,6 +29,8 @@ export class AppConfig {
registry_storage_provider_name: string; registry_storage_provider_name: string;
read_only: boolean; read_only: boolean;
show_popular_repo: boolean; show_popular_repo: boolean;
banner_message: string;
current_time: string;
constructor() { constructor() {
// Set default value // Set default value
@ -49,5 +51,6 @@ export class AppConfig {
this.registry_storage_provider_name = ''; this.registry_storage_provider_name = '';
this.read_only = false; this.read_only = false;
this.show_popular_repo = false; this.show_popular_repo = false;
this.banner_message = '';
} }
} }

View File

@ -81,4 +81,5 @@ export enum HarborEvent {
REFRESH_EXPORT_JOBS = 'refreshExportJobs', REFRESH_EXPORT_JOBS = 'refreshExportJobs',
DELETE_ACCESSORY = 'deleteAccessory', DELETE_ACCESSORY = 'deleteAccessory',
COPY_DIGEST = 'copyDigest', COPY_DIGEST = 'copyDigest',
REFRESH_BANNER_MESSAGE = 'refreshBannerMessage',
} }

View File

@ -1860,5 +1860,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -804,7 +804,7 @@
"LABEL": "Labels", "LABEL": "Labels",
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"REPO_READ_ONLY": "Repository Read Only", "REPO_READ_ONLY": "Repository Read Only",
"WEBHOOK_NOTIFICATION_ENABLED": "Webhooks enabled", "WEBHOOK_NOTIFICATION_ENABLED": "Webhooks Enabled",
"SYSTEM": "System Settings", "SYSTEM": "System Settings",
"PROJECT_QUOTAS": "Project Quotas", "PROJECT_QUOTAS": "Project Quotas",
"VULNERABILITY": "Vulnerability", "VULNERABILITY": "Vulnerability",
@ -834,7 +834,7 @@
"ROOT_CERT_LINK": "Download", "ROOT_CERT_LINK": "Download",
"REGISTRY_CERTIFICATE": "Registry certificate", "REGISTRY_CERTIFICATE": "Registry certificate",
"NO_CHANGE": "Save abort because nothing changed", "NO_CHANGE": "Save abort because nothing changed",
"SKIP_SCANNER_PULL_TIME": "Retain image \"last pull time\" on scanning", "SKIP_SCANNER_PULL_TIME": "Retain Image \"last pull time\" On Scanning",
"TOOLTIP": { "TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "Enable sign up.", "SELF_REGISTRATION_ENABLE": "Enable sign up.",
"SELF_REGISTRATION_DISABLE": "Deactivate sign up.", "SELF_REGISTRATION_DISABLE": "Deactivate sign up.",
@ -1861,5 +1861,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -1857,5 +1857,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -1827,5 +1827,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Sélectionner le mois, le mois courant est {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Sélectionner le mois, le mois courant est {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Sélectionner l'année, l'année courante est {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Sélectionner l'année, l'année courante est {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Sélectionné" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Sélectionné"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -1857,5 +1857,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -1860,5 +1860,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "Select month, the current month is {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "Select year, the current year is {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - Selected"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -1857,5 +1857,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "选择月, 当前月是 {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "选择月, 当前月是 {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "选择年, 当前年是 {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "选择年, 当前年是 {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "已选择 - {FULL_DATE}" "DATE_PICKER_SELECTED_LABEL": "已选择 - {FULL_DATE}"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "横幅消息",
"MESSAGE_TYPE": "消息类型",
"CLOSABLE": "可关闭",
"FROM": "从",
"TO": "至",
"SUCCESS": "成功",
"INFO": "信息",
"WARNING": "警告",
"DANGER": "危险",
"ENTER_MESSAGE": "请输入消息内容"
} }
} }

View File

@ -1849,5 +1849,17 @@
"DATE_PICKER_SELECT_MONTH_TEXT": "選擇月份,目前月份為 {CALENDAR_MONTH}", "DATE_PICKER_SELECT_MONTH_TEXT": "選擇月份,目前月份為 {CALENDAR_MONTH}",
"DATE_PICKER_SELECT_YEAR_TEXT": "選擇年份,目前年份為 {CALENDAR_YEAR}", "DATE_PICKER_SELECT_YEAR_TEXT": "選擇年份,目前年份為 {CALENDAR_YEAR}",
"DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - 已選擇" "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - 已選擇"
},
"BANNER_MESSAGE": {
"BANNER_MESSAGE": "Banner Message",
"MESSAGE_TYPE": "Message type",
"CLOSABLE": "Closable",
"FROM": "From",
"TO": "To",
"SUCCESS": "Success",
"INFO": "Info",
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
} }
} }

View File

@ -86,6 +86,7 @@ func (s *sysInfoAPI) convertInfo(d *si.Data) *models.GeneralInfo {
PrimaryAuthMode: &d.PrimaryAuthMode, PrimaryAuthMode: &d.PrimaryAuthMode,
SelfRegistration: &d.SelfRegistration, SelfRegistration: &d.SelfRegistration,
HarborVersion: &d.HarborVersion, HarborVersion: &d.HarborVersion,
BannerMessage: &d.BannerMessage,
} }
if d.AuthProxySettings != nil { if d.AuthProxySettings != nil {
res.AuthproxySettings = &models.AuthproxySetting{ res.AuthproxySettings = &models.AuthproxySetting{