add descriptions to marketplace list page (#1812)

* add descriptions to marketplace list page

* clean up unused styling

* rip descriptions from registry marketplace, use binary choice custom default and alternative messages

* cleanup

* fix selected type and remove uneeded conditional

* conditional color

* cleanup

* better comparision of marketplace url duplicates

* add logic to handle marketplace description display based on url

* decrease font size

* abstract helper fn to get url hostname; add error toast when adding duplicate marketplace

* move helper function to more appropriate file location

* rework marketplace list and don't worry about patch db firing before bootstrapped

* remove aes-js

* reinstall aes just to please things for now

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Lucy C
2022-09-21 12:41:19 -06:00
committed by GitHub
parent 7575e8c1de
commit f8ea2ebf62
26 changed files with 930 additions and 893 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0",
"@start9labs/emver": "^0.1.5",
"@types/aes-js": "^3.1.1",
"aes-js": "^3.1.2",
"ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1",
@@ -66,7 +67,6 @@
"@angular/compiler-cli": "^14.1.0",
"@angular/language-service": "^14.1.0",
"@ionic/cli": "^6.19.0",
"@types/aes-js": "^3.1.1",
"@types/dompurify": "^2.3.3",
"@types/estree": "^0.0.51",
"@types/js-yaml": "^4.0.5",

View File

@@ -49,3 +49,7 @@ export function isValidHttpUrl(string: string): boolean {
return false
}
}
export function getUrlHostname(url: string): string {
return new URL(url).hostname
}

View File

