From 05dd760388d8d23e00902b8b624a157ba3e2532e Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 24 Apr 2025 14:14:08 -0600 Subject: [PATCH] Fix links for docs (#2908) * fix docs paths * docsLink directive * fix: bugs (#2909) --------- Co-authored-by: Alex Inkin --- CONTRIBUTING.md | 2 +- sdk/package/lib/StartSdk.ts | 7 +-- .../src/components/menu/menu.component.html | 4 +- .../app/components/documentation.component.ts | 12 +++--- .../src/directives/docs-link.directive.ts | 31 +++++++++++++ web/projects/shared/src/public-api.ts | 1 + .../shared/src/services/error.service.ts | 2 +- web/projects/ui/src/app/app.providers.ts | 6 +++ .../notifications-toast.component.ts | 10 +++-- .../login/ca-wizard/ca-wizard.component.html | 43 ++++++++++++++----- .../login/ca-wizard/ca-wizard.component.ts | 3 +- .../components/header/about.component.ts | 25 ++++++----- .../components/header/menu.component.ts | 39 ++++++++++------- .../interfaces/clearnet.component.ts | 2 +- .../components/interfaces/local.component.ts | 10 ++--- .../components/interfaces/tor.component.ts | 9 ++-- .../routes/backups/modals/jobs.component.ts | 11 ++--- .../backups/modals/targets.component.ts | 11 ++--- .../portal/routes/metrics/time.component.ts | 8 ++-- .../system/routes/acme/acme.component.ts | 13 ++++-- .../routes/backups/backups.component.ts | 17 ++++---- .../system/routes/domains/info.component.ts | 12 ++---- .../system/routes/email/email.component.ts | 7 +-- .../routes/general/general.component.ts | 8 +++- .../system/routes/proxies/info.component.ts | 12 ++---- .../system/routes/router/info.component.ts | 21 ++------- .../routes/system/routes/ssh/ssh.component.ts | 7 +-- web/projects/ui/src/app/utils/resources.ts | 19 -------- web/projects/ui/src/styles.scss | 8 +++- 29 files changed, 193 insertions(+), 167 deletions(-) create mode 100644 web/projects/shared/src/directives/docs-link.directive.ts delete mode 100644 web/projects/ui/src/app/utils/resources.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbc6c9039..b81e9ce82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to StartOS -This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/developer-docs/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute). +This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/packaging-guide/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute). ## Collaboration diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 6dbafae57..a8b0adacf 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -601,10 +601,6 @@ export class StartSdk { setupPostInstall: (fn: InstallFn) => PostInstall.of(fn), /** * @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save. - * - * "input" will be of type `Input` for inputSpec save. It will be `null` for install and update. - * - * To learn about creating multi-hosts and interfaces, check out the {@link https://docs.start9.com/packaging-guide/learn/interfaces documentation}. * @param inputSpec - The inputSpec spec of this service as exported from /inputSpec/spec. * @param fn - an async function that returns an array of interface receipts. The function always has access to `effects`; it has access to `input` only after inputSpec save, otherwise `input` will be null. * @example @@ -612,8 +608,7 @@ export class StartSdk { * * ``` export const setInterfaces = sdk.setupInterfaces( - inputSpecSpec, - async ({ effects, input }) => { + async ({ effects }) => { // ** UI multi-host ** const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') const uiMultiOrigin = await uiMulti.bindPort(80, { diff --git a/web/projects/marketplace/src/components/menu/menu.component.html b/web/projects/marketplace/src/components/menu/menu.component.html index 69d7e8510..47fe4ffd0 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.html +++ b/web/projects/marketplace/src/components/menu/menu.component.html @@ -62,7 +62,7 @@ Package a service @@ -90,7 +90,7 @@ Package a service diff --git a/web/projects/setup-wizard/src/app/components/documentation.component.ts b/web/projects/setup-wizard/src/app/components/documentation.component.ts index 43473c1b9..6494d2116 100644 --- a/web/projects/setup-wizard/src/app/components/documentation.component.ts +++ b/web/projects/setup-wizard/src/app/components/documentation.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { DocsLinkDirective } from '@start9labs/shared' @Component({ standalone: true, @@ -43,9 +44,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'

Download your server's Root CA and follow the instructions @@ -110,9 +110,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' Note: This address will only work from a Tor-enabled browser. Follow the instructions @@ -135,6 +134,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' `, changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DocsLinkDirective], }) export class DocumentationComponent { @Input({ required: true }) lanAddress!: string diff --git a/web/projects/shared/src/directives/docs-link.directive.ts b/web/projects/shared/src/directives/docs-link.directive.ts new file mode 100644 index 000000000..3aebf54d2 --- /dev/null +++ b/web/projects/shared/src/directives/docs-link.directive.ts @@ -0,0 +1,31 @@ +import { + computed, + Directive, + inject, + InjectionToken, + input, +} from '@angular/core' + +const HOST = 'https://staging.docs.start9.com' +export const VERSION = new InjectionToken('VERSION') + +@Directive({ + selector: '[docsLink]', + standalone: true, + host: { + target: '_blank', + rel: 'noreferrer', + '[href]': 'url()', + }, +}) +export class DocsLinkDirective { + private readonly version = inject(VERSION) + + readonly href = input.required() + + protected readonly url = computed(() => { + const path = this.href() + const relative = path.startsWith('/') ? path : `/${path}` + return `${HOST}${relative}?os=${this.version}` + }) +} diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index 1bbbaee14..9b2697d00 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -13,6 +13,7 @@ export * from './components/markdown.component' export * from './components/prompt.component' export * from './components/server.component' +export * from './directives/docs-link.directive' export * from './directives/drag-scroller.directive' export * from './directives/safe-links.directive' diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts index 5246778df..46e129649 100644 --- a/web/projects/shared/src/services/error.service.ts +++ b/web/projects/shared/src/services/error.service.ts @@ -30,7 +30,7 @@ export function getErrorMessage(e: HttpError | string, link?: string): string { 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' } else if (!e.message) { message = 'Unknown Error' - link = 'https://docs.start9.com/latest/support/faq' + link = 'https://docs.start9.com/help/common-issues.html' } else { message = e.message } diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 79abcedfe..5ac21f426 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -10,6 +10,7 @@ import { I18N_STORAGE, i18nService, RELATIVE_URL, + VERSION, WorkspaceConfig, } from '@start9labs/shared' import { @@ -27,6 +28,7 @@ import { import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy' import { PatchDB } from 'patch-db-client' import { filter, of, pairwise } from 'rxjs' +import { ConfigService } from 'src/app/services/config.service' import { PATCH_CACHE, PatchDbSource, @@ -112,6 +114,10 @@ export const APP_PROVIDERS: Provider[] = [ return (language: string) => api.setDbValue(['language'], language) }, }, + { + provide: VERSION, + useFactory: () => inject(ConfigService).version, + }, ] export function appInitializer(): () => void { diff --git a/web/projects/ui/src/app/components/notifications-toast.component.ts b/web/projects/ui/src/app/components/notifications-toast.component.ts index c15be9e7e..3c2e20977 100644 --- a/web/projects/ui/src/app/components/notifications-toast.component.ts +++ b/web/projects/ui/src/app/components/notifications-toast.component.ts @@ -2,7 +2,7 @@ import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' import { i18nPipe } from '@start9labs/shared' -import { TuiAlert } from '@taiga-ui/core' +import { TuiAlert, TuiLink } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { endWith, map, merge, Observable, pairwise, Subject } from 'rxjs' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -17,13 +17,17 @@ import { DataModel } from 'src/app/services/patch-db/data-model' (tuiAlertChange)="onDismiss()" > {{ 'New notifications' | i18n }} - + {{ 'View' | i18n }} `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiAlert, RouterLink, AsyncPipe, i18nPipe], + imports: [TuiAlert, RouterLink, AsyncPipe, i18nPipe, TuiLink], }) export class NotificationsToastComponent { private readonly dismiss$ = new Subject() diff --git a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html index 03b6685f0..e4c1c4719 100644 --- a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html +++ b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html @@ -7,16 +7,27 @@

{{ 'Trust your Root CA' | i18n }}

- {{ 'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.' | i18n }} + {{ + 'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.' + | i18n + }}

  1. {{ 'Bookmark this page' | i18n }} - - {{ 'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.' | i18n }} + - + {{ + 'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.' + | i18n + }}
  2. {{ 'Download your Root CA' | i18n }} - - {{ 'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.' | i18n }} + - + {{ + 'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.' + | i18n + }}
  3. {{ 'Trust your Root CA' | i18n }} - - {{ 'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.' | i18n }} + - + {{ + 'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.' + | i18n + }}
    {{ 'View instructions' | i18n }} @@ -44,7 +58,11 @@
  4. {{ 'Test' | i18n }} - - {{ 'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.' | i18n }} + - + {{ + 'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.' + | i18n + }}
    -
    ({{ 'not recommended' | i18n }})
    +
    + ({{ 'not recommended' | i18n }}) +
    @@ -76,7 +96,10 @@

    {{ 'Root CA Trusted!' | i18n }}

    - {{ 'You have successfully trusted your Root CA and may now log in securely.' | i18n }} + {{ + 'You have successfully trusted your Root CA and may now log in securely.' + | i18n + }}

    - - @for (link of links; track $index) { -
    - {{ link.name | i18n }} - - } + + + {{ 'User manual' | i18n }} + + + {{ 'Contact support' | i18n }} + + + {{ 'Donate to Start9' | i18n }} + diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts index 5d9e878c4..696db6974 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts @@ -5,7 +5,7 @@ import { TableComponent } from 'src/app/routes/portal/components/table.component import { InterfaceActionsComponent } from './actions.component' import { AddressDetails } from './interface.utils' import { MaskPipe } from './mask.pipe' -import { i18nPipe } from '@start9labs/shared' +import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -19,12 +19,7 @@ import { i18nPipe } from '@start9labs/shared' 'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.' | i18n }} - + {{ 'Learn More' | i18n }} @@ -48,6 +43,7 @@ import { i18nPipe } from '@start9labs/shared' InterfaceActionsComponent, MaskPipe, i18nPipe, + DocsLinkDirective, ], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts index 790942f3c..e01f10617 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts @@ -6,6 +6,7 @@ import { } from '@angular/core' import { DialogService, + DocsLinkDirective, ErrorService, i18nPipe, LoadingService, @@ -50,12 +51,7 @@ type OnionForm = { 'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.' | i18n }} - + {{ 'Learn More' | i18n }} @@ -131,6 +127,7 @@ type OnionForm = { TuiFade, TuiFluidTypography, i18nPipe, + DocsLinkDirective, ], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts index 92def4e0a..427bc2bb6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts @@ -18,6 +18,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe' import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe' import { BackupJobBuilder } from '../utils/job-builder' import { EDIT } from './edit.component' +import { DocsLinkDirective } from 'projects/shared/src/public-api' @Component({ template: ` @@ -25,14 +26,7 @@ import { EDIT } from './edit.component' Scheduling automatic backups is an excellent way to ensure your StartOS data is safely backed up. StartOS will issue a notification whenever one of your scheduled backups succeeds or fails. - - View instructions - + View instructions

    Saved Jobs @@ -147,6 +141,7 @@ import { EDIT } from './edit.component' GetBackupIconPipe, TuiSkeleton, TuiLink, + DocsLinkDirective, ], }) export class BackupsJobsModal implements OnInit { diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts index 2ba35d915..6c3f58072 100644 --- a/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts @@ -23,6 +23,7 @@ import { googleDriveSpec, remoteBackupTargetSpec, } from '../types/target' +import { DocsLinkDirective } from 'projects/shared/src/public-api' @Component({ template: ` @@ -31,14 +32,7 @@ import { backups. They can be physical drives plugged into your server, shared folders on your Local Area Network (LAN), or third party clouds such as Dropbox or Google Drive. - - View instructions - + View instructions

    Unknown Physical Drives @@ -77,6 +71,7 @@ import { BackupsPhysicalComponent, BackupsTargetsComponent, TuiLink, + DocsLinkDirective, ], }) export class BackupsTargetsModal implements OnInit { diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts index fea30fc2e..7153f42cb 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { i18nPipe } from '@start9labs/shared' +import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { TuiHint, TuiIcon, @@ -47,11 +47,10 @@ import { TimeService } from 'src/app/services/time.service' To resolve it, refer to @@ -110,6 +109,7 @@ import { TimeService } from 'src/app/services/time.service' TuiIcon, TuiHint, i18nPipe, + DocsLinkDirective, ], }) export class TimeComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts index 629f158b4..905bad0fb 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts @@ -1,7 +1,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { RouterLink } from '@angular/router' -import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared' +import { + DocsLinkDirective, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { ISB, utils } from '@start9labs/start-sdk' import { TuiButton, TuiLink, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiCell, TuiHeader } from '@taiga-ui/layout' @@ -33,9 +38,8 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' }} Adding domains permits accessing your server and services over clearnet. - - View instructions - + View instructions `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiNotification, TuiLink], + imports: [TuiNotification, TuiLink, DocsLinkDirective], }) export class DomainsInfoComponent {} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts index c0c747b4a..4fefa5654 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts @@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { RouterLink } from '@angular/router' import { DialogService, + DocsLinkDirective, ErrorService, i18nKey, i18nPipe, @@ -40,9 +41,8 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' }} ({ label: 'Browser Tab Title', data: { @@ -279,8 +280,11 @@ export default class SystemGeneralComponent { try { await this.api.setDbValue(['name'], name || null) + } catch (e: any) { + this.errorService.handleError(e) } finally { loader.unsubscribe() + sub.unsubscribe() } }) } @@ -293,7 +297,7 @@ export default class SystemGeneralComponent { data: { content: new PolymorpheusComponent( SystemWipeComponent, - inject(INJECTOR), + this.injector, ), yes: 'Reset', no: 'Cancel', diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/info.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/info.component.ts index 3af589d65..6fa44609d 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/info.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/info.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { TuiLink, TuiNotification } from '@taiga-ui/core' +import { DocsLinkDirective } from 'projects/shared/src/public-api' @Component({ selector: 'proxies-info', @@ -24,18 +25,11 @@ import { TuiLink, TuiNotification } from '@taiga-ui/core' VPN access to your server/services

- - View instructions - + View instructions `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiNotification, TuiLink], + imports: [TuiNotification, TuiLink, DocsLinkDirective], }) export class ProxiesInfoComponent {} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/router/info.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/router/info.component.ts index ebc21a374..ac333a4ac 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/router/info.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/router/info.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { TuiLink, TuiNotification } from '@taiga-ui/core' +import { DocsLinkDirective } from 'projects/shared/src/public-api' @Component({ selector: 'router-info', @@ -14,14 +15,7 @@ import { TuiLink, TuiNotification } from '@taiga-ui/core'

If you are running multiple servers, you may want to override specific ports to suite your needs. - - View instructions - + View instructions } @else { UPnP Disabled

@@ -31,21 +25,14 @@ import { TuiLink, TuiNotification } from '@taiga-ui/core'

Alternatively, you can enable UPnP in your router for automatic configuration. - - View instructions - + View instructions } `, styles: ['strong { font-size: 1rem }'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiNotification, TuiLink], + imports: [TuiNotification, TuiLink, DocsLinkDirective], }) export class RouterInfoComponent { @Input() diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts index 5d2e4e832..08244ceb8 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts @@ -9,6 +9,7 @@ import { catchError, defer, of } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { TitleDirective } from 'src/app/services/title.service' import { SSHTableComponent } from './table.component' +import { DocsLinkDirective } from 'projects/shared/src/public-api' @Component({ template: ` @@ -23,9 +24,8 @@ import { SSHTableComponent } from './table.component' Manage your SSH keys to access your server from the command line