src/app/core/support/support/support.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-support |
| standalone | true |
| imports |
MatExpansionModule
MatButtonModule
MatTooltipModule
|
| styleUrls | ./support.component.scss |
| templateUrl | ./support.component.html |
No results matching.
MatExpansionModule
MatButtonModule
MatTooltipModule
HintBoxComponent
OnInit
Properties |
Methods |
|
| copyDetails |
copyDetails()
|
|
Returns :
void
|
| Async downloadLocalDatabase |
downloadLocalDatabase()
|
|
Returns :
any
|
| Async resetLocalDevice |
resetLocalDevice()
|
|
Returns :
any
|
| appVersion |
Type : string
|
| Protected Readonly assistantService |
Type : unknown
|
Default value : inject(AssistantService)
|
| currentSyncState |
Type : string
|
| currentUser |
Type : Entity
|
| dbAdapter |
Type : string
|
| dbInfo |
Type : string
|
| dbName |
Type : string
|
| lastRemoteLogin |
Type : string
|
| lastSync |
Type : string
|
| sessionInfo |
Type : SessionInfo
|
| sessionType |
Type : string
|
Default value : environment.session_type
|
| storageInfo |
Type : unknown
|
Default value : signal<string | undefined>(undefined)
|
| storagePersistent |
Type : unknown
|
Default value : signal<boolean | undefined>(undefined)
|
| swLog |
Type : string
|
Default value : "not available"
|
| swStatus |
Type : string
|
| useIndexeddbAdapter |
Type : boolean
|
Default value : environment.use_indexeddb_adapter
|
| userAgent |
Type : string
|
import {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
signal,
} from "@angular/core";
import { WINDOW_TOKEN, LOCAL_STORAGE_TOKEN } from "../../../utils/di-tokens";
import { SyncState } from "../../session/session-states/sync-state.enum";
import { SwUpdate } from "@angular/service-worker";
import { HttpClient } from "@angular/common/http";
import { environment } from "../../../../environments/environment";
import { SessionInfo, SessionSubject } from "../../session/auth/session-info";
import { firstValueFrom } from "rxjs";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatButtonModule } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";
import { BackupService } from "../../admin/backup/backup.service";
import { LocalDeviceResetService } from "../../database/local-device-reset.service";
import { DownloadService } from "../../export/download-service/download.service";
import { SyncStateSubject } from "../../session/session-type";
import { KeycloakAuthService } from "../../session/auth/keycloak/keycloak-auth.service";
import { CurrentUserSubject } from "../../session/current-user-subject";
import { Entity } from "../../entity/model/entity";
import { SyncedPouchDatabase } from "../../database/pouchdb/synced-pouch-database";
import { DatabaseResolverService } from "../../database/database-resolver.service";
import { PouchDatabase } from "../../database/pouchdb/pouch-database";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { AssistantService } from "#src/app/core/setup/assistant.service";
import { Clipboard } from "@angular/cdk/clipboard";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-support",
templateUrl: "./support.component.html",
styleUrls: ["./support.component.scss"],
imports: [
MatExpansionModule,
MatButtonModule,
MatTooltipModule,
HintBoxComponent,
],
})
export class SupportComponent implements OnInit {
private readonly localStorage = inject(LOCAL_STORAGE_TOKEN);
private syncState = inject(SyncStateSubject);
private sessionSubject = inject(SessionSubject);
private currentUserSubject = inject(CurrentUserSubject);
private sw = inject(SwUpdate);
private databaseResolver = inject(DatabaseResolverService);
private http = inject(HttpClient);
private backupService = inject(BackupService);
private readonly localDeviceResetService = inject(LocalDeviceResetService);
private downloadService = inject(DownloadService);
private window = inject<Window>(WINDOW_TOKEN);
protected readonly assistantService = inject(AssistantService);
private readonly clipboard = inject(Clipboard);
sessionInfo: SessionInfo;
currentUser: Entity;
currentSyncState: string;
lastSync: string;
lastRemoteLogin: string;
storageInfo = signal<string | undefined>(undefined);
storagePersistent = signal<boolean | undefined>(undefined);
swStatus: string;
swLog = "not available";
userAgent: string;
appVersion: string;
dbInfo: string;
dbName: string;
dbAdapter: string;
useIndexeddbAdapter: boolean = environment.use_indexeddb_adapter;
sessionType: string = environment.session_type;
ngOnInit() {
this.userAgent = this.window.navigator.userAgent;
this.sessionInfo = this.sessionSubject.value;
this.currentUser = this.currentUserSubject.value;
this.appVersion = environment.appVersion;
this.initCurrentSyncState();
this.initLastSync();
this.initLastRemoteLogin();
this.initStorageInfo();
this.initSwStatus();
return this.initDbInfo();
}
private initCurrentSyncState() {
switch (this.syncState.value) {
case SyncState.COMPLETED:
this.currentSyncState = "synced";
return;
case SyncState.STARTED:
this.currentSyncState = "in progress";
return;
default:
this.currentSyncState = "unsynced";
}
}
private initLastSync() {
const db = this.databaseResolver.getDatabase();
const lastSyncKey =
db instanceof SyncedPouchDatabase ? db.LAST_SYNC_KEY : undefined;
this.lastSync =
(lastSyncKey && this.localStorage.getItem(lastSyncKey)) || "never";
}
private initLastRemoteLogin() {
this.lastRemoteLogin =
this.localStorage.getItem(KeycloakAuthService.LAST_AUTH_KEY) || "never";
}
private async initStorageInfo() {
const storage = this.window.navigator?.storage;
if (!storage) {
return;
}
try {
const [estimateResult, persistedResult] = await Promise.allSettled([
storage.estimate(),
storage.persisted(),
]);
if (persistedResult.status === "fulfilled") {
this.storagePersistent.set(persistedResult.value);
}
const estimate =
estimateResult.status === "fulfilled"
? estimateResult.value
: undefined;
if (
Number.isFinite(estimate?.usage) &&
Number.isFinite(estimate?.quota)
) {
const used = estimate.usage / 1048576;
const available = estimate.quota / 1048576;
this.storageInfo.set(
`${used.toFixed(2)}MB / ${available.toFixed(2)}MB`,
);
}
} catch {
// Storage information is unavailable in some browser contexts.
}
}
private initSwStatus() {
if (this.sw.isEnabled) {
this.swStatus = "enabled";
} else {
this.swStatus = "not enabled";
}
this.window.navigator.serviceWorker.ready
.then(() =>
firstValueFrom(this.http.get("/ngsw/state", { responseType: "text" })),
)
.then((res) => (this.swLog = res));
}
private async initDbInfo() {
const db = this.databaseResolver.getDatabase();
if (!(db instanceof PouchDatabase)) {
this.dbInfo = "db not initialized";
return;
}
try {
const pouchDb = await db.getPouchDBOnceReady();
const res = await pouchDb.info();
this.dbAdapter =
res && typeof res["adapter"] === "string" ? res["adapter"] : db.adapter;
this.dbName = res.db_name;
this.dbInfo = `${res.doc_count} (update sequence ${res.update_seq})`;
} catch {
this.dbInfo = "db not initialized";
}
}
copyDetails() {
// This is sent even without submitting the crash report.
const debugInfo = {
user: {
id: this.sessionInfo?.id,
email: this.sessionInfo?.email,
name: this.sessionInfo?.name,
},
level: "debug",
extra: {
currentUser: this.currentUser?.getId(),
currentSyncState: this.currentSyncState,
lastSync: this.lastSync,
lastRemoteLogin: this.lastRemoteLogin,
swStatus: this.swStatus,
userAgent: this.userAgent,
swLog: this.swLog,
storageInfo: this.storageInfo(),
storagePersistent: this.storagePersistent(),
dbInfo: this.dbInfo,
dbName: this.dbName,
dbAdapter: this.dbAdapter,
useIndexeddbAdapter: this.useIndexeddbAdapter,
sessionType: this.sessionType,
timestamp: new Date().toISOString(),
},
};
this.clipboard.copy(JSON.stringify(debugInfo, null, 2));
}
async resetLocalDevice() {
await this.localDeviceResetService.resetLocalDevice();
}
async downloadLocalDatabase() {
const backup = await this.backupService.getDatabaseExport();
await this.downloadService.triggerDownload(
backup,
"json",
"aamdigital_data_" + new Date().toISOString(),
);
}
}
<app-hint-box>
<h2 i18n>User Guides & Support</h2>
<p i18n>
You can use our context-aware "Virtual Assistant" to get support and access
our help articles and other resources. Click on the button in the top
toolbar to open it. This page here is providing technical details for
advanced troubleshooting.
</p>
<div>
<button
mat-raised-button
color="accent"
(click)="assistantService.openAssistant()"
i18n
>
Open Virtual Assistant
</button>
</div>
</app-hint-box>
<div class="margin-top-large"></div>
<h1 i18n>Technical User Support Details</h1>
<p id="table-description" class="text-secondary" i18n>
The following details of your device and app can help the user support
troubleshoot when you are having technical issues.
</p>
<table class="support-table" aria-describedby="table-description">
<thead>
<tr>
<th i18n>Detail</th>
<th i18n>Value</th>
</tr>
</thead>
<tbody class="support-table--body">
<tr>
<td i18n>Session</td>
<td>{{ sessionInfo?.id ?? "-" }}</td>
</tr>
<tr>
<td i18n>Session type</td>
<td>{{ sessionType }}</td>
</tr>
<tr>
<td i18n>Username</td>
<td>{{ sessionInfo?.email ?? "-" }}</td>
</tr>
<tr>
<td i18n>User Profile</td>
<td>
@if (currentUser) {
{{ sessionInfo?.entityId }} ({{ currentUser?.toString() }})
} @else {
-
}
</td>
</tr>
<tr>
<td i18n>App version</td>
<td>{{ appVersion }}</td>
</tr>
<tr>
<td i18n>Device info</td>
<td>{{ userAgent }}</td>
</tr>
<tr>
<td i18n>Current sync state</td>
<td>{{ currentSyncState }}</td>
</tr>
<tr>
<td i18n>Last sync</td>
<td>{{ lastSync }}</td>
</tr>
<tr>
<td i18n>Last remote login</td>
<td>{{ lastRemoteLogin }}</td>
</tr>
<tr>
<td i18n>Database name</td>
<td>{{ dbName ?? "-" }}</td>
</tr>
<tr>
<td i18n>Database adapter</td>
<!-- eslint-disable @angular-eslint/template/i18n -- raw diagnostic output incl. a literal config flag name -->
<td>
{{ dbAdapter ?? "-" }}
(use_indexeddb_adapter config flag: {{ useIndexeddbAdapter }})
</td>
<!-- eslint-enable @angular-eslint/template/i18n -->
</tr>
<tr>
<td i18n>Storage usage</td>
<td>{{ storageInfo() || "No information" }}</td>
</tr>
<tr>
<td i18n>Persistent storage</td>
<td>
@if (storagePersistent() === true) {
<span i18n>Yes</span>
} @else if (storagePersistent() === false) {
<span i18n>No</span>
} @else {
<span i18n>No information</span>
}
</td>
</tr>
<tr>
<td i18n>Database documents</td>
<td>{{ dbInfo }}</td>
</tr>
<tr>
<td i18n>Service Worker</td>
<td>{{ swStatus }}</td>
</tr>
</tbody>
</table>
<mat-expansion-panel class="mat-elevation-z0">
<mat-expansion-panel-header>
<mat-panel-title class="tech-details">
<span i18n>Service Worker Logs</span>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="sw-logs tech-details">{{ swLog }}</div>
</mat-expansion-panel>
<div class="flex-row gap-regular">
<button mat-raised-button (click)="copyDetails()" i18n>Copy Details</button>
<button
mat-raised-button
matTooltip="Download a backup of all data in your local database."
i18n-matTooltip="Support Panel - Download local db tooltip"
(click)="downloadLocalDatabase()"
i18n
>
Download Local Database
</button>
<button mat-raised-button color="warn" (click)="resetLocalDevice()" i18n>
Reset Application
</button>
</div>
./support.component.scss
@use "variables/sizes";
@use "variables/colors";
$table-odd-color: colors.$grey-light;
$table-even-color: colors.$grey-medium;
$table-border-color: colors.$border-color;
$table-border: 1px solid $table-border-color;
$table-padding-horizontal: 12px;
$table-padding-vertical: 16px;
$table-padding: $table-padding-vertical $table-padding-horizontal;
:host {
max-width: sizes.$max-text-width;
margin: 0 auto;
display: block;
}
.support-table {
border-collapse: collapse;
border-spacing: 0;
margin-bottom: sizes.$large;
&--body {
> tr:nth-of-type(odd) {
background-color: $table-odd-color;
border-top: $table-border;
border-bottom: $table-border;
}
> tr:nth-of-type(even) {
background-color: $table-even-color;
}
> tr td:first-child {
width: 25%;
}
> tr td:nth-child(2) {
width: 75%;
}
}
th {
padding: $table-padding;
text-align: left;
}
td {
padding: $table-padding;
}
}
.tech-details {
color: colors.$hint-text;
}
.mat-expansion-panel {
background-color: transparent;
border: 1px colors.$hint-text solid;
margin-bottom: sizes.$large;
}
.sw-logs {
overflow: auto;
white-space: pre;
}