@@ -7,7 +7,7 @@
>
<ion-menu contentId="main-content" type="overlay">
<ion-content color="light" scrollY="false">
<app-menu></app-menu>
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { filter, map, take } from 'rxjs/operators'
import { map, take } from 'rxjs/operators'
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
@@ -29,7 +29,6 @@ export class BackupSelectPage {
this.patch
.watch$('package-data')
.pipe(
filter(Boolean),
map(pkgs => {
return Object.values(pkgs).map(pkg => {
const { id, title } = pkg.manifest

View File

@@ -2,11 +2,7 @@ import { Inject, Pipe, PipeTransform } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { DOCUMENT } from '@angular/common'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import {
isValidHttpUrl,
MarkdownComponent,
removeTrailingSlash,
} from '@start9labs/shared'
import { getUrlHostname, MarkdownComponent } from '@start9labs/shared'
import {
DataModel,
PackageDataEntry,
@@ -144,7 +140,7 @@ export class ToButtonsPipe implements PipeTransform {
currentMarketplace: Marketplace | null,
altMarketplaces: UIMarketplaceData | null | undefined,
): Button {
const pkgMarketplace = pkg.installed?.['marketplace-url']
const pkgMarketplaceUrl = pkg.installed?.['marketplace-url']
// default button if package marketplace and current marketplace are the same
let button: Button = {
title: 'Marketplace',
@@ -154,37 +150,32 @@ export class ToButtonsPipe implements PipeTransform {
disabled: false,
description: 'View service in marketplace',
}
if (!pkgMarketplace) {
if (!pkgMarketplaceUrl) {
button.disabled = true
button.description = 'This package was not installed from a marketplace.'
button.action = () => {}
} else if (
pkgMarketplace &&
pkgMarketplaceUrl &&
currentMarketplace &&
removeTrailingSlash(pkgMarketplace) !==
removeTrailingSlash(currentMarketplace.url)
getUrlHostname(pkgMarketplaceUrl) !==
getUrlHostname(currentMarketplace.url)
) {
// attempt to get name for pkg marketplace
let pkgTitle = removeTrailingSlash(pkgMarketplace)
let pkgMarketplaceName = getUrlHostname(pkgMarketplaceUrl)
if (altMarketplaces) {
const nameOptions = Object.values(
const pkgMarketplaces = Object.values(
altMarketplaces['known-hosts'],
).filter(m => m.url === pkgTitle)
if (nameOptions.length) {
).filter(m => getUrlHostname(m.url) === pkgMarketplaceName)
if (pkgMarketplaces.length) {
// if multiple of the same url exist, they will have the same name, so fine to grab first
pkgTitle = nameOptions[0].name
pkgMarketplaceName = pkgMarketplaces[0].name
}
}
let marketplaceTitle = removeTrailingSlash(currentMarketplace.url)
// if we found a name for the pkg marketplace, use the name of the currently connected marketplace
if (!isValidHttpUrl(pkgTitle)) {
marketplaceTitle = currentMarketplace.name
}
button.action = () =>
this.differentMarketplaceAction(
pkgTitle,
marketplaceTitle,
pkgMarketplaceName,
currentMarketplace.name,
pkg.manifest.id,
)
button.description = 'Service was installed from a different marketplace'

View File

@@ -1,36 +0,0 @@
<h1 class="heading montserrat ion-text-center">{{ name }}</h1>
<marketplace-search [(query)]="query"></marketplace-search>
<ng-container *ngIf="pkgs && categories; else loading">
<marketplace-categories
[categories]="categories"
[category]="category"
[updatesAvailable]="(pkgs | filterPackages: '':'updates':localPkgs).length"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<div class="divider"></div>
<ion-grid *ngIf="pkgs | filterPackages: query:category:localPkgs as filtered">
<div *ngIf="!filtered.length && category === 'updates'" class="ion-padding">
<h1>All services are up to date!</h1>
</div>
<ion-row>
<ion-col *ngFor="let pkg of filtered" sizeXs="12" sizeSm="12" sizeMd="6">
<marketplace-item [pkg]="pkg">
<marketplace-status
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</marketplace-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
<ng-template #loading>
<marketplace-skeleton></marketplace-skeleton>
</ng-template>

View File

@@ -1,16 +0,0 @@
.heading {
font-size: 42px;
margin: 32px 0;
}
.divider {
margin: 24px;
}
.ion-padding {
text-align: center;
}
.status {
font-size: 14px;
}

View File

@@ -1,32 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-list-content',
templateUrl: 'marketplace-list-content.component.html',
styleUrls: ['./marketplace-list-content.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListContentComponent {
@Input()
pkgs: MarketplacePkg[] | null = null
@Input()
localPkgs: Record<string, PackageDataEntry> = {}
@Input()
categories: Set<string> | null = null
@Input()
name = ''
category = 'featured'
query = ''
onCategoryChange(category: string): void {
this.category = category
this.query = ''
}
}

View File

@@ -14,7 +14,6 @@ import {
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceListPage } from './marketplace-list.page'
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
const routes: Routes = [
{
@@ -39,7 +38,7 @@ const routes: Routes = [
SearchModule,
SkeletonModule,
],
declarations: [MarketplaceListPage, MarketplaceListContentComponent],
exports: [MarketplaceListPage, MarketplaceListContentComponent],
declarations: [MarketplaceListPage],
exports: [MarketplaceListPage],
})
export class MarketplaceListPageModule {}

View File

@@ -7,10 +7,76 @@
</ion-header>
<ion-content class="ion-padding">
<marketplace-list-content
[localPkgs]="(localPkgs$ | async) || {}"
[pkgs]="pkgs$ | async"
[categories]="categories$ | async"
[name]="(name$ | async) || ''"
></marketplace-list-content>
<ion-grid *ngIf="details$ | async as details">
<ion-row>
<ion-col size-lg="10" offset-lg="1" size-sm="12">
<ion-item class="description" [color]="details.color">
<ion-icon
text-wrap
size="large"
name="information-circle-outline"
></ion-icon>
<ion-label [innerHtml]="details.description"></ion-label>
</ion-item>
</ion-col>
</ion-row>
<ion-row>
<ion-col size="12">
<h1 class="heading montserrat ion-text-center">{{ details.name }}</h1>
<p style="margin-top: 0">{{ details.url }}</p>
<marketplace-search [(query)]="query"></marketplace-search>
</ion-col>
</ion-row>
<ion-row class="ion-align-items-center">
<ion-col size="12">
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
<ng-container *ngIf="localPkgs$ | async as localPkgs">
<marketplace-categories
*ngIf="categories$ | async as categories"
[categories]="categories"
[category]="category"
[updatesAvailable]="
(pkgs | filterPackages: '':'updates':localPkgs).length
"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<div class="divider"></div>
<ion-grid
*ngIf="pkgs | filterPackages: query:category:localPkgs as filtered"
>
<div
*ngIf="!filtered.length && category === 'updates'"
class="ion-padding"
>
<h1>All services are up to date!</h1>
</div>
<ion-row>
<ion-col
*ngFor="let pkg of filtered"
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<marketplace-item [pkg]="pkg">
<marketplace-status
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</marketplace-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ng-container>
<ng-template #loading>
<marketplace-skeleton></marketplace-skeleton>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,32 @@
.heading {
font-size: 42px;
margin-top: 32px;
}
.divider {
margin: 24px;
}
.ion-padding {
text-align: center;
}
.status {
font-size: 14px;
}
.description {
ion-icon {
padding-right: 8px;
}
@media (min-width: 1000px) {
ion-label {
::ng-deep p {
font-size: 1.1rem;
line-height: 25px;
}
}
}
}

View File

@@ -1,31 +1,65 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { map } from 'rxjs/operators'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { getUrlHostname } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { ConnectionService } from 'src/app/services/connection.service'
import { map } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-list',
templateUrl: './marketplace-list.page.html',
templateUrl: 'marketplace-list.page.html',
styleUrls: ['./marketplace-list.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListPage {
readonly connected$ = this.connectionService.connected$
readonly localPkgs$ = this.patch.watch$('package-data')
readonly categories$ = this.marketplaceService.getCategories()
readonly pkgs$ = this.marketplaceService.getPackages()
readonly details$ = this.marketplaceService.getMarketplace().pipe(
map(d => {
let color: string
let description: string
switch (getUrlHostname(d.url)) {
case 'registry.start9.com':
color = 'success'
description =
'Services in this marketplace are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service in this marketplace, one of our dedicated support staff will be happy to assist you.'
break
case 'beta-registry-0-3.start9labs.com':
color = 'primary'
description =
'Services in this marketplace are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
break
case 'community.start9labs.com':
color = 'tertiary'
description =
'Services in this marketplace are packaged and maintained by members of the Start9 community. <b>Install at your own risk</b>. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.'
break
default:
// alt marketplace
color = 'warning'
description =
'Warning. This is an <b>Alternative</b> Marketplace. Start9 cannot verify the integrity or functionality of services in this marketplace, and they may cause harm to your system. <b>Install at your own risk</b>.'
}
readonly name$ = this.marketplaceService
.getMarketplace()
.pipe(map(({ name }) => name))
return {
...d,
color,
description,
}
}),
)
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly connectionService: ConnectionService,
) {}
category = 'featured'
query = ''
onCategoryChange(category: string): void {
this.category = category
this.query = ''
}
}

View File

@@ -6,7 +6,11 @@ import {
ModalController,
} from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core'
import { DestroyService, ErrorToastService } from '@start9labs/shared'
import {
DestroyService,
ErrorToastService,
getUrlHostname,
} from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
@@ -244,8 +248,8 @@ export class MarketplacesPage {
}
// no-op on duplicates
const currentUrls = this.marketplaces.map(mp => mp.url)
if (currentUrls.includes(new URL(url).hostname)) return
const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url))
if (currentUrls.includes(getUrlHostname(url))) return
const loader = await this.loadingCtrl.create({
message: 'Validating Marketplace...',
@@ -288,8 +292,11 @@ export class MarketplacesPage {
}
// no-op on duplicates
const currentUrls = this.marketplaces.map(mp => mp.url)
if (currentUrls.includes(new URL(url).hostname)) return
const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url))
if (currentUrls.includes(getUrlHostname(url))) {
this.errToast.present({ message: 'Marketplace already added' })
return
}
const loader = await this.loadingCtrl.create({
message: 'Validating Marketplace...',

View File

@@ -5,7 +5,7 @@ import {
PipeTransform,
} from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { filter, take } from 'rxjs/operators'
import { take } from 'rxjs/operators'
import {
DataModel,
PackageMainStatus,
@@ -18,9 +18,7 @@ import { Observable } from 'rxjs'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BackingUpComponent {
readonly pkgs$ = this.patch
.watch$('package-data')
.pipe(filter(Boolean), take(1))
readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1))
readonly backupProgress$ = this.patch.watch$(
'server-info',
'status-info',

View File

@@ -10,7 +10,7 @@ import { ActivatedRoute } from '@angular/router'
import { PatchDB } from 'patch-db-client'
import { ServerNameService } from 'src/app/services/server-name.service'
import { Observable, of } from 'rxjs'
import { filter, take, tap } from 'rxjs/operators'
import { take, tap } from 'rxjs/operators'
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@@ -52,7 +52,6 @@ export class ServerShowPage {
this.patch
.watch$('recovered-packages')
.pipe(
filter(Boolean),
take(1),
tap(data => (this.hasRecoveredPackage = !isEmptyObject(data))),
)

View File

@@ -1786,7 +1786,7 @@ export module Mock {
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'marketplace-url.com',
'marketplace-url': 'https://marketplace-url.com',
'developer-key': 'developer-key',
},
'install-progress': undefined,
@@ -1835,7 +1835,7 @@ export module Mock {
icon: 'assets/img/service-icons/bitcoind.png',
},
},
'marketplace-url': 'marketplace-url.com',
'marketplace-url': 'https://marketplace-url.com',
'developer-key': 'developer-key',
},
'install-progress': undefined,
@@ -1895,7 +1895,7 @@ export module Mock {
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
},
},
'marketplace-url': 'marketplace-url.com',
'marketplace-url': 'https://marketplace-url.com',
'developer-key': 'developer-key',
},
'install-progress': undefined,

View File

@@ -1,4 +1,4 @@
import { Observable, ReplaySubject } from 'rxjs'
import { BehaviorSubject, Observable } from 'rxjs'
import { Update } from 'patch-db-client'
import { RR } from './api.types'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -6,7 +6,7 @@ import { Log } from '@start9labs/shared'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
export abstract class ApiService {
readonly patchStream$ = new ReplaySubject<Update<DataModel>[]>(1)
readonly patchStream$ = new BehaviorSubject<Update<DataModel>[]>([])
// http

View File

@@ -19,6 +19,7 @@ import { BehaviorSubject, interval, map, Observable } from 'rxjs'
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
import { mockPatchData } from './mock-patch'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { AuthService } from '../auth.service'
const PROGRESS: InstallProgress = {
size: 120,
@@ -39,8 +40,21 @@ export class MockApiService extends ApiService {
private readonly revertTime = 2000
sequence = 0
constructor(private readonly bootstrapper: LocalStorageBootstrap) {
constructor(
private readonly bootstrapper: LocalStorageBootstrap,
private readonly auth: AuthService,
) {
super()
this.auth.isVerified$.subscribe(verified => {
if (!verified) {
this.patchStream$.next([])
this.mockWsSource$.next({
id: 1,
value: mockPatchData,
})
this.sequence = 0
}
})
}
async getStatic(url: string): Promise<string> {

View File

@@ -436,7 +436,7 @@ export const mockPatchData: DataModel = {
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'marketplace-url.com',
'marketplace-url': 'https://marketplace-url.com',
'developer-key': 'developer-key',
},
},
@@ -650,7 +650,7 @@ export const mockPatchData: DataModel = {
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
},
},
'marketplace-url': 'marketplace-url.com',
'marketplace-url': 'https://marketplace-url.com',
'developer-key': 'developer-key',
},
},

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { BehaviorSubject, combineLatest } from 'rxjs'
import { distinctUntilChanged, filter, map } from 'rxjs/operators'
import { distinctUntilChanged, map } from 'rxjs/operators'
import { MarketplaceEOS } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -17,7 +17,6 @@ export class EOSService {
updateAvailable$ = new BehaviorSubject<boolean>(false)
readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe(
filter(Boolean),
map(status => !!status['update-progress'] || status.updated),
distinctUntilChanged(),
)

View File

@@ -56,7 +56,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
private readonly serverInfo$: Observable<ServerInfo> = this.patch
.watch$('server-info')
.pipe(filter(Boolean), take(1), shareReplay())
.pipe(take(1), shareReplay())
private readonly registryData$: Observable<MarketplaceData> =
this.uiMarketplaceData$.pipe(

View File

@@ -11,9 +11,7 @@ export interface ServerNameInfo {
@Injectable({ providedIn: 'root' })
export class ServerNameService {
private readonly chosenName$ = this.patch.watch$('ui', 'name')
private readonly hostname$ = this.patch
.watch$('server-info', 'hostname')
.pipe(filter(Boolean))
private readonly hostname$ = this.patch.watch$('server-info', 'hostname')
readonly name$: Observable<ServerNameInfo> = combineLatest([
this.chosenName$,

View File

@@ -15,5 +15,5 @@ export function getPackage(
export function getAllPackages(
patch: PatchDB<DataModel>,
): Promise<Record<string, PackageDataEntry>> {
return firstValueFrom(patch.watch$('package-data').pipe(filter(Boolean)))
return firstValueFrom(patch.watch$('package-data'))
}

View File

@@ -1,7 +1,7 @@
import { PatchDB } from 'patch-db-client'
import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model'
import { filter, firstValueFrom } from 'rxjs'
import { firstValueFrom } from 'rxjs'
export function getServerInfo(patch: PatchDB<DataModel>): Promise<ServerInfo> {
return firstValueFrom(patch.watch$('server-info').pipe(filter(Boolean)))
return firstValueFrom(patch.watch$('server-info'))
}