Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 54 additions & 47 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ import {
computed,
DestroyRef,
inject,
Injector,
OnInit,
signal,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SwUpdate } from '@angular/service-worker';
import {
get,
invalidateToken,
isMock,
onlineState,
} from '@placeos/ts-client';
import { get, invalidateToken, isMock, onlineState } from '@placeos/ts-client';

import {
ActivatedRoute,
Expand All @@ -28,14 +23,12 @@ import { setNotifyOutlet } from './common/notifications';
import { PlaceSettings, setLoadingMessage, setupPlace } from './common/placeos';
import { SettingsService } from './common/settings.service';
import { signalFromClient, waitForSignalValue } from './common/signals';
import { syncUploadToken } from './common/uploads';
import { currentUser } from './common/user-state';
import { BackofficeUsersService } from './users/users.service';

import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PlaceTenant } from './admin/staff-api.component';
import { tenantExpiryBanner } from './admin/staff-api.utilities';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatTooltip } from '@angular/material/tooltip';
import type { PlaceTenant } from './admin/staff-api.component';
import {
localeFromUrl,
LocaleService,
Expand Down Expand Up @@ -67,12 +60,16 @@ function browserOnline() {
template: `
<div class="flex h-full w-full flex-col overflow-hidden">
@if (!loading()) {
<global-banner />
@defer (on idle) {
<global-banner />
}
<div class="relative h-1/2 w-full flex-1">
<router-outlet />
</div>
@if (!simple()) {
<app-upload-list />
@defer (on idle) {
<app-upload-list />
}
}
} @else {
<div
Expand All @@ -99,30 +96,35 @@ function browserOnline() {
</div>
}
@if (update_available() && !loading()) {
<section
role="status"
aria-live="polite"
class="border-info/30 bg-base-100 text-base-content fixed right-3 bottom-3 z-100 w-88 max-w-[calc(100vw-2rem)] rounded-md border p-3 shadow-lg"
>
<div class="flex items-center gap-2">
<div class="min-w-0 flex-1">
<div class="text-sm font-medium">Update available</div>
<p class="mt-0.5 text-xs opacity-70">
Refresh to load the latest Backoffice version.
</p>
<!-- Deferred so the tooltip and overlay code stay out of the initial bundle -->
@defer {
<section
role="status"
aria-live="polite"
class="border-info/30 bg-base-100 text-base-content fixed right-3 bottom-3 z-100 w-88 max-w-[calc(100vw-2rem)] rounded-md border p-3 shadow-lg"
>
<div class="flex items-center gap-2">
<div class="min-w-0 flex-1">
<div class="text-sm font-medium">
Update available
</div>
<p class="mt-0.5 text-xs opacity-70">
Refresh to load the latest Backoffice version.
</p>
</div>
<button
icon
default
type="button"
(click)="refreshApplication()"
matTooltip="Refresh"
matTooltipPosition="left"
>
<icon>refresh</icon>
</button>
</div>
<button
icon
default
type="button"
(click)="refreshApplication()"
matTooltip="Refresh"
matTooltipPosition="left"
>
<icon>refresh</icon>
</button>
</div>
</section>
</section>
}
}
`,
styles: [
Expand All @@ -140,17 +142,17 @@ function browserOnline() {
GlobalBannerComponent,
RouterOutlet,
UploadListComponent,
MatProgressBarModule,
MatProgressBar,
GlobalLoadingComponent,
IconComponent,
MatTooltipModule,
MatTooltip,
],
})
export class AppComponent extends AsyncHandler implements OnInit {
private _settings = inject(SettingsService);
private _users = inject(BackofficeUsersService);
private _cache = inject(SwUpdate);
private _snackbar = inject(MatSnackBar);
private _injector = inject(Injector);
private _router = inject(Router);
private _route = inject(ActivatedRoute);
private _locale = inject(LocaleService, { optional: true });
Expand Down Expand Up @@ -186,7 +188,12 @@ export class AppComponent extends AsyncHandler implements OnInit {
localStorage.setItem('BACKOFFICE.locale', locale);
}
});
setNotifyOutlet(this._snackbar);
// Load the snackbar lazily to keep the overlay code out of the initial bundle
setNotifyOutlet(
import('@angular/material/snack-bar').then(({ MatSnackBar }) =>
this._injector.get(MatSnackBar),
),
);
setTranslationService(this._locale);
this.loading.set(true);
setLoadingMessage('Loading application settings...');
Expand Down Expand Up @@ -215,8 +222,6 @@ export class AppComponent extends AsyncHandler implements OnInit {
// TranslatePipe is pure, so load translations before the shell renders
await this._initLocale();
this.loading.set(false);
setLoadingMessage('Initialising upload service...');
this.timeout('init_uploads', () => syncUploadToken());
this._router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.simple.set(this._router.url.includes('mqtt'));
Expand All @@ -240,7 +245,10 @@ export class AppComponent extends AsyncHandler implements OnInit {
/** Show one banner for staff tenants with expiring secrets */
private async _checkTenants() {
if (!currentUser()?.sys_admin) return;
const tenants = await get('/api/staff/v1/tenants').catch(() => []);
const [tenants, { tenantExpiryBanner }] = await Promise.all([
get('/api/staff/v1/tenants').catch(() => []),
import('./admin/staff-api.utilities'),
]);
const banner = tenantExpiryBanner(
Array.isArray(tenants) ? (tenants as PlaceTenant[]) : [],
);
Expand All @@ -249,7 +257,7 @@ export class AppComponent extends AsyncHandler implements OnInit {

/**
* Set the locale from the URL `lang` param, storage or the browser languages.
* Resolves when translations load, or after a timeout.
* Resolves when the fallback and locale translations load, or after a timeout.
*/
private async _initLocale() {
let load: Promise<void> | undefined;
Expand Down Expand Up @@ -284,10 +292,9 @@ export class AppComponent extends AsyncHandler implements OnInit {
} catch {
// Ignore locale parsing errors
}
if (!load) return;
// Do not block start up on a slow or failed locale file
await Promise.race([
load.catch(() => undefined),
Promise.all([this._locale?.ready, load]).catch(() => undefined),
new Promise((resolve) => setTimeout(resolve, LOCALE_TIMEOUT_MS)),
]);
}
Expand Down
14 changes: 10 additions & 4 deletions src/app/common/locale.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Service } from '@angular/core';

import * as DEFAULT_LOCALE from '../../../public/assets/locale/en-AU.json';

import { showMetadata } from '@placeos/ts-client';
import { log } from './general';

Expand Down Expand Up @@ -93,8 +91,16 @@ export class LocaleService {
private _cache_time = 7 * 24 * 60 * 60 * 1000;
private _load_promises: Record<string, Promise<void>> = {};

private _default_mappings: Record<string, string> =
removeNesting(DEFAULT_LOCALE);
private _default_mappings: Record<string, string> = {};
/**
* Resolves when the fallback translations have loaded.
* They load lazily to keep them out of the initial bundle.
*/
public readonly ready: Promise<void> = import(
'../../../public/assets/locale/en-AU.json'
).then(({ default: data }) => {
this._default_mappings = removeNesting(data);
});
private _locale_mappings: Record<string, Record<string, string>> = {};

public locale_folder = 'assets/locale';
Expand Down
35 changes: 31 additions & 4 deletions src/app/common/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { MatSnackBar } from '@angular/material/snack-bar';
import type { MatSnackBar } from '@angular/material/snack-bar';

import { ApplicationIcon } from './types';

let _service: MatSnackBar;
/** Snackbar service, or a promise of it while the snackbar module loads */
let _service: MatSnackBar | Promise<MatSnackBar> | null = null;

export function setNotifyOutlet(snackbar: MatSnackBar) {
/**
* Set the snackbar used for notifications.
* Pass a promise to load the snackbar module lazily.
* Notifications made before it resolves open once it does.
*/
export function setNotifyOutlet(snackbar: MatSnackBar | Promise<MatSnackBar>) {
_service = snackbar;
if (snackbar instanceof Promise) {
snackbar.then((service) => {
if (_service === snackbar) _service = service;
});
}
}

/**
Expand All @@ -29,7 +40,23 @@ export function notify(
if (!_service) {
throw new Error("Snackbar service hasn't been initialised");
}
const snackbar_ref = _service.open(message, action, {
if (_service instanceof Promise) {
_service.then((service) =>
openNotification(service, type, message, action, on_action),
);
return;
}
openNotification(_service, type, message, action, on_action);
}

function openNotification(
service: MatSnackBar,
type: string,
message: string,
action: string,
on_action?: () => void,
) {
const snackbar_ref = service.open(message, action, {
panelClass: [type],
duration: 5000,
});
Expand Down
11 changes: 7 additions & 4 deletions src/app/common/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from '@angular/core';
import { Title } from '@angular/platform-browser';
import { showMetadata, updateMetadata } from '@placeos/ts-client';
import { format, isSameDay } from 'date-fns';

import { AsyncHandler } from './async-handler.class';
import { getItemWithKeys, log } from './general';
Expand Down Expand Up @@ -74,9 +73,13 @@ export class SettingsService extends AsyncHandler {
super();
const now = new Date();
const time = new Date(VERSION.time);
const built = isSameDay(now, time)
? `Today at ${format(time, 'h:mma')}`
: format(time, 'do MMM yyyy, h:mma');
const built =
now.toDateString() === time.toDateString()
? `Today at ${time.toLocaleTimeString([], { timeStyle: 'short' })}`
: time.toLocaleString([], {
dateStyle: 'medium',
timeStyle: 'short',
});
log('CORE', `${VERSION.semver}`, null, 'debug', true);
log('APP', `${VERSION.hash} | Built: ${built}`, null, 'debug', true);
this.init();
Expand Down
4 changes: 2 additions & 2 deletions src/app/ui/global-banner.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, inject } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatRipple } from '@angular/material/core';
import { SettingsService } from '../common/settings.service';
import { IconComponent } from './icon.component';

Expand Down Expand Up @@ -39,7 +39,7 @@ export interface BannerDetails {
}
`,
],
imports: [IconComponent, MatRippleModule],
imports: [IconComponent, MatRipple],
})
export class GlobalBannerComponent {
private _settings = inject(SettingsService);
Expand Down
4 changes: 2 additions & 2 deletions src/app/ui/global-loading.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { authority, isOnline, token } from '@placeos/ts-client';

import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressBar } from '@angular/material/progress-bar';
import { AsyncHandler } from '../common/async-handler.class';
import { getLoadingMessage } from '../common/placeos';
import { SettingsService } from '../common/settings.service';
Expand Down Expand Up @@ -46,7 +46,7 @@ import { waitForSignalValue } from '../common/signals';
}
`,
],
imports: [MatProgressBarModule],
imports: [MatProgressBar],
})
export class GlobalLoadingComponent extends AsyncHandler implements OnInit {
private _settings = inject(SettingsService);
Expand Down
28 changes: 0 additions & 28 deletions src/app/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ import {
onlineState,
queryUsers,
} from '@placeos/ts-client';
import { Md5 } from 'ts-md5';

import { AsyncHandler } from '../common/async-handler.class';
import { SettingsService } from '../common/settings.service';
import { loadSupportAccess } from '../common/support-access';
import { FilterFn } from '../common/types';
import { current_user, setCurrentUser } from '../common/user-state';

import * as Sentry from '@sentry/browser';
import { addDays } from 'date-fns';

type ServiceItem = PlaceUser;

@Service()
Expand Down Expand Up @@ -102,9 +98,6 @@ export class BackofficeUsersService extends AsyncHandler {
}
await loadSupportAccess(user);
setCurrentUser(user);
Sentry.withScope((scope) =>
scope.setUser({ email: user.email }),
);
this.state.set('success');
this._initialised.set(true);
// Trigger dark mode getter to apply current theme
Expand All @@ -121,27 +114,6 @@ export class BackofficeUsersService extends AsyncHandler {
});
}

/**
* Manually set the user access token
* @param token Token to set
* @param expiry Expiry time of the token
*/
public setToken(token: string, expiry: number) {
if (!expiry) {
expiry = addDays(Date.now(), 7).valueOf();
}
const path = `${location.origin}${
this._settings.get('composer.route') || ''
}/oauth-resp.html`;
if (localStorage) {
const client_id = Md5.hashStr(path);
localStorage.setItem(`${client_id}_access_token`, token);
localStorage.setItem(`${client_id}_expires_at`, `${expiry}`);
location.reload();
}
return path;
}

/**
* Login with given credentials
* @param fields Key value pairs of post parameters
Expand Down
Loading
Loading