src/app/core/support/support/support.component.ts

Implements

OnInit

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Methods

copyDetails
copyDetails()
Returns : void
Async downloadLocalDatabase
downloadLocalDatabase()
Returns : any
Async resetLocalDevice
resetLocalDevice()
Returns : any

Properties

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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""