-
-
Update available
-
- Refresh to load the latest Backoffice version.
-
+
+ @defer {
+
+
+
+
+ Update available
+
+
+ Refresh to load the latest Backoffice version.
+
+
+
-
-
-
+
+ }
}
`,
styles: [
@@ -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 });
@@ -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...');
@@ -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'));
@@ -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[]) : [],
);
@@ -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
| undefined;
@@ -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)),
]);
}
diff --git a/src/app/common/locale.service.ts b/src/app/common/locale.service.ts
index ea19bfb74..25a067fe6 100644
--- a/src/app/common/locale.service.ts
+++ b/src/app/common/locale.service.ts
@@ -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';
@@ -93,8 +91,16 @@ export class LocaleService {
private _cache_time = 7 * 24 * 60 * 60 * 1000;
private _load_promises: Record> = {};
- private _default_mappings: Record =
- removeNesting(DEFAULT_LOCALE);
+ private _default_mappings: Record = {};
+ /**
+ * Resolves when the fallback translations have loaded.
+ * They load lazily to keep them out of the initial bundle.
+ */
+ public readonly ready: Promise = import(
+ '../../../public/assets/locale/en-AU.json'
+ ).then(({ default: data }) => {
+ this._default_mappings = removeNesting(data);
+ });
private _locale_mappings: Record> = {};
public locale_folder = 'assets/locale';
diff --git a/src/app/common/notifications.ts b/src/app/common/notifications.ts
index bdcc10508..b6e6709c4 100644
--- a/src/app/common/notifications.ts
+++ b/src/app/common/notifications.ts
@@ -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 | 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) {
_service = snackbar;
+ if (snackbar instanceof Promise) {
+ snackbar.then((service) => {
+ if (_service === snackbar) _service = service;
+ });
+ }
}
/**
@@ -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,
});
diff --git a/src/app/common/settings.service.ts b/src/app/common/settings.service.ts
index c920e5337..d5f93f573 100644
--- a/src/app/common/settings.service.ts
+++ b/src/app/common/settings.service.ts
@@ -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';
@@ -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();
diff --git a/src/app/ui/global-banner.component.ts b/src/app/ui/global-banner.component.ts
index 9e2464aaa..334e6dc91 100644
--- a/src/app/ui/global-banner.component.ts
+++ b/src/app/ui/global-banner.component.ts
@@ -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';
@@ -39,7 +39,7 @@ export interface BannerDetails {
}
`,
],
- imports: [IconComponent, MatRippleModule],
+ imports: [IconComponent, MatRipple],
})
export class GlobalBannerComponent {
private _settings = inject(SettingsService);
diff --git a/src/app/ui/global-loading.component.ts b/src/app/ui/global-loading.component.ts
index 0fa2b5582..907ab6e0a 100644
--- a/src/app/ui/global-loading.component.ts
+++ b/src/app/ui/global-loading.component.ts
@@ -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';
@@ -46,7 +46,7 @@ import { waitForSignalValue } from '../common/signals';
}
`,
],
- imports: [MatProgressBarModule],
+ imports: [MatProgressBar],
})
export class GlobalLoadingComponent extends AsyncHandler implements OnInit {
private _settings = inject(SettingsService);
diff --git a/src/app/users/users.service.ts b/src/app/users/users.service.ts
index 0d41ba58b..f5989bfbe 100644
--- a/src/app/users/users.service.ts
+++ b/src/app/users/users.service.ts
@@ -7,7 +7,6 @@ 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';
@@ -15,9 +14,6 @@ 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()
@@ -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
@@ -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
diff --git a/src/tests/common/notifications.spec.ts b/src/tests/common/notifications.spec.ts
index c1d692dff..53ec32850 100644
--- a/src/tests/common/notifications.spec.ts
+++ b/src/tests/common/notifications.spec.ts
@@ -65,6 +65,23 @@ describe('notifications.ts', () => {
// If set correctly, notify should not throw
expect(() => notify('test', 'message')).not.toThrow();
});
+
+ it('should open notifications made while a lazy snackbar loads', async () => {
+ let resolve: (snackbar: unknown) => void;
+ const pending = new Promise((r) => (resolve = r));
+ setNotifyOutlet(pending as any);
+
+ notify('info', 'Early message');
+ expect(mock_snackbar.open).not.toHaveBeenCalled();
+
+ resolve(mock_snackbar);
+ await pending;
+ expect(mock_snackbar.open).toHaveBeenCalledWith(
+ 'Early message',
+ 'OK',
+ expect.any(Object),
+ );
+ });
});
describe('notify', () => {