Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: organization branding #258

Merged
merged 26 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c3f62e9
feat: organization branding
pmig Jan 12, 2025
2d43b7f
Merge branch 'main' into pmig/org-branding
christophenne Jan 13, 2025
0a6f196
chore: update migration version
christophenne Jan 13, 2025
8cb72b9
chore: remove duplicate sidebar entry
christophenne Jan 13, 2025
dacd38a
chore: form validations
christophenne Jan 13, 2025
60cf27d
chore: switch to 404 and add clientside error handling
christophenne Jan 13, 2025
87ffd61
chore: use pointer and path value
christophenne Jan 13, 2025
d02245a
fix: no more expression checked errors and logo
christophenne Jan 13, 2025
df89f87
fix: make logo deletable
christophenne Jan 13, 2025
64b4a0d
chore: guard for branding routes
christophenne Jan 13, 2025
cf7d421
chore: lint
christophenne Jan 13, 2025
c22e3fe
Merge branch 'main' into pmig/org-branding
christophenne Jan 14, 2025
74212d0
chore: everything optional
christophenne Jan 14, 2025
064a2b1
feat: show branding in customer portal
christophenne Jan 14, 2025
556303e
feat: markdown rendering
christophenne Jan 14, 2025
1f6cb47
fix: update immediately after creation not working because of missing id
christophenne Jan 14, 2025
3625625
Merge branch 'main' into pmig/org-branding
christophenne Jan 15, 2025
9445eaa
chore: apply suggestions from code review
christophenne Jan 15, 2025
e4bf1c7
Merge remote-tracking branch 'origin/pmig/org-branding' into pmig/org…
christophenne Jan 15, 2025
c249c81
chore: incorporate review comments
christophenne Jan 15, 2025
66c682c
chore: style links in markdown
christophenne Jan 15, 2025
bcabd73
chore: change form placeholders
christophenne Jan 15, 2025
9556f66
Update frontend/cloud-ui/src/app/organization-branding/organization-b…
pmig Jan 15, 2025
29b2e01
refactor: directly create base64 image
pmig Jan 15, 2025
70d609f
Merge branch 'main' into pmig/org-branding
pmig Jan 15, 2025
feadc68
chore: default redirection to home page and placeholder text
pmig Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading