mgr/dashboard: customizable log-in page text/banner

Fixes:https://tracker.ceph.com/issues/55231
Signed-off-by: Sarthak0702 <sarthak.dev.0702@gmail.com>
This commit is contained in:
Sarthak0702 2022-04-14 15:47:21 +05:30
parent a31813d251
commit 9f8bcd764e
12 changed files with 193 additions and 9 deletions

View File

@ -14,6 +14,7 @@ import cherrypy
from cherrypy.lib.static import serve_file
from .. import mgr
from ..services.custom_banner import get_login_banner_mgr
from . import BaseController, Endpoint, Proxy, Router, UIRouter
logger = logging.getLogger("controllers.home")
@ -139,3 +140,10 @@ class LangsController(BaseController, LanguageMixin):
@Endpoint('GET')
def __call__(self):
return list(self.LANGUAGES)
@UIRouter("/login", secure=False)
class LoginController(BaseController):
@Endpoint('GET', 'custom_banner')
def __call__(self):
return get_login_banner_mgr()

View File

@ -9,14 +9,14 @@
</header>
<section>
<div class="container">
<div class="row full-height vertical-align">
<div class="col-sm-12 col-md-6 d-sm-block">
<div class="row full-height">
<div class="col-sm-12 col-md-6 d-sm-block login-form">
<router-outlet></router-outlet>
</div>
<div class="col-sm-12 col-md-6 d-sm-block">
<div class="col-sm-12 col-md-6 d-sm-block branding-info">
<img src="assets/Ceph_Ceph_Logo_with_text_white.svg"
alt="Ceph"
class="img-fluid">
class="img-fluid pb-3">
<ul class="list-inline">
<li class="list-inline-item p-3"
*ngFor="let docItem of docItems">
@ -26,6 +26,7 @@
i18n-docText></cd-doc>
</li>
</ul>
<cd-custom-login-banner></cd-custom-login-banner>
</div>
</div>
</div>

View File

@ -31,7 +31,7 @@
}
.list-inline {
margin-bottom: 20%;
margin-bottom: 0;
margin-left: 20%;
}
@ -42,4 +42,20 @@
color: vv.$fg-hover-color-over-dark-bg;
}
}
@media screen and (min-width: vv.$screen-sm-min) {
.login-form,
.branding-info {
padding-top: 30vh;
}
}
@media screen and (max-width: vv.$screen-sm-max) {
.login-form {
padding-top: 10vh;
}
.branding-info {
padding-top: 0;
}
}
}

View File

@ -0,0 +1,35 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { configureTestBed } from '~/testing/unit-test-helper';
import { CustomLoginBannerService } from './custom-login-banner.service';
describe('CustomLoginBannerService', () => {
let service: CustomLoginBannerService;
let httpTesting: HttpTestingController;
const baseUiURL = 'ui-api/login/custom_banner';
configureTestBed({
providers: [CustomLoginBannerService],
imports: [HttpClientTestingModule]
});
beforeEach(() => {
service = TestBed.inject(CustomLoginBannerService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTesting.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should call getBannerText', () => {
service.getBannerText().subscribe();
const req = httpTesting.expectOne(baseUiURL);
expect(req.request.method).toBe('GET');
});
});

View File

@ -0,0 +1,15 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CustomLoginBannerService {
baseUiURL = 'ui-api/login/custom_banner';
constructor(private http: HttpClient) {}
getBannerText() {
return this.http.get<string>(this.baseUiURL);
}
}

View File

@ -25,6 +25,7 @@ import { ConfigOptionComponent } from './config-option/config-option.component';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import { Copy2ClipboardButtonComponent } from './copy2clipboard-button/copy2clipboard-button.component';
import { CriticalConfirmationModalComponent } from './critical-confirmation-modal/critical-confirmation-modal.component';
import { CustomLoginBannerComponent } from './custom-login-banner/custom-login-banner.component';
import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component';
import { DocComponent } from './doc/doc.component';
import { DownloadButtonComponent } from './download-button/download-button.component';
@ -95,7 +96,8 @@ import { WizardComponent } from './wizard/wizard.component';
DownloadButtonComponent,
FormButtonPanelComponent,
MotdComponent,
WizardComponent
WizardComponent,
CustomLoginBannerComponent
],
providers: [],
exports: [
@ -123,7 +125,8 @@ import { WizardComponent } from './wizard/wizard.component';
DownloadButtonComponent,
FormButtonPanelComponent,
MotdComponent,
WizardComponent
WizardComponent,
CustomLoginBannerComponent
]
})
export class ComponentsModule {}

