diff --git a/src/assets/styles/common-base-screen.scss b/src/assets/styles/common-base-screen.scss
index 7486d77..9353ed2 100644
--- a/src/assets/styles/common-base-screen.scss
+++ b/src/assets/styles/common-base-screen.scss
@@ -1,74 +1,74 @@
@use 'common-mixins';
.top-bar {
- height: var(--iqser-top-bar-height);
- width: 100vw;
+ height: var(--iqser-top-bar-height);
+ width: 100vw;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 24px;
+ border-bottom: 1px solid var(--iqser-separator);
+ box-sizing: border-box;
+ background-color: var(--iqser-background);
+
+ > *:not(:last-child) {
+ margin-right: 50px;
+ }
+
+ .menu {
display: flex;
- justify-content: space-between;
align-items: center;
- padding: 0 24px;
- border-bottom: 1px solid var(--iqser-separator);
- box-sizing: border-box;
- background-color: var(--iqser-background);
+
+ &.right {
+ justify-content: flex-end;
+ }
+ }
+
+ .menu-placeholder {
+ display: flex;
+ flex: 2;
+ }
+
+ .buttons {
+ display: flex;
+ margin-right: 8px;
> *:not(:last-child) {
- margin-right: 50px;
+ margin-right: 14px;
}
+ }
+}
- .logo {
- @include common-mixins.clear-a;
- display: flex;
- align-items: center;
- }
+.logo {
+ @include common-mixins.clear-a;
+ display: flex;
+ align-items: center;
+}
- .app-name {
- font-family: var(--iqser-app-name-font-family);
- font-size: var(--iqser-app-name-font-size);
- color: var(--iqser-app-name-color);
- font-weight: 800;
- white-space: nowrap;
- }
-
- .menu {
- display: flex;
- align-items: center;
-
- &.right {
- justify-content: flex-end;
- }
- }
-
- .menu-placeholder {
- display: flex;
- flex: 2;
- }
-
- .buttons {
- display: flex;
- margin-right: 8px;
-
- > *:not(:last-child) {
- margin-right: 14px;
- }
- }
+.app-name {
+ font-family: var(--iqser-app-name-font-family);
+ font-size: var(--iqser-app-name-font-size);
+ color: var(--iqser-app-name-color);
+ font-weight: 800;
+ white-space: nowrap;
}
.dev-mode {
- background-color: var(--iqser-primary);
- color: var(--iqser-white);
- font-size: 22px;
- line-height: 16px;
- text-align: center;
- position: fixed;
- top: 0;
- z-index: 100;
- right: 0;
- height: var(--iqser-top-bar-height);
- word-break: break-all;
- display: flex;
- justify-content: center;
- align-items: center;
- font-family: monospace;
- width: 24px;
- font-weight: bold;
+ background-color: var(--iqser-primary);
+ color: var(--iqser-white);
+ font-size: 22px;
+ line-height: 16px;
+ text-align: center;
+ position: fixed;
+ top: 0;
+ z-index: 100;
+ right: 0;
+ height: var(--iqser-top-bar-height);
+ word-break: break-all;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: monospace;
+ width: 24px;
+ font-weight: bold;
}
diff --git a/src/lib/permissions/services/permissions-guard.service.ts b/src/lib/permissions/services/permissions-guard.service.ts
index 51875a3..e023583 100644
--- a/src/lib/permissions/services/permissions-guard.service.ts
+++ b/src/lib/permissions/services/permissions-guard.service.ts
@@ -85,10 +85,10 @@ export class IqserPermissionsGuard implements CanActivate, CanMatch, CanActivate
}
if (Array.isArray(_redirectTo)) {
- return this._router.navigate([this._tenantsService.currentTenant, ..._redirectTo]);
+ return this._router.navigate([this._tenantsService.activeTenantId, ..._redirectTo]);
}
- return this._router.navigate([`${this._tenantsService.currentTenant}${_redirectTo}`]);
+ return this._router.navigate([`${this._tenantsService.activeTenantId}${_redirectTo}`]);
}
#checkRedirect(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
diff --git a/src/lib/shared/logo/logo.component.ts b/src/lib/shared/logo/logo.component.ts
index 9de965e..61871d0 100644
--- a/src/lib/shared/logo/logo.component.ts
+++ b/src/lib/shared/logo/logo.component.ts
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
@Component({
- selector: 'iqser-logo [icon]',
+ selector: 'iqser-logo',
template: ` `,
styles: [
`
@@ -21,5 +21,5 @@ import { MatIconModule } from '@angular/material/icon';
imports: [MatIconModule],
})
export class LogoComponent {
- @Input() icon!: string;
+ @Input({ required: true }) icon!: string;
}
diff --git a/src/lib/shared/spacer/spacer.component.scss b/src/lib/shared/spacer/spacer.component.scss
new file mode 100644
index 0000000..d1f864b
--- /dev/null
+++ b/src/lib/shared/spacer/spacer.component.scss
@@ -0,0 +1,15 @@
+:host {
+ --height: 20px;
+
+ div {
+ height: #{var(--height)};
+
+ @media screen and (max-width: 1600px) {
+ height: calc(#{var(--height)} * 0.9);
+ }
+
+ @media screen and (max-width: 1200px) {
+ height: calc(#{var(--height)} * 0.85);
+ }
+ }
+}
diff --git a/src/lib/shared/spacer/spacer.component.ts b/src/lib/shared/spacer/spacer.component.ts
new file mode 100644
index 0000000..becc99a
--- /dev/null
+++ b/src/lib/shared/spacer/spacer.component.ts
@@ -0,0 +1,16 @@
+import { Component, HostBinding, Input } from '@angular/core';
+
+@Component({
+ selector: 'iqser-spacer [height]',
+ template: '
',
+ styleUrls: ['./spacer.component.scss'],
+ standalone: true,
+})
+export class SpacerComponent {
+ @Input({ required: true }) height!: number;
+
+ @HostBinding('style.--height')
+ get heightStyle() {
+ return this.height + 'px';
+ }
+}
diff --git a/src/lib/tenants/index.ts b/src/lib/tenants/index.ts
index 4c6d75d..dfb088e 100644
--- a/src/lib/tenants/index.ts
+++ b/src/lib/tenants/index.ts
@@ -4,5 +4,5 @@ export * from './guards/if-not-logged-in.guard';
export * from './services';
export * from './tenant.pipe';
export * from './tenants.module';
-export * from './tenant-resolve/tenant-resolve.component';
+export * from './tenant-select/tenant-select.component';
export * from './services/keycloak-status.service';
diff --git a/src/lib/tenants/services/keycloak-status.service.ts b/src/lib/tenants/services/keycloak-status.service.ts
index 1b49cf2..75c14e2 100644
--- a/src/lib/tenants/services/keycloak-status.service.ts
+++ b/src/lib/tenants/services/keycloak-status.service.ts
@@ -30,7 +30,7 @@ export class KeycloakStatusService {
}
createLoginUrl() {
- const currentTenant = this.#tenantsService.currentTenant;
+ const currentTenant = this.#tenantsService.activeTenantId;
if (currentTenant && window.location.href.indexOf('/' + currentTenant) > 0) {
return window.location.href;
}
diff --git a/src/lib/tenants/services/tenant-id-interceptor.ts b/src/lib/tenants/services/tenant-id-interceptor.ts
index ab5b242..b0f87af 100644
--- a/src/lib/tenants/services/tenant-id-interceptor.ts
+++ b/src/lib/tenants/services/tenant-id-interceptor.ts
@@ -8,9 +8,9 @@ export class TenantIdInterceptor implements HttpInterceptor {
protected readonly _tenantsService = inject(TenantsService);
intercept(req: HttpRequest, next: HttpHandler): Observable> {
- if (this._tenantsService.currentTenant) {
+ if (this._tenantsService.activeTenantId) {
const updatedRequest = req.clone({
- setHeaders: { 'X-TENANT-ID': this._tenantsService.currentTenant },
+ setHeaders: { 'X-TENANT-ID': this._tenantsService.activeTenantId },
});
return next.handle(updatedRequest);
diff --git a/src/lib/tenants/services/tenants.service.ts b/src/lib/tenants/services/tenants.service.ts
index b95c6a0..bda98ba 100644
--- a/src/lib/tenants/services/tenants.service.ts
+++ b/src/lib/tenants/services/tenants.service.ts
@@ -2,7 +2,7 @@ import { computed, inject, Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { NGXLogger } from 'ngx-logger';
-import { BASE_HREF } from '../../utils';
+import { BASE_HREF, List } from '../../utils';
import { Router } from '@angular/router';
export interface IBaseTenant {
@@ -11,6 +11,22 @@ export interface IBaseTenant {
readonly guid: string;
}
+export interface IStoredTenantId {
+ readonly tenantId: string;
+ readonly email: string;
+}
+
+export type StoredTenantIds = List;
+
+export interface IStoredTenant {
+ readonly tenant: IBaseTenant;
+ readonly email: string;
+}
+
+export type StoredTenants = IStoredTenant[];
+
+const STORED_TENANTS_KEY = 'red-stored-tenants';
+
@Injectable({ providedIn: 'root' })
export class TenantsService {
readonly tenants = signal(undefined);
@@ -31,8 +47,11 @@ export class TenantsService {
key: localStorage.key.bind(localStorage),
};
readonly #activeTenantId = signal('');
+ readonly activeTenant = computed(() => {
+ return this.tenants()?.find(t => t.tenantId === this.#activeTenantId());
+ });
- get currentTenant() {
+ get activeTenantId() {
return this.#activeTenantId();
}
@@ -82,11 +101,61 @@ export class TenantsService {
}
this.#mutateStorage(tenantId);
- this.#setCurrentTenantId(tenantId);
+ this.#setActiveTenantId(tenantId);
return true;
}
- #setCurrentTenantId(tenantId: string) {
+ storeTenant(email: string) {
+ if (!email) {
+ this.#logger.warn('[TENANTS] Email is null, skip storing');
+ return;
+ }
+
+ const storedTenants = this.getStoredTenants();
+ const activeTenant = this.activeTenant();
+ const existing = storedTenants.find(s => s.email === email && s.tenant.tenantId === activeTenant?.tenantId);
+ if (existing) {
+ this.#logger.info('[TENANTS] Stored tenant exists: ', storedTenants);
+ return;
+ }
+
+ if (!activeTenant) {
+ this.#logger.error('[TENANTS] Active tenant is null when storing tenants');
+ return;
+ }
+
+ storedTenants.push({ tenant: activeTenant, email: email });
+ const serializableTenants = storedTenants.map(s => ({
+ tenantId: s.tenant.tenantId,
+ email: s.email,
+ }));
+ this.#storageReference.setItem(STORED_TENANTS_KEY, JSON.stringify(serializableTenants));
+ this.#logger.info('[TENANTS] Stored tenants: ', storedTenants);
+ }
+
+ getStoredTenants(): StoredTenants {
+ const rawStoredTenants = this.#storageReference.getItem(STORED_TENANTS_KEY);
+ const stored = JSON.parse(rawStoredTenants ?? '[]') as StoredTenantIds;
+ const tenants = this.tenants();
+ if (!tenants) {
+ this.#logger.error('[TENANTS] Tenants data is null when retrieving stored tenants');
+ return [];
+ }
+
+ const trueStored: StoredTenants = [];
+ for (const s of stored) {
+ const tenant = tenants.find(t => t.tenantId === s.tenantId);
+ if (!tenant) {
+ this.#logger.warn(`[TENANTS] Stored tenant with id ${tenant} not found, skipping`);
+ continue;
+ }
+ trueStored.push({ tenant, email: s.email });
+ }
+
+ return trueStored;
+ }
+
+ #setActiveTenantId(tenantId: string) {
this.#logger.info('[TENANTS] Set current tenant id: ', tenantId);
this.#activeTenantId.set(tenantId);
}
diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html b/src/lib/tenants/tenant-resolve/tenant-resolve.component.html
deleted file mode 100644
index 9a9580f..0000000
--- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss b/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss
deleted file mode 100644
index 4838f02..0000000
--- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.tenant-section {
- display: flex;
- align-items: flex-start;
- justify-content: center;
- width: 100vw;
- height: 100vh;
-
- margin-top: 32px;
-
- > form > * {
- padding: 8px;
- }
-}
diff --git a/src/lib/tenants/tenant-select/tenant-select.component.html b/src/lib/tenants/tenant-select/tenant-select.component.html
new file mode 100644
index 0000000..35161cb
--- /dev/null
+++ b/src/lib/tenants/tenant-select/tenant-select.component.html
@@ -0,0 +1,55 @@
+
diff --git a/src/lib/tenants/tenant-select/tenant-select.component.scss b/src/lib/tenants/tenant-select/tenant-select.component.scss
new file mode 100644
index 0000000..5389816
--- /dev/null
+++ b/src/lib/tenants/tenant-select/tenant-select.component.scss
@@ -0,0 +1,65 @@
+.tenant-section {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ width: 100vw;
+ height: 100vh;
+
+ margin-top: 32px;
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.stored-tenant-card {
+ width: 450px;
+ height: 90px;
+}
+
+.card {
+ display: flex;
+ align-items: center;
+ border-radius: 10px;
+}
+
+.card-icon {
+ margin-right: 20px;
+ margin-left: 20px;
+}
+
+.upside-down {
+ transform: rotate(180deg);
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
+
+.pb-30 {
+ padding-bottom: 30px;
+}
+
+.input-card {
+ width: 600px;
+ height: 65px;
+}
+
+.url {
+ margin-left: 20px;
+ margin-right: 10px;
+}
+
+form {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+}
+
+.subheading {
+ font-size: 17px;
+}
diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts b/src/lib/tenants/tenant-select/tenant-select.component.ts
similarity index 55%
rename from src/lib/tenants/tenant-resolve/tenant-resolve.component.ts
rename to src/lib/tenants/tenant-select/tenant-select.component.ts
index 99000d1..c3802a2 100644
--- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts
+++ b/src/lib/tenants/tenant-select/tenant-select.component.ts
@@ -4,26 +4,27 @@ import { FormBuilder, Validators } from '@angular/forms';
import { TenantsService } from '../services';
import { IconButtonTypes } from '../../buttons';
import { LoadingService } from '../../loading';
+import { Title } from '@angular/platform-browser';
+import { getConfig } from '../../services';
+import { BASE_HREF } from '../../utils';
@Component({
- templateUrl: './tenant-resolve.component.html',
- styleUrls: ['./tenant-resolve.component.scss'],
+ templateUrl: './tenant-select.component.html',
+ styleUrls: ['./tenant-select.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class TenantResolveComponent {
+export class TenantSelectComponent {
readonly loadingService = inject(LoadingService);
readonly iconButtonTypes = IconButtonTypes;
- readonly form;
+ readonly form = inject(FormBuilder).group({
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ tenantId: ['', Validators.required],
+ });
+ readonly url = getConfig().OAUTH_URL.replace('https://', '').replace('/auth', '') + inject(BASE_HREF) + '/';
protected readonly _tenantsService = inject(TenantsService);
+ protected readonly storedTenants = this._tenantsService.getStoredTenants();
+ protected readonly titleService = inject(Title);
readonly #router = inject(Router);
- readonly #formBuilder = inject(FormBuilder);
-
- constructor() {
- this.form = this.#formBuilder.group({
- // eslint-disable-next-line @typescript-eslint/unbound-method
- tenantId: ['', Validators.required],
- });
- }
updateTenantSelection() {
const tenant = this.form.controls.tenantId.value;
diff --git a/src/lib/tenants/tenant.pipe.ts b/src/lib/tenants/tenant.pipe.ts
index 5a13295..8b581b7 100644
--- a/src/lib/tenants/tenant.pipe.ts
+++ b/src/lib/tenants/tenant.pipe.ts
@@ -14,6 +14,6 @@ export class TenantPipe implements PipeTransform {
return undefined;
}
const _value = Array.isArray(value) ? value.join('/') : value;
- return '/' + this.#tenantsService.currentTenant + _value;
+ return '/' + this.#tenantsService.activeTenantId + _value;
}
}
diff --git a/src/lib/tenants/tenants.module.ts b/src/lib/tenants/tenants.module.ts
index 0bfe705..2a5aa88 100644
--- a/src/lib/tenants/tenants.module.ts
+++ b/src/lib/tenants/tenants.module.ts
@@ -3,14 +3,20 @@ import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
import { CircleButtonComponent, IconButtonComponent } from '../buttons';
-import { TenantResolveComponent } from './tenant-resolve/tenant-resolve.component';
+import { TenantSelectComponent } from './tenant-select/tenant-select.component';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { ReactiveFormsModule } from '@angular/forms';
import { TenantIdInterceptor, TenantIdResponseInterceptor, TenantsService } from './services';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { MatCardModule } from '@angular/material/card';
+import { LogoComponent } from '../shared/logo/logo.component';
+import { SpacerComponent } from '../shared/spacer/spacer.component';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterLink } from '@angular/router';
-const components = [TenantResolveComponent];
+const components = [TenantSelectComponent];
@NgModule({
declarations: [...components],
@@ -23,6 +29,12 @@ const components = [TenantResolveComponent];
MatSelectModule,
IconButtonComponent,
ReactiveFormsModule,
+ MatCardModule,
+ LogoComponent,
+ SpacerComponent,
+ MatIconModule,
+ MatButtonModule,
+ RouterLink,
],
exports: [...components],
})
diff --git a/src/lib/users/guards/iqser-auth-guard.service.ts b/src/lib/users/guards/iqser-auth-guard.service.ts
index 413557b..fa3971f 100644
--- a/src/lib/users/guards/iqser-auth-guard.service.ts
+++ b/src/lib/users/guards/iqser-auth-guard.service.ts
@@ -35,7 +35,7 @@ export class IqserAuthGuard extends KeycloakAuthGuard {
const user = await this._userService.loadCurrentUser();
if (user?.hasAnyRole && route.routeConfig?.path === 'auth-error') {
- await this._router.navigate([`/${this._tenantsService.currentTenant}/main`]);
+ await this._router.navigate([`/${this._tenantsService.activeTenantId}/main`]);
return false;
}
diff --git a/src/lib/users/guards/iqser-role-guard.service.ts b/src/lib/users/guards/iqser-role-guard.service.ts
index 810b630..0c071bc 100644
--- a/src/lib/users/guards/iqser-role-guard.service.ts
+++ b/src/lib/users/guards/iqser-role-guard.service.ts
@@ -14,7 +14,7 @@ export class IqserRoleGuard implements CanActivate {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const currentUser = this._userService.currentUser;
if (!currentUser || !currentUser.hasAnyRole) {
- await this._router.navigate([`/${this._tenantsService.currentTenant}/auth-error`]);
+ await this._router.navigate([`/${this._tenantsService.activeTenantId}/auth-error`]);
this._loadingService.stop();
return false;
}