Skip to content

Commit

Permalink
feat: organization branding (#258)
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Miglinci <pmig@glasskube.com>
Signed-off-by: christophenne <christoph.enne@glasskube.eu>
Co-authored-by: christophenne <christoph.enne@glasskube.eu>
Co-authored-by: Jakob Steiner <kosmoz@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 15, 2025
1 parent e43d3a3 commit 489217f
Show file tree
Hide file tree
Showing 23 changed files with 1,982 additions and 25 deletions.
16 changes: 9 additions & 7 deletions frontend/cloud-ui/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import {provideHttpClient, withInterceptors} from '@angular/common/http';
import {
ApplicationConfig,
ErrorHandler,
inject,
provideAppInitializer,
provideZoneChangeDetection,
} from '@angular/core';
import {ApplicationConfig, ErrorHandler, provideZoneChangeDetection} from '@angular/core';
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
import {provideRouter, Router} from '@angular/router';
import {routes} from './app.routes';
import {tokenInterceptor} from './services/auth.service';
import {errorToastInterceptor} from './services/error-toast.interceptor';
import {provideToastr} from 'ngx-toastr';
import * as Sentry from '@sentry/angular';
import {MARKED_OPTIONS, provideMarkdown} from 'ngx-markdown';
import {markedOptionsFactory} from './services/markdown-options.factory';

export const appConfig: ApplicationConfig = {
providers: [
Expand All @@ -25,5 +21,11 @@ export const appConfig: ApplicationConfig = {
provideHttpClient(withInterceptors([tokenInterceptor, errorToastInterceptor])),
provideAnimationsAsync(),
provideToastr(),
provideMarkdown({
markedOptions: {
provide: MARKED_OPTIONS,
useFactory: markedOptionsFactory,
},
}),
],
};
14 changes: 13 additions & 1 deletion frontend/cloud-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {SettingsService} from './services/settings.service';
import {ToastService} from './services/toast.service';
import {UserRole} from './types/user-account';
import {VerifyComponent} from './verify/verify.component';
import {OrganizationBrandingComponent} from './organization-branding/organization-branding.component';

const emailVerificationGuard: CanActivateFn = async () => {
const auth = inject(AuthService);
Expand Down Expand Up @@ -85,7 +86,7 @@ const baseRoteRedirectGuard: CanActivateFn = () => {
const router = inject(Router);
switch (auth.getClaims().role) {
case 'customer':
return router.createUrlTree(['/deployments']);
return router.createUrlTree(['/home']);
case 'vendor':
return router.createUrlTree(['/dashboard']);
default:
Expand Down Expand Up @@ -125,6 +126,11 @@ export const routes: Routes = [
.DashboardPlaceholderComponent,
canActivate: [requiredRoleGuard('vendor')],
},
{
path: 'home',
loadComponent: async () => (await import('./components/home/home.component')).HomeComponent,
canActivate: [requiredRoleGuard('customer')],
},
{path: 'applications', component: ApplicationsPageComponent, canActivate: [requiredRoleGuard('vendor')]},
{path: 'deployments', component: DeploymentsPageComponent},
{
Expand All @@ -139,6 +145,12 @@ export const routes: Routes = [
data: {userRole: 'vendor'},
canActivate: [requiredRoleGuard('vendor')],
},
{
path: 'branding',
component: OrganizationBrandingComponent,
data: {userRole: 'vendor'},
canActivate: [requiredRoleGuard('vendor')],
},
],
},
],
Expand Down
11 changes: 11 additions & 0 deletions frontend/cloud-ui/src/app/components/home/home.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="p-4 sm:ml-64 bg-gray-50 dark:bg-gray-900">
<div class="p-4 text-gray-900 dark:text-white">
@if (brandingDescription$ | async; as description) {
<div [innerHTML]="description | markdown | async"></div>
} @else {
<div class="text-gray-500 dark:text-gray-400 italic">
Homepage not yet configured by vendor in branding settings
</div>
}
</div>
</div>
17 changes: 17 additions & 0 deletions frontend/cloud-ui/src/app/components/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {AsyncPipe} from '@angular/common';
import {Component, inject} from '@angular/core';
import {map, Observable} from 'rxjs';
import {OrganizationBrandingService} from '../../services/organization-branding.service';
import {MarkdownPipe} from 'ngx-markdown';

@Component({
selector: 'app-home',
imports: [AsyncPipe, MarkdownPipe],
templateUrl: './home.component.html',
})
export class HomeComponent {
private readonly organizationBranding = inject(OrganizationBrandingService);
readonly brandingDescription$: Observable<string | undefined> = this.organizationBranding
.get()
.pipe(map((b) => b.description));
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
<fa-icon [icon]="faBarsStaggered" size="xl" class="h-6 w-6"></fa-icon>
</button>
<a routerLink="/" class="flex ms-2 md:me-24">
<img src="/glasskube-logo.svg" class="h-8 me-3" alt="Glasskube Logo" />
<img [src]="logoUrl" class="h-8 me-3" alt="Glasskube Logo" />
<h1 class="font-display self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">
Glasskube
@if (role === 'vendor') {
Glasskube
}
<small class="ms-2 font-semibold text-gray-500 dark:text-gray-400">
@if (role === 'vendor') {
Vendor Platform
} @else {
Customer Portal
{{ customerSubtitle }}
}
</small>
</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {AuthService} from '../../services/auth.service';
import {SidebarService} from '../../services/sidebar.service';
import {ColorSchemeSwitcherComponent} from '../color-scheme-switcher/color-scheme-switcher.component';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faBars, faBarsStaggered} from '@fortawesome/free-solid-svg-icons';
import {faBarsStaggered} from '@fortawesome/free-solid-svg-icons';
import {UserRole} from '../../types/user-account';
import {RouterLink} from '@angular/router';
import {OrganizationBrandingService} from '../../services/organization-branding.service';

@Component({
selector: 'app-nav-bar',
Expand All @@ -20,11 +21,14 @@ import {RouterLink} from '@angular/router';
export class NavBarComponent implements OnInit {
private readonly auth = inject(AuthService);
public readonly sidebar = inject(SidebarService);
private readonly organizationBranding = inject(OrganizationBrandingService);
showDropdown = false;
email?: string;
name?: string;
role?: UserRole;
imageUrl?: string;
logoUrl = '/glasskube-logo.svg';
customerSubtitle = 'Customer Portal';

protected readonly faBarsStaggered = faBarsStaggered;

Expand All @@ -34,12 +38,29 @@ export class NavBarComponent implements OnInit {
this.email = email;
this.name = name;
this.role = role;
this.initBranding();
this.imageUrl = `https://www.gravatar.com/avatar/${await digestMessage(email)}`;
} catch (e) {
console.error(e);
}
}

private async initBranding() {
if (this.auth.hasRole('customer')) {
try {
const branding = await lastValueFrom(this.organizationBranding.get());
if (branding.logo) {
this.logoUrl = `data:${branding.logoContentType};base64,${branding.logo}`;
}
if (branding.title) {
this.customerSubtitle = branding.title;
}
} catch (e) {
console.error(e);
}
}
}

public async logout() {
await lastValueFrom(this.auth.logout());
// This is necessary to flush the caching crud services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,6 @@
>
</a>
</li>
<li>
<a class="flex items-center p-2 text-gray-400 rounded-lg dark:text-gray-400">
<fa-icon [icon]="faPalette" size="lg" class="pl-0.5 w-6 h-6 text-gray-300 dark:text-gray-600"></fa-icon>
<span class="ms-3">Branding</span>
<span
class="ml-2 bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-300"
>Pro</span
>
</a>
</li>
<hr class="dark:border-gray-600" />
<li>
<a
Expand All @@ -104,6 +94,17 @@
<span class="ms-3">Manage Users</span>
</a>
</li>
<li>
<a
class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group"
routerLink="/branding">
<fa-icon
[icon]="faPalette"
size="lg"
class="pl-0.5 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></fa-icon>
<span class="ms-3">Branding</span>
</a>
</li>
<li>
<a class="flex items-center p-2 text-gray-400 rounded-lg dark:text-gray-400">
<fa-icon [icon]="faGear" size="lg" class="pl-0.5 w-6 h-6 text-gray-300 dark:text-gray-600"></fa-icon>
Expand All @@ -116,6 +117,18 @@
</li>
</ul>
<ul class="space-y-2 font-medium flex-1" *appRequiredRole="'customer'">
<li>
<a
(click)="sidebar.hide()"
routerLink="/home"
class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<fa-icon
[icon]="faHome"
size="lg"
class="pl-0.5 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></fa-icon>
<span class="ms-3">Home</span>
</a>
</li>
<li>
<a
(click)="sidebar.hide()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
faCheckDouble,
faDashboard,
faGear,
faHome,
faKey,
faLightbulb,
faPalette,
Expand Down Expand Up @@ -50,4 +51,5 @@ export class SideBarComponent {
}

protected readonly faArrowRightLong = faArrowRightLong;
protected readonly faHome = faHome;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<section class="bg-gray-50 dark:bg-gray-900 p-3 sm:p-5 antialiased sm:ml-64">
<div class="mx-auto max-w-screen-md px-4 lg:px-12">
<h1 class="mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 md:text-4xl dark:text-white">
Branding
</h1>
<p class="text-gray-500 dark:text-gray-400">
Customize your organization's branding by uploading a logo and setting a title and description for your
organization. Your customers will see this information when they access their customer Portal.
</p>

<form [formGroup]="form" (ngSubmit)="save()">
<div class="grid gap-4 mb-4 sm:mb-6 mt-6">
<div class="space-y-4">
<h2 class="text-xl font-bold dark:text-white">Portal Header</h2>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="file_input">
Company Logo
</label>
<div>
@if (logoSrc | async; as logo) {
<img class="h-8 mb-4" [src]="logo" alt="Logo" />
<button
type="button"
(click)="deleteLogo()"
class="px-3 py-2 text-xs font-medium text-gray-900 bg-white border border-gray-200 rounded-lg focus:outline-none hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
Delete
</button>
} @else {
<input
accept="image/svg+xml,image/png,image/jpeg,image/gif"
(change)="onLogoChange($event)"
class="w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400"
aria-describedby="file_input_help"
id="file_input"
type="file" />
}
</div>

<p class="mt-1 mb-3 text-xs font-normal text-gray-500 dark:text-gray-400" id="file_input_help">
SVG, PNG, JPG or GIF (recommended height 32px). If not set, the Glasskube Logo will be shown.
</p>
<div class="flex items-center space-x-2.5"></div>
</div>

<div>
<label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
<input
formControlName="title"
type="text"
name="title"
id="title"
class="bg-gray-50 border border-gray-300 text-sm text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Customer Portal" />
<p class="mt-1 mb-3 text-xs font-normal text-gray-500 dark:text-gray-400">
The title will be shown in the header next to the logo.
</p>
</div>

<h2 class="text-xl font-bold dark:text-white mt-8">Welcome Page</h2>

<div>
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Markdown Description</label
>
<div
class="mb-4 w-full bg-gray-100 rounded-lg border border-gray-200 dark:bg-gray-600 dark:border-gray-600">
<div class="py-2 px-4 bg-gray-50 rounded-b-lg dark:bg-gray-700">
<textarea
formControlName="description"
id="description"
rows="8"
class="block px-0 w-full text-sm text-gray-800 bg-gray-50 border-0 dark:bg-gray-700 focus:ring-0 dark:text-white dark:placeholder-gray-400"
placeholder="# Welcome"></textarea>
</div>
</div>
</div>
<div class="mt-8 flex justify-center w-full pb-4 space-x-4 sm:mt-0">
<button
type="submit"
class="text-white w-full inline-flex items-center justify-center bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
<fa-icon [icon]="faFloppyDisk" size="lg" class="h-4 w-4 mr-2 -ml-0.5 mb-1"></fa-icon>
Save
</button>
</div>
</div>
</div>
</form>
</div>
</section>
Loading

0 comments on commit 489217f

Please sign in to comment.