View File

@ -0,0 +1,2 @@
<p class="login-text"
*ngIf="bannerText$ | async as bannerText">{{ bannerText }}</p>

View File

@ -0,0 +1,5 @@
.login-text {
font-weight: bold;
margin: 0;
padding: 12px 20% 12px 12px;
}

View File

@ -0,0 +1,25 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { configureTestBed } from '~/testing/unit-test-helper';
import { CustomLoginBannerComponent } from './custom-login-banner.component';
describe('CustomLoginBannerComponent', () => {
let component: CustomLoginBannerComponent;
let fixture: ComponentFixture<CustomLoginBannerComponent>;
configureTestBed({
declarations: [CustomLoginBannerComponent],
imports: [HttpClientTestingModule]
});
beforeEach(() => {
fixture = TestBed.createComponent(CustomLoginBannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import _ from 'lodash';
import { Observable } from 'rxjs';
import { CustomLoginBannerService } from '~/app/shared/api/custom-login-banner.service';
@Component({
selector: 'cd-custom-login-banner',
templateUrl: './custom-login-banner.component.html',
styleUrls: ['./custom-login-banner.component.scss']
})
export class CustomLoginBannerComponent implements OnInit {
bannerText$: Observable<string>;
constructor(private customLoginBannerService: CustomLoginBannerService) {}
ngOnInit(): void {
this.bannerText$ = this.customLoginBannerService.getBannerText();
}
}

View File

@ -13,14 +13,17 @@ import threading
import time
from typing import TYPE_CHECKING, Optional
from .services.custom_banner import get_login_banner_mgr, \
set_login_banner_mgr, unset_login_banner_mgr
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
from mgr_module import CLIWriteCommand, HandleCommandResult, MgrModule, \
MgrStandbyModule, NotifyType, Option, _get_localized_key
from mgr_module import CLIReadCommand, CLIWriteCommand, HandleCommandResult, \
MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
from mgr_util import ServerConfigException, build_url, \
create_self_signed_cert, get_default_addr, verify_tls_files
@ -417,6 +420,30 @@ class Module(MgrModule, CherryPyConfig):
return 0, 'RGW credentials configured', ''
@CLIWriteCommand("dashboard set-login-banner")
def set_login_banner(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
item_label = 'login banner file'
if inbuf is None:
return HandleCommandResult(
-errno.EINVAL,
stderr=f'Please specify the {item_label} with "-i" option'
)
set_login_banner_mgr(inbuf, mgr_id)
return HandleCommandResult(stdout=f'{item_label} added')
@CLIReadCommand("dashboard get-login-banner")
def get_login_banner(self):
banner_text = get_login_banner_mgr()
if banner_text is None:
return HandleCommandResult(stdout='No login banner set')
else:
return HandleCommandResult(stdout=banner_text)
@CLIWriteCommand("dashboard unset-login-banner")
def unset_login_banner(self):
unset_login_banner_mgr()
return HandleCommandResult(stdout='Login banner removed')
def handle_command(self, inbuf, cmd):
# pylint: disable=too-many-return-statements
res = handle_option_command(cmd, inbuf)

View File

@ -0,0 +1,27 @@
import logging
from typing import Optional
from mgr_module import _get_localized_key
from .. import mgr
logger = logging.getLogger(__name__)
def set_login_banner_mgr(inbuf: str, mgr_id: Optional[str] = None):
item_key = 'custom_login_banner'
if mgr_id is not None:
mgr.set_store(_get_localized_key(mgr_id, item_key), inbuf)
else:
mgr.set_store(item_key, inbuf)
def get_login_banner_mgr():
banner_text = mgr.get_store('custom_login_banner')
logger.info('Reading custom login banner: %s', banner_text)
return banner_text
def unset_login_banner_mgr():
mgr.set_store('custom_login_banner', None)
logger.info('Removing custom login banner')