feat(marketplace): add separate package and move some entities in it (#1283)

* feat(marketplace): add separate package and move some entities in it

* feat(marketplace): refactor release notes and list

* feat(marketplace): refactor showing a package

* chore: fix install progress

* chore: fix angular.json

* chore: properly share stream
This commit is contained in:
Alex Inkin
2022-03-15 20:11:54 +03:00
committed by GitHub
parent 72cb451f5a
commit 8942c29229
115 changed files with 1848 additions and 1457 deletions

View File

@@ -38,8 +38,9 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/shared/styles/shared.scss",
"projects/ui/src/styles.scss"
],
"scripts": []
@@ -157,8 +158,8 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/setup-wizard/src/styles.scss"
],
"scripts": []
@@ -276,8 +277,8 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/diagnostic-ui/src/styles.scss"
],
"scripts": []
@@ -376,6 +377,29 @@
}
}
},
"marketplace": {
"projectType": "library",
"root": "projects/marketplace",
"sourceRoot": "projects/marketplace/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/marketplace/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/marketplace/tsconfig.prod.json"
},
"development": {
"tsConfig": "projects/marketplace/tsconfig.json"
}
},
"defaultConfiguration": "production"
}
}
},
"shared": {
"projectType": "library",
"root": "projects/shared",

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/marketplace",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "@start9labs/marketplace",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^13.2.0",
"@angular/core": "^13.2.0",
"@start9labs/shared": "^0.0.1",
"fuse.js": "^6.4.6"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,65 @@
import { Pipe, PipeTransform } from '@angular/core'
import Fuse from 'fuse.js/dist/fuse.min.js'
import { LocalPkg } from '../types/local-pkg'
import { MarketplacePkg } from '../types/marketplace-pkg'
const defaultOps = {
isCaseSensitive: false,
includeScore: true,
shouldSort: true,
includeMatches: false,
findAllMatches: false,
minMatchCharLength: 1,
location: 0,
threshold: 0.6,
distance: 100,
useExtendedSearch: false,
ignoreLocation: false,
ignoreFieldNorm: false,
keys: [
'manifest.id',
'manifest.title',
'manifest.description.short',
'manifest.description.long',
],
}
@Pipe({
name: 'filterPackages',
})
export class FilterPackagesPipe implements PipeTransform {
transform(
packages: MarketplacePkg[] | null,
local: Record<string, LocalPkg>,
query: string,
category: string,
): MarketplacePkg[] | null {
if (!packages) {
return null
}
if (query) {
const fuse = new Fuse(packages, defaultOps)
return fuse.search(query).map(p => p.item)
}
if (category === 'updates') {
return packages.filter(
({ manifest }) =>
local[manifest.id] &&
manifest.version !== local[manifest.id].manifest.version,
)
}
const pkgsToSort = packages.filter(
p => category === 'all' || p.categories.includes(category),
)
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
return fuse
.search(category !== 'all' ? category || '' : 'bit')
.map(p => p.item)
}
}

View File

@@ -1,9 +1,10 @@
import { NgModule } from '@angular/core'
import { InstallProgressPipe } from './install-progress.pipe'
import { TrustPipe } from './trust.pipe'
import { FilterPackagesPipe } from './filter-packages.pipe'
@NgModule({
declarations: [InstallProgressPipe, TrustPipe],
exports: [InstallProgressPipe, TrustPipe],
declarations: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
exports: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
})
export class MarketplacePipesModule {}

View File

@@ -0,0 +1,17 @@
/*
* Public API Surface of @start9labs/marketplace
*/
export * from './pipes/install-progress.pipe'
export * from './pipes/trust.pipe'
export * from './pipes/marketplace-pipes.module'
export * from './services/marketplace.service'
export * from './types/local-pkg'
export * from './types/marketplace'
export * from './types/marketplace-data'
export * from './types/marketplace-manifest'
export * from './types/marketplace-pkg'
export * from './utils/spread-progress'

View File

@@ -0,0 +1,17 @@
import { Observable } from 'rxjs'
import { MarketplacePkg } from '../types/marketplace-pkg'
import { Marketplace } from '../types/marketplace'
export abstract class AbstractMarketplaceService {
abstract install(id: string, version?: string): Observable<unknown>
abstract getMarketplace(): Observable<Marketplace>
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
abstract getCategories(): Observable<string[]>
abstract getPackages(): Observable<MarketplacePkg[]>
abstract getPackage(id: string, version: string): Observable<MarketplacePkg>
}

View File

@@ -0,0 +1,8 @@
import { PackageState } from '@start9labs/shared'
import { MarketplaceManifest } from './marketplace-manifest'
export interface LocalPkg {
state: PackageState
manifest: MarketplaceManifest
}

View File

@@ -0,0 +1,4 @@
export interface MarketplaceData {
categories: string[]
name: string
}

View File

@@ -0,0 +1,25 @@
import { Url } from '@start9labs/shared'
export interface MarketplaceManifest {
id: string
title: string
version: string
description: {
short: string
long: string
}
'release-notes': string
license: string // name
'wrapper-repo': Url
'upstream-repo': Url
'support-site': Url
'marketing-site': Url
'donation-url': Url | null
alerts: {
install: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
}
}

View File

@@ -0,0 +1,17 @@
import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from './marketplace-manifest'
export interface MarketplacePkg {
icon: Url
license: Url
instructions: Url
manifest: MarketplaceManifest
categories: string[]
versions: string[]
'dependency-metadata': {
[id: string]: {
title: string
icon: Url
}
}
}

View File

@@ -0,0 +1,4 @@
export interface Marketplace {
url: string
name: string
}

View File

@@ -0,0 +1,5 @@
import { LocalPkg } from '../types/local-pkg'
export function spreadProgress(pkg: LocalPkg) {
pkg['install-progress'] = { ...pkg['install-progress'] }
}

View File

@@ -0,0 +1,13 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from './services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from './services/state.service'
@Component({
@@ -30,7 +30,7 @@ export class AppComponent {
await this.navCtrl.navigateForward(`/recover`)
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
}
}
}

View File

@@ -10,7 +10,7 @@ import {
DiskInfo,
DiskRecoverySource,
} from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
@@ -74,7 +74,7 @@ export class EmbassyPage {
await alert.present()
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
this.loading = false
}
@@ -142,9 +142,9 @@ export class EmbassyPage {
await this.navCtrl.navigateForward(`/success`)
}
} catch (e) {
this.errorToastService.present(
`${e.message}\n\nRestart Embassy to try again.`,
)
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
console.error(e)
} finally {
loader.dismiss()

View File

@@ -8,7 +8,7 @@ import {
} from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@@ -120,7 +120,7 @@ export class RecoverPage {
this.hasShownGuidAlert = true
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
this.loading = false
}
@@ -206,7 +206,7 @@ export class RecoverPage {
await this.stateService.importDrive(guid)
await this.navCtrl.navigateForward(`/success`)
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
loader.dismiss()
}

View File

@@ -1,6 +1,6 @@
import { Component, EventEmitter, Output } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
@Component({

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core'
import { ToastController } from '@ionic/angular'
@Injectable({
providedIn: 'root',
})
export class ErrorToastService {
private toast: HTMLIonToastElement
constructor (
private readonly toastCtrl: ToastController,
) { }
async present (message: string): Promise<void> {
if (this.toast) return
this.toast = await this.toastCtrl.create({
header: 'Error',
message,
duration: 0,
position: 'top',
cssClass: 'error-toast',
animated: true,
buttons: [
{
side: 'end',
icon: 'close',
handler: () => {
this.dismiss()
},
},
],
})
await this.toast.present()
}
async dismiss (): Promise<void> {
if (this.toast) {
await this.toast.dismiss()
this.toast = undefined
}
}
}

View File

@@ -5,8 +5,7 @@ import {
CifsRecoverySource,
DiskRecoverySource,
} from './api/api.service'
import { ErrorToastService } from './error-toast.service'
import { pauseFor } from '@start9labs/shared'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
@Injectable({
providedIn: 'root',
@@ -51,9 +50,9 @@ export class StateService {
try {
progress = await this.apiService.getRecoveryStatus()
} catch (e) {
this.errorToastService.present(
`${e.message}\n\nRestart Embassy to try again.`,
)
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
}
if (progress) {
this.dataTransferProgress = {

View File

@@ -1,6 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/shared",
"assets": ["styles"],
"lib": {
"entryFile": "src/public-api.ts"
}

View File

@@ -4,6 +4,7 @@
"peerDependencies": {
"@angular/common": "^13.2.0",
"@angular/core": "^13.2.0",
"@ionic/angular": "^6.0.3",
"@start9labs/emver": "^0.1.5"
},
"dependencies": {

View File

@@ -0,0 +1,11 @@
import { Directive, ElementRef, Inject } from '@angular/core'
@Directive({
selector: '[elementRef]',
exportAs: 'elementRef',
})
export class ElementDirective<T extends Element> extends ElementRef<T> {
constructor(@Inject(ElementRef) { nativeElement }: ElementRef<T>) {
super(nativeElement)
}
}

View File

@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core'
import { ElementDirective } from './element.directive'
@NgModule({
declarations: [ElementDirective],
exports: [ElementDirective],
})
export class ElementModule {}

View File

@@ -5,6 +5,9 @@
export * from './components/text-spinner/text-spinner.component.module'
export * from './components/text-spinner/text-spinner.component'
export * from './directives/element/element.directive'
export * from './directives/element/element.module'
export * from './pipes/emver/emver.module'
export * from './pipes/emver/emver.pipe'
export * from './pipes/markdown/markdown.module'
@@ -17,11 +20,13 @@ export * from './pipes/unit-conversion/unit-conversion.pipe'
export * from './services/destroy.service'
export * from './services/emver.service'
export * from './services/error-toast.service'
export * from './types/dependent-info'
export * from './types/install-progress'
export * from './types/package-state'
export * from './types/progress-data'
export * from './types/url'
export * from './types/workspace-config'
export * from './util/misc.util'

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'
import { IonicSafeString, ToastController } from '@ionic/angular'
import { RequestError } from './http.service'
@Injectable({
providedIn: 'root',
@@ -10,7 +9,7 @@ export class ErrorToastService {
constructor(private readonly toastCtrl: ToastController) {}
async present(e: RequestError, link?: string): Promise<void> {
async present(e: { message: string }, link?: string): Promise<void> {
console.error(e)
if (this.toast) return
@@ -43,18 +42,16 @@ export class ErrorToastService {
}
export function getErrorMessage(
e: RequestError,
{ message }: { message: string },
link?: string,
): string | IonicSafeString {
let message: string | IonicSafeString = e.message
if (!message) {
message = 'Unknown Error.'
link = 'https://start9.com/latest/support/FAQ'
}
if (link) {
message = new IonicSafeString(
return new IonicSafeString(
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
)
}

View File

@@ -0,0 +1 @@
export type Url = string

View File

@@ -56,8 +56,7 @@ export function isObject(val: any): boolean {
}
export function isEmptyObject(obj: object): boolean {
if (obj === undefined) return true
return !Object.keys(obj).length
return obj === undefined || !Object.keys(obj).length
}
export function pauseFor(ms: number): Promise<void> {

View File

@@ -24,53 +24,3 @@
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
ion-input {
caret-color: gray;
}
.item-interactive {
--highlight-background: var(--ion-color-light) !important;
}
ion-modal::part(content) {
position: absolute;
height: 90% !important;
top: 5%;
width: 90% !important;
left: 5%;
display: block;
border-radius: 6px;
border: 2px solid rgba(255,255,255,.03);
box-shadow: 0 0 70px 70px black;
}
@media (min-width:1000px) {
ion-modal::part(content) {
position: absolute;
height: 80% !important;
top: 10%;
width: 50% !important;
left: 25%;
}
}
.alertlike-modal {
&::part(content) {
max-height: 380px !important;
top: 25% !important;
width: 90% !important;
left: 5% !important;
--box-shadow: none !important;
}
}
@media (min-width:1000px) {
.alertlike-modal {
&::part(content) {
width: 60% !important;
left: 20% !important;
}
}
}

View File

@@ -0,0 +1,85 @@
ion-input {
caret-color: gray;
}
ion-modal::part(content) {
position: absolute;
height: 90% !important;
top: 5%;
width: 90% !important;
left: 5%;
display: block;
border-radius: 6px;
border: 2px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 0 70px 70px black;
}
@media (min-width: 1000px) {
ion-modal::part(content) {
position: absolute;
height: 80% !important;
top: 10%;
width: 50% !important;
left: 25%;
}
}
.alertlike-modal {
&::part(content) {
max-height: 380px !important;
top: 25% !important;
width: 90% !important;
left: 5% !important;
--box-shadow: none !important;
}
}
@media (min-width: 1000px) {
.alertlike-modal {
&::part(content) {
width: 60% !important;
left: 20% !important;
}
}
}
.item-interactive {
--highlight-background: var(--ion-color-light) !important;
}
.hidden-scrollbar {
overflow: auto;
white-space: nowrap;
height: 60px;
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
width: 0;
height: 0;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.divider {
background: linear-gradient(
90deg,
var(--ion-color-light) 0,
var(--ion-color-dark) 50%,
var(--ion-color-light) 100%
);
height: 1px;
}
.loading-dots:after {
content: '...';
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis-dot 1s infinite 0.3s;
animation-fill-mode: forwards;
width: 1em;
}

View File

@@ -1,14 +1,5 @@
import { Component, HostListener, NgZone } from '@angular/core'
import { Storage } from '@ionic/storage-angular'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy-api.service'
import { Component, HostListener, Inject, NgZone } from '@angular/core'
import { Router, RoutesRecognized } from '@angular/router'
import {
debounceTime,
distinctUntilChanged,
filter,
take,
} from 'rxjs/operators'
import {
AlertController,
IonicSafeString,
@@ -16,21 +7,33 @@ import {
ModalController,
ToastController,
} from '@ionic/angular'
import { SplitPaneTracker } from './services/split-pane.service'
import { ToastButton } from '@ionic/core'
import { Storage } from '@ionic/storage-angular'
import {
debounce,
isEmptyObject,
Emver,
ErrorToastService,
} from '@start9labs/shared'
import { Subscription } from 'rxjs'
import {
debounceTime,
distinctUntilChanged,
filter,
take,
} from 'rxjs/operators'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy-api.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { PatchDbService } from './services/patch-db/patch-db.service'
import {
ConnectionFailure,
ConnectionService,
} from './services/connection.service'
import { ConfigService } from './services/config.service'
import { debounce, isEmptyObject, Emver } from '@start9labs/shared'
import { ServerStatus, UIData } from 'src/app/services/patch-db/data-model'
import { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs'
import { LocalStorageService } from './services/local-storage.service'
import { EOSService } from './services/eos.service'
import { MarketplaceService } from './pages/marketplace-routes/marketplace.service'
import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
import { SnakePage } from './modals/snake/snake.page'
@@ -128,7 +131,6 @@ export class AppComponent {
private readonly emver: Emver,
private readonly connectionService: ConnectionService,
private readonly modalCtrl: ModalController,
private readonly marketplaceService: MarketplaceService,
private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService,
private readonly config: ConfigService,
@@ -190,8 +192,6 @@ export class AppComponent {
this.watchVersion(),
// watch unread notification count to display toast
this.watchNotifications(),
// watch marketplace URL for changes
this.marketplaceService.init(),
])
})
// UNVERIFIED

View File

@@ -23,6 +23,7 @@ import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared'
import { MarketplaceModule } from './marketplace.module'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -48,6 +49,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
GenericInputComponentModule,
MonacoEditorModule,
SharedPipesModule,
MarketplaceModule,
],
providers: [
FormBuilder,
@@ -79,4 +81,4 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule { }
export class AppModule {}

View File

@@ -14,7 +14,7 @@ import {
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
@Component({

View File

@@ -1,14 +1,13 @@
import { Injectable } from '@angular/core'
import { IonicSafeString } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import {
BackupTarget,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { Emver } from '@start9labs/shared'
import { getErrorMessage, Emver } from '@start9labs/shared'
@Injectable({
providedIn: 'root',

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'
import { Inject, Injectable } from '@angular/core'
import { exists } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Breakages } from 'src/app/services/api/api.types'
import { exists } from '@start9labs/shared'
import { ApiService } from '../../services/api/embassy-api.service'
import {
InstallWizardComponent,
@@ -9,13 +10,14 @@ import {
TopbarParams,
} from './install-wizard.component'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Injectable({ providedIn: 'root' })
export class WizardBaker {
constructor(
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}
@@ -77,10 +79,12 @@ export class WizardBaker {
verb: 'beginning update for',
title,
executeAction: () =>
this.marketplaceService.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
}),
this.marketplaceService
.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
})
.toPromise(),
},
},
bottomBar: {
@@ -202,10 +206,12 @@ export class WizardBaker {
verb: 'beginning downgrade for',
title,
executeAction: () =>
this.marketplaceService.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
}),
this.marketplaceService
.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
})
.toPromise(),
},
},
bottomBar: {

View File

@@ -1,6 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonContent } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { RR } from 'src/app/services/api/api.types'
var Convert = require('ansi-to-html')
var convert = new Convert({
@@ -14,7 +14,11 @@ var convert = new Convert({
})
export class LogsPage {
@ViewChild(IonContent) private content: IonContent
@Input() fetchLogs: (params: { before_flag?: boolean, limit?: number, cursor?: string }) => Promise<RR.LogsRes>
@Input() fetchLogs: (params: {
before_flag?: boolean
limit?: number
cursor?: string
}) => Promise<RR.LogsRes>
loading = true
loadingMore = false
logs: string
@@ -25,15 +29,13 @@ export class LogsPage {
scrollToBottomButton = false
isOnBottom = true
constructor (
private readonly errToast: ErrorToastService,
) { }
constructor(private readonly errToast: ErrorToastService) {}
ngOnInit () {
ngOnInit() {
this.getLogs()
}
async fetch (isBefore: boolean = true) {
async fetch(isBefore: boolean = true) {
try {
const cursor = isBefore ? this.startCursor : this.endCursor
const logsRes = await this.fetchLogs({
@@ -57,7 +59,7 @@ export class LogsPage {
}
}
async getLogs () {
async getLogs() {
try {
// get logs
const logs = await this.fetch()
@@ -65,47 +67,60 @@ export class LogsPage {
const container = document.getElementById('container')
const beforeContainerHeight = container.scrollHeight
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
const newLogs = document
.getElementById('template')
.cloneNode(true) as HTMLElement
newLogs.innerHTML =
logs
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
.join('\n') + (logs.length ? '\n' : '')
container.prepend(newLogs)
const afterContainerHeight = container.scrollHeight
// scroll down
scrollBy(0, afterContainerHeight - beforeContainerHeight)
this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
this.content.scrollToPoint(
0,
afterContainerHeight - beforeContainerHeight,
)
if (logs.length < this.limit) {
this.needInfinite = false
}
} catch (e) { }
} catch (e) {}
}
async loadMore () {
async loadMore() {
try {
this.loadingMore = true
const logs = await this.fetch(false)
if (!logs.length) return this.loadingMore = false
if (!logs.length) return (this.loadingMore = false)
const container = document.getElementById('container')
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
const newLogs = document
.getElementById('template')
.cloneNode(true) as HTMLElement
newLogs.innerHTML =
logs
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
.join('\n') + (logs.length ? '\n' : '')
container.append(newLogs)
this.loadingMore = false
this.scrollEvent()
} catch (e) { }
} catch (e) {}
}
scrollEvent () {
scrollEvent() {
const buttonDiv = document.getElementById('button-div')
this.isOnBottom = buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
this.isOnBottom =
buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
}
scrollToBottom () {
scrollToBottom() {
this.content.scrollToBottom(500)
}
async loadData (e: any): Promise<void> {
async loadData(e: any): Promise<void> {
await this.getLogs()
e.target.complete()
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@NgModule({
providers: [
{
provide: AbstractMarketplaceService,
useClass: MarketplaceService,
},
],
})
export class MarketplaceModule {}

View File

@@ -7,16 +7,18 @@ import {
IonicSafeString,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DependentInfo, isEmptyObject, isObject } from '@start9labs/shared'
import {
ErrorToastService,
getErrorMessage,
DependentInfo,
isEmptyObject,
isObject,
} from '@start9labs/shared'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import {
ErrorToastService,
getErrorMessage,
} from 'src/app/services/error-toast.service'
import { FormGroup } from '@angular/forms'
import {
convertValuesRecursive,

View File

@@ -7,8 +7,7 @@ import {
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { Emver } from '@start9labs/shared'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { getErrorMessage, Emver } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({

View File

@@ -1,6 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core'
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { getErrorMessage } from '@start9labs/shared'
@Component({
selector: 'generic-input',
@@ -14,11 +14,9 @@ export class GenericInputComponent {
unmasked = false
error: string | IonicSafeString
constructor (
private readonly modalCtrl: ModalController,
) { }
constructor(private readonly modalCtrl: ModalController) {}
ngOnInit () {
ngOnInit() {
const defaultOptions: Partial<GenericInputOptions> = {
buttonText: 'Submit',
placeholder: 'Enter value',
@@ -34,19 +32,19 @@ export class GenericInputComponent {
this.value = this.options.initialValue
}
ngAfterViewInit () {
ngAfterViewInit() {
setTimeout(() => this.elem.setFocus(), 400)
}
toggleMask () {
toggleMask() {
this.unmasked = !this.unmasked
}
cancel () {
cancel() {
this.modalCtrl.dismiss()
}
async submit () {
async submit() {
const value = this.value.trim()
if (!value && !this.options.nullable) return

View File

@@ -1,8 +1,7 @@
import { Component, Input } from '@angular/core'
import { ModalController, IonicSafeString } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { pauseFor } from '../../../../../shared/src/util/misc.util'
import { getErrorMessage, pauseFor } from '@start9labs/shared'
@Component({
selector: 'markdown',

View File

@@ -18,8 +18,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { isEmptyObject } from '@start9labs/shared'
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
@Component({

View File

@@ -2,16 +2,18 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Inject,
Input,
Output,
} from '@angular/core'
import { AlertController } from '@ionic/angular'
import { ErrorToastService } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
import { RecoveredInfo } from 'src/app/util/parse-data-model'
import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Component({
selector: 'app-list-rec',
@@ -33,17 +35,14 @@ export class AppListRecComponent {
readonly installing$ = this.install$.pipe(
switchMap(({ id, version }) =>
// Mapping each installation to API request
from(
this.marketplaceService.installPackage({
id,
'version-spec': `>=${version}`,
'version-priority': 'min',
}),
).pipe(
// Mapping operation to true/false loading indication
loading(this.errToast),
),
this.marketplaceService.installPackage({
id,
'version-spec': `>=${version}`,
'version-priority': 'min',
}),
),
// Mapping operation to true/false loading indication
loading(this.errToast),
)
// Deleting package
@@ -66,6 +65,7 @@ export class AppListRecComponent {
private readonly api: ApiService,
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}

View File

@@ -4,9 +4,8 @@ import { IonContent } from '@ionic/angular'
import { Subscription } from 'rxjs'
import { Metric } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MainStatus } from 'src/app/services/patch-db/data-model'
import { pauseFor } from '@start9labs/shared'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
@Component({
selector: 'app-metrics',

View File

@@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { getValueByPointer } from 'fast-json-patch'
@Component({

View File

@@ -10,14 +10,17 @@ import {
PackageDataEntry,
Status,
} from 'src/app/services/patch-db/data-model'
import { isEmptyObject, PackageState } from '@start9labs/shared'
import {
isEmptyObject,
ErrorToastService,
PackageState,
} from '@start9labs/shared'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import {
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'

View File

@@ -7,7 +7,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { debounce } from '../../../../../../shared/src/util/misc.util'
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
import { ErrorToastService } from '../../../services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
@Component({
selector: 'dev-config',

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'
import { ModalController } from '@ionic/angular'
import { take } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { debounce } from '../../../../../../shared/src/util/misc.util'
import { MarkdownPage } from '../../../modals/markdown/markdown.page'

View File

@@ -17,9 +17,8 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
import * as yaml from 'js-yaml'
import { v4 } from 'uuid'
import { DevData } from 'src/app/services/patch-db/data-model'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ActivatedRoute } from '@angular/router'
import { DestroyService } from '@start9labs/shared'
import { DestroyService, ErrorToastService } from '@start9labs/shared'
import { takeUntil } from 'rxjs/operators'
@Component({

View File

@@ -5,7 +5,7 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { BasicInfo, getBasicInfoSpec } from './form-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { takeUntil } from 'rxjs/operators'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'

View File

@@ -1,35 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="'/marketplace/' + pkgId"></ion-back-button>
</ion-buttons>
<ion-title>Release Notes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<text-spinner *ngIf="loading; else loaded" text="Loading Release Notes"></text-spinner>
<ng-template #loaded>
<div style="margin: 0px;" *ngFor="let note of marketplaceService.releaseNotes[pkgId] | keyvalue : asIsOrder">
<ion-button
(click)="setSelected(note.key)"
expand="full" color="light"
style="height: 50px; margin: 1px;"
[class]="selected === note.key ? 'ion-activated' : ''"
>
<p style="position: absolute; left: 10px;">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
[id]="note.key"
[ngStyle]="{
'max-height': selected === note.key ? getDocSize(note.key) : '0px',
'transition': 'max-height 0.2s ease-out'
}"
class="panel"
color="light" >
<ion-text id='release-notes' [innerHTML]="note.value | markdown"></ion-text>
</ion-card>
</div>
</ng-template>
</ion-content>

View File

@@ -1,8 +0,0 @@
.panel {
margin: 0px;
padding: 0px 24px;
}
.active {
border: 5px solid #4d4d4d;
}

View File

@@ -1,62 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { IonContent } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MarketplaceService } from '../marketplace.service'
@Component({
selector: 'app-release-notes',
templateUrl: './app-release-notes.page.html',
styleUrls: ['./app-release-notes.page.scss'],
})
export class AppReleaseNotes {
@ViewChild(IonContent) content: IonContent
selected: string
pkgId: string
loading = true
constructor(
private readonly route: ActivatedRoute,
public marketplaceService: MarketplaceService,
public errToast: ErrorToastService,
) {}
async ngOnInit() {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
try {
const promises = []
if (!this.marketplaceService.releaseNotes[this.pkgId]) {
promises.push(this.marketplaceService.cacheReleaseNotes(this.pkgId))
}
if (!this.marketplaceService.pkgs.length) {
promises.push(this.marketplaceService.load())
}
await Promise.all(promises)
} catch (e) {
this.errToast.present(e)
} finally {
this.loading = false
}
}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
setSelected(selected: string) {
if (this.selected === selected) {
this.selected = null
} else {
this.selected = selected
}
}
getDocSize(selected: string) {
const element = document.getElementById(selected)
return `${element.scrollHeight}px`
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -0,0 +1,62 @@
<h1 class="heading ion-text-center">{{ name }}</h1>
<ion-grid class="grid">
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
<ion-searchbar
enterkeyhint="search"
color="dark"
debounce="250"
[(ngModel)]="query"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>
<ng-container *ngIf="pkgs && categories; else loading">
<div class="hidden-scrollbar ion-text-center">
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="isSelected(cat)"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>
</div>
<div class="divider"></div>
<ion-grid *ngIf="pkgs | filterPackages: localPkgs:query:category 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">
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trust" />
</ion-thumbnail>
<ion-label>
<h2 class="pkg-title">
{{ pkg.manifest.title }}
</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<marketplace-status
class="status"
[pkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
<ng-template #loading>
<marketplace-list-skeleton></marketplace-list-skeleton>
</ng-template>

View File

@@ -0,0 +1,46 @@
.heading {
font-family: 'Montserrat', sans-serif;
font-size: 42px;
margin: 32px 0;
}
.divider {
margin: 24px;
}
.ion-padding {
text-align: center;
}
.grid {
padding-bottom: 32px;
}
.pkg-title {
font-family: 'Montserrat', sans-serif;
font-weight: bold;
}
.eos-item {
--border-style: none;
--background: linear-gradient(
45deg,
var(--ion-color-dark) -380%,
var(--ion-color-medium) 100%
);
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
}
}
.status {
font-size: 14px;
}

View File

@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { LocalPkg, MarketplacePkg } from '@start9labs/marketplace'
@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, LocalPkg> = {}
@Input()
categories: Set<string> | null = null
@Input()
name = ''
category = 'featured'
query = ''
isSelected(category: string) {
return category === this.category && !this.query
}
switchCategory(category: string): void {
this.category = category
this.query = ''
}
}

View File

@@ -0,0 +1,7 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-list-header',
templateUrl: 'marketplace-list-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListHeaderComponent {}

View File

@@ -0,0 +1,38 @@
<div class="hidden-scrollbar ion-text-center">
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
<ion-skeleton-text
animated
style="width: 80px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
</div>
<div class="divider" style="margin: 24px 0"></div>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text
style="border-radius: 100%"
animated
></ion-skeleton-text>
</ion-thumbnail>
<ion-label>
<ion-skeleton-text
animated
style="width: 150px; height: 18px; margin-bottom: 8px"
></ion-skeleton-text>
<ion-skeleton-text animated style="width: 400px"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 100px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-list-skeleton',
templateUrl: 'marketplace-list-skeleton.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListSkeletonComponent {}

View File

@@ -8,9 +8,14 @@ import {
EmverPipesModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceListPage } from './marketplace-list.page'
import { MarketplaceListHeaderComponent } from './marketplace-list-header/marketplace-list-header.component'
import { MarketplaceListSkeletonComponent } from './marketplace-list-skeleton/marketplace-list-skeleton.component'
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
const routes: Routes = [
{
@@ -29,8 +34,20 @@ const routes: Routes = [
SharedPipesModule,
EmverPipesModule,
MarketplacePipesModule,
MarketplaceStatusModule,
BadgeMenuComponentModule,
],
declarations: [MarketplaceListPage],
declarations: [
MarketplaceListPage,
MarketplaceListHeaderComponent,
MarketplaceListContentComponent,
MarketplaceListSkeletonComponent,
],
exports: [
MarketplaceListPage,
MarketplaceListHeaderComponent,
MarketplaceListContentComponent,
MarketplaceListSkeletonComponent,
],
})
export class MarketplaceListPageModule {}

View File

@@ -1,175 +1,15 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<marketplace-list-header></marketplace-list-header>
<ion-content class="ion-padding">
<!-- loading -->
<text-spinner
*ngIf="!patch.loaded else data"
text="Connecting to Embassy"
></text-spinner>
<marketplace-list-content
*ngIf="loaded else loading"
[localPkgs]="localPkgs$ | async"
[pkgs]="pkgs$ | async"
[categories]="categories$ | async"
[name]="name$ | async"
></marketplace-list-content>
<!-- not loading -->
<ng-template #data>
<h1
style="font-family: 'Montserrat'; font-size: 42px; margin: 32px 0"
class="ion-text-center"
>
{{ marketplaceService.marketplace.name }}
</h1>
<ion-grid style="padding-bottom: 32px">
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
<ion-searchbar
enterkeyhint="search"
color="dark"
debounce="250"
[(ngModel)]="query"
(ionChange)="search()"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>
<!-- loading -->
<ng-container *ngIf="loading; else pageLoaded">
<div class="scrollable ion-text-center">
<ion-button
*ngFor="let cat of ['', '', '', '', '', '', '']"
fill="clear"
>
<ion-skeleton-text
animated
style="width: 80px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
</div>
<div class="divider" style="margin: 24px 0"></div>
</ng-container>
<!-- loaded -->
<ng-template #pageLoaded>
<div class="scrollable ion-text-center">
<ion-button
*ngFor="let cat of categories"
fill="clear"
[class]="cat === category ? 'selected' : 'dim'"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>
</div>
<div class="divider" style="margin: 24px"></div>
</ng-template>
<!-- loading -->
<ng-container *ngIf="loading; else pkgsLoaded">
<ion-grid>
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text
style="border-radius: 100%"
animated
></ion-skeleton-text>
</ion-thumbnail>
<ion-label>
<ion-skeleton-text
animated
style="width: 150px; height: 18px; margin-bottom: 8px"
></ion-skeleton-text>
<ion-skeleton-text
animated
style="width: 400px"
></ion-skeleton-text>
<ion-skeleton-text
animated
style="width: 100px"
></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
<!-- packages loaded -->
<ng-template #pkgsLoaded>
<div
class="ion-padding"
*ngIf="!pkgs.length && category ==='updates'"
style="text-align: center"
>
<h1>All services are up to date!</h1>
</div>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeSm="12" sizeMd="6">
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img [src]="('data:image/png;base64,' + pkg.icon) | trust" />
</ion-thumbnail>
<ion-label>
<h2 style="font-family: 'Montserrat'; font-weight: bold">
{{ pkg.manifest.title }}
</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<ng-container
*ngIf="localPkgs[pkg.manifest.id] as localPkg; else none"
>
<p *ngIf="localPkg.state === PackageState.Installed">
<ion-text
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0"
color="success"
>Installed</ion-text
>
<ion-text
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1"
color="warning"
>Update Available</ion-text
>
</p>
<p
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
>
<ion-text
color="primary"
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
>
Installing
<span class="loading-dots"></span>{{ progress }}
</ion-text>
</p>
<p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="danger">
Removing
<span class="loading-dots"></span>
</ion-text>
</p>
</ng-container>
<ng-template #none>
<p>Not Installed</p>
</ng-template>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
<ng-template #loading>
<text-spinner text="Connecting to Embassy"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -1,30 +0,0 @@
.scrollable {
overflow: auto;
white-space: nowrap;
// background-color: var(--ion-color-light);
height: 60px;
/* Hide scrollbar for Chrome, Safari and Opera */
::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.eos-item {
--border-style: none;
--background: linear-gradient(45deg, var(--ion-color-dark) -380%, var(--ion-color-medium) 100%)
}
.selected {
font-weight: bold;
font-size: 17px;
}
.dim {
font-weight: 300;
color: var(--ion-color-dark-shade);
}

View File

@@ -1,146 +1,53 @@
import { Component, ViewChild } from '@angular/core'
import { MarketplacePkg } from 'src/app/services/api/api.types'
import { IonContent } from '@ionic/angular'
import { Subscription } from 'rxjs'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MarketplaceService } from '../marketplace.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import Fuse from 'fuse.js/dist/fuse.min.js'
import { exists, isEmptyObject, PackageState } from '@start9labs/shared'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { filter, first } from 'rxjs/operators'
import { Component } from '@angular/core'
import { defer, Observable } from 'rxjs'
import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators'
import { exists, isEmptyObject } from '@start9labs/shared'
import {
AbstractMarketplaceService,
LocalPkg,
MarketplacePkg,
spreadProgress,
} from '@start9labs/marketplace'
const defaultOps = {
isCaseSensitive: false,
includeScore: true,
shouldSort: true,
includeMatches: false,
findAllMatches: false,
minMatchCharLength: 1,
location: 0,
threshold: 0.6,
distance: 100,
useExtendedSearch: false,
ignoreLocation: false,
ignoreFieldNorm: false,
keys: [
'manifest.id',
'manifest.title',
'manifest.description.short',
'manifest.description.long',
],
}
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'marketplace-list',
templateUrl: './marketplace-list.page.html',
styleUrls: ['./marketplace-list.page.scss'],
})
export class MarketplaceListPage {
PackageState = PackageState
readonly localPkgs$: Observable<Record<string, LocalPkg>> = defer(() =>
this.patch.watch$('package-data'),
).pipe(
filter(data => exists(data) && !isEmptyObject(data)),
tap(pkgs => Object.values(pkgs).forEach(spreadProgress)),
startWith({}),
)
@ViewChild(IonContent) content: IonContent
readonly categories$ = this.marketplaceService
.getCategories()
.pipe(
map(categories => new Set(['featured', 'updates', ...categories, 'all'])),
)
pkgs: MarketplacePkg[] = []
categories: string[]
localPkgs: Record<string, PackageDataEntry> = {}
category = 'featured'
query: string
loading = true
readonly pkgs$: Observable<MarketplacePkg[]> = defer(() =>
this.patch.watch$('server-info'),
).pipe(
filter(data => exists(data) && !isEmptyObject(data)),
first(),
switchMapTo(this.marketplaceService.getPackages()),
)
subs: Subscription[] = []
readonly name$: Observable<string> = this.marketplaceService
.getMarketplace()
.pipe(map(({ name }) => name))
constructor(
private readonly errToast: ErrorToastService,
public readonly patch: PatchDbService,
public readonly marketplaceService: MarketplaceService,
private readonly patch: PatchDbService,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
async ngOnInit() {
this.subs = [
this.patch
.watch$('package-data')
.pipe(filter(data => exists(data) && !isEmptyObject(data)))
.subscribe(pkgs => {
this.localPkgs = pkgs
Object.values(this.localPkgs).forEach(pkg => {
pkg['install-progress'] = { ...pkg['install-progress'] }
})
}),
]
this.patch
.watch$('server-info')
.pipe(
filter(data => exists(data) && !isEmptyObject(data)),
first(),
)
.subscribe(async _ => {
try {
if (!this.marketplaceService.pkgs.length) {
await this.marketplaceService.load()
}
// category should start as first item in array
// remove here then add at beginning
const filterdCategories =
this.marketplaceService.data.categories.filter(
cat => this.category !== cat,
)
this.categories = [this.category, 'updates']
.concat(filterdCategories)
.concat(['all'])
this.filterPkgs()
} catch (e) {
this.errToast.present(e)
} finally {
this.loading = false
}
})
}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
}
search(): void {
if (this.query) {
this.category = undefined
}
this.filterPkgs()
}
switchCategory(category: string): void {
this.category = category
this.query = undefined
this.filterPkgs()
}
private filterPkgs(): void {
if (this.category === 'updates') {
this.pkgs = this.marketplaceService.pkgs.filter(pkg => {
const { id, version } = pkg.manifest
return (
this.localPkgs[id] && version !== this.localPkgs[id].manifest.version
)
})
} else if (this.query) {
const fuse = new Fuse(this.marketplaceService.pkgs, defaultOps)
this.pkgs = fuse.search(this.query).map(p => p.item)
} else {
const pkgsToSort = this.marketplaceService.pkgs.filter(p => {
return this.category === 'all' || p.categories.includes(this.category)
})
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
this.pkgs = fuse
.search(this.category !== 'all' ? this.category || '' : 'bit')
.map(p => p.item)
}
get loaded(): boolean {
return this.patch.loaded
}
}

View File

@@ -9,15 +9,24 @@ const routes: Routes = [
},
{
path: 'browse',
loadChildren: () => import('./marketplace-list/marketplace-list.module').then(m => m.MarketplaceListPageModule),
loadChildren: () =>
import('./marketplace-list/marketplace-list.module').then(
m => m.MarketplaceListPageModule,
),
},
{
path: ':pkgId',
loadChildren: () => import('./marketplace-show/marketplace-show.module').then(m => m.MarketplaceShowPageModule),
loadChildren: () =>
import('./marketplace-show/marketplace-show.module').then(
m => m.MarketplaceShowPageModule,
),
},
{
path: ':pkgId/notes',
loadChildren: () => import('./app-release-notes/app-release-notes.module').then(m => m.ReleaseNotesModule),
loadChildren: () =>
import('./release-notes/release-notes.module').then(
m => m.ReleaseNotesPageModule,
),
},
]
@@ -25,4 +34,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MarketplaceRoutingModule { }
export class MarketplaceRoutingModule {}

View File

@@ -0,0 +1,23 @@
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
<ion-button routerLink="notes" class="all-notes" fill="clear" color="dark">
All Release Notes
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div
class="release-notes"
[innerHTML]="pkg.manifest['release-notes'] | markdown"
></div>
</ion-label>
</ion-item>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div class="release-notes">{{ pkg.manifest.description.long }}</div>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,9 @@
.all-notes {
position: absolute;
right: 10px;
}
.release-notes {
overflow: auto;
max-height: 120px;
}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
@Component({
selector: 'marketplace-show-about',
templateUrl: 'marketplace-show-about.component.html',
styleUrls: ['marketplace-show-about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowAboutComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,76 @@
<ion-item-divider>Additional Info</ion-item-divider>
<ion-card>
<ion-grid>
<ion-row>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item button detail="false" (click)="presentAlertVersions()">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('license')">
<ion-label>
<h2>License</h2>
<p>{{ pkg.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="pkg.manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ pkg.manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ pkg.manifest['support-site'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</ion-card>

View File

@@ -0,0 +1,71 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
import { AlertController, ModalController } from '@ionic/angular'
import { MarketplacePkg } from '@start9labs/marketplace'
import { displayEmver, Emver } from '@start9labs/shared'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
@Component({
selector: 'marketplace-show-additional',
templateUrl: 'marketplace-show-additional.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowAdditionalComponent {
@Input()
pkg: MarketplacePkg
@Output()
version = new EventEmitter<string>()
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly emver: Emver,
) {}
async presentAlertVersions() {
const alert = await this.alertCtrl.create({
header: 'Versions',
inputs: this.pkg.versions
.sort((a, b) => -1 * this.emver.compare(a, b))
.map(v => ({
name: v, // for CSS
type: 'radio',
label: displayEmver(v), // appearance on screen
value: v, // literal SEM version value
checked: this.pkg.manifest.version === v,
})),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Ok',
handler: (version: string) => this.version.emit(version),
cssClass: 'enter-click',
},
],
})
await alert.present()
}
async presentModalMd(title: string) {
const modal = await this.modalCtrl.create({
componentProps: {
title,
contentUrl: `/marketplace${this.pkg[title]}`,
},
component: MarkdownPage,
})
await modal.present()
}
}

View File

@@ -0,0 +1,33 @@
<ng-container *ngIf="localPkg; else install">
<!-- not installing, updating, or removing -->
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button
*ngIf="(version | compareEmver: pkg.manifest.version) === -1"
expand="block"
(click)="presentModal('update')"
>
Update
</ion-button>
<ion-button
*ngIf="(version | compareEmver: pkg.manifest.version) === 1"
expand="block"
color="warning"
(click)="presentModal('downgrade')"
>
Downgrade
</ion-button>
<ng-container *ngIf="localStorageService.showDevTools$ | async">
<ion-button
*ngIf="(version | compareEmver: pkg.manifest.version) === 0"
expand="block"
(click)="tryInstall()"
>
Reinstall
</ion-button>
</ng-container>
</ng-container>
</ng-container>
<ng-template #install>
<ion-button expand="block" (click)="tryInstall()">Install</ion-button>
</ng-template>

View File

@@ -0,0 +1,92 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import {
AbstractMarketplaceService,
MarketplacePkg,
LocalPkg,
} from '@start9labs/marketplace'
import { pauseFor, PackageState } from '@start9labs/shared'
import { Manifest } from 'src/app/services/patch-db/data-model'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@Component({
selector: 'marketplace-show-controls',
templateUrl: 'marketplace-show-controls.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowControlsComponent {
@Input()
pkg: MarketplacePkg
@Input()
localPkg: LocalPkg
readonly PackageState = PackageState
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly wizardBaker: WizardBaker,
private readonly navCtrl: NavController,
private readonly marketplaceService: AbstractMarketplaceService,
public readonly localStorageService: LocalStorageService,
) {}
get version(): string {
return this.localPkg?.manifest.version || ''
}
async tryInstall() {
const { id, title, version, alerts } = this.pkg.manifest
if (!alerts.install) {
this.marketplaceService.install(id, version).subscribe()
} else {
const alert = await this.alertCtrl.create({
header: title,
subHeader: version,
message: alerts.install,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Install',
handler: () =>
this.marketplaceService.install(id, version).subscribe(),
},
],
})
await alert.present()
}
}
async presentModal(action: 'update' | 'downgrade') {
// TODO: Fix type
const { id, title, version, dependencies, alerts } = this.pkg
.manifest as Manifest
const value = {
id,
title,
version,
serviceRequirements: dependencies,
installAlert: alerts.install,
}
const { cancelled } = await wizardModal(
this.modalCtrl,
action === 'update'
? this.wizardBaker.update(value)
: this.wizardBaker.downgrade(value),
)
if (cancelled) return
await pauseFor(250)
this.navCtrl.back()
}
}

View File

@@ -0,0 +1,32 @@
<ng-container *ngIf="!(dependencies | empty)">
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img alt="" [src]="getImg(dep.key) | trust" />
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { DependencyInfo, Manifest } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-show-dependencies',
templateUrl: 'marketplace-show-dependencies.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowDependenciesComponent {
@Input()
pkg: MarketplacePkg
get dependencies(): DependencyInfo {
// TODO: Fix type
return (this.pkg.manifest as Manifest).dependencies
}
getImg(key: string): string {
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
}
}

View File

@@ -0,0 +1,30 @@
<!-- auto-config -->
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
<ion-label>
<h2 class="heading">
<ion-text class="title">
{{ title }}
</ion-text>
</h2>
<p>
<ion-text color="dark">
{{ dependentInfo.title }} requires an install of {{ title }} satisfying
{{ dependentInfo.version }}.
<br />
<br />
<span
*ngIf="version | satisfiesEmver: dependentInfo.version"
class="text"
>
{{ title }} version {{ version | displayEmver }} is compatible.
</span>
<span
*ngIf="!(version | satisfiesEmver: dependentInfo.version)"
class="text text_error"
>
{{ title }} version {{ version | displayEmver }} is NOT compatible.
</span>
</ion-text>
</p>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,18 @@
.heading {
display: flex;
align-items: center;
}
.title {
margin: 5px;
font-family: 'Montserrat', sans-serif;
font-size: 18px;
}
.text {
font-style: italic;
&_error {
color: var(--ion-color-danger);
}
}

View File

@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { DependentInfo } from '@start9labs/shared'
@Component({
selector: 'marketplace-show-dependent',
templateUrl: 'marketplace-show-dependent.component.html',
styleUrls: ['marketplace-show-dependent.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowDependentComponent {
@Input()
pkg: MarketplacePkg
readonly dependentInfo?: DependentInfo = history.state?.dependentInfo
get title(): string {
return this.pkg?.manifest.title || ''
}
get version(): string {
return this.pkg?.manifest.version || ''
}
}

View File

@@ -0,0 +1,8 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="marketplace"></ion-back-button>
</ion-buttons>
<ion-title>Marketplace Listing</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-show-header',
templateUrl: 'marketplace-show-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowHeaderComponent {}

View File

@@ -2,15 +2,23 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { MarketplaceShowPage } from './marketplace-show.page'
import {
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowDependenciesComponent } from './marketplace-show-dependencies/marketplace-show-dependencies.component'
import { MarketplaceShowAdditionalComponent } from './marketplace-show-additional/marketplace-show-additional.component'
import { MarketplaceShowAboutComponent } from './marketplace-show-about/marketplace-show-about.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
const routes: Routes = [
{
@@ -29,8 +37,26 @@ const routes: Routes = [
EmverPipesModule,
MarkdownPipeModule,
MarketplacePipesModule,
MarketplaceStatusModule,
InstallWizardComponentModule,
],
declarations: [MarketplaceShowPage],
declarations: [
MarketplaceShowPage,
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
MarketplaceShowAboutComponent,
MarketplaceShowDependenciesComponent,
MarketplaceShowAdditionalComponent,
],
exports: [
MarketplaceShowPage,
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
MarketplaceShowAboutComponent,
MarketplaceShowDependenciesComponent,
MarketplaceShowAdditionalComponent,
],
})
export class MarketplaceShowPageModule {}

View File

@@ -1,68 +1,21 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="marketplace"></ion-back-button>
</ion-buttons>
<ion-title>Marketplace Listing</ion-title>
</ion-toolbar>
</ion-header>
<marketplace-show-header></marketplace-show-header>
<ion-content class="ion-padding">
<text-spinner
*ngIf="loading; else loaded"
text="Loading Package"
></text-spinner>
<ng-template #loaded>
<ng-container *ngIf="pkg$ | async as pkg else loading">
<ion-grid>
<ion-row>
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
<div class="header">
<img [src]="('data:image/png;base64,' + pkg.icon) | trust" />
<img alt="" [src]="getIcon(pkg.icon) | trust" />
<div class="header-text">
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
<p class="header-version">
{{ pkg.manifest.version | displayEmver }}
</p>
<div class="header-status">
<!-- no localPkg -->
<p *ngIf="!localPkg; else local">Not Installed</p>
<!-- localPkg -->
<ng-template #local>
<!-- installed -->
<p *ngIf="localPkg.state === PackageState.Installed">
<ion-text
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0"
color="success"
>Installed</ion-text
>
<ion-text
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1"
color="warning"
>Update Available</ion-text
>
</p>
<!-- installing, updating -->
<p
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
>
<ion-text
color="primary"
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
>
Installing
<span class="loading-dots"></span>{{ progress }}
</ion-text>
</p>
<!-- removing -->
<p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="danger">
Removing
<span class="loading-dots"></span>
</ion-text>
</p>
</ng-template>
</div>
<marketplace-status
class="header-status"
[pkg]="localPkg$ | async"
></marketplace-status>
</div>
</div>
</ion-col>
@@ -74,41 +27,13 @@
sizeXs="12"
class="ion-align-self-center"
>
<!-- no localPkg -->
<ion-button *ngIf="!localPkg" expand="block" (click)="tryInstall()">
Install
</ion-button>
<!-- localPkg -->
<ng-container *ngIf="localPkg">
<!-- not installing, updating, or removing -->
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === -1"
expand="block"
(click)="presentModal('update')"
>
Update
</ion-button>
<ion-button
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 0 && (localStorageService.showDevTools$ | async)"
expand="block"
(click)="tryInstall()"
>
Reinstall
</ion-button>
<ion-button
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 1"
expand="block"
color="warning"
(click)="presentModal('downgrade')"
>
Downgrade
</ion-button>
</ng-container>
</ng-container>
<marketplace-show-controls
[pkg]="pkg"
[localPkg]="localPkg$ | async"
></marketplace-show-controls>
</ion-col>
</ion-row>
<ion-row *ngIf="localPkg">
<ion-row *ngIf="localPkg$ | async">
<ion-col
sizeXl="3"
sizeLg="3"
@@ -129,189 +54,23 @@
</ion-row>
</ion-grid>
<!-- auto-config -->
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
<ion-label>
<h2 style="display: flex; align-items: center">
<ion-text
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
>{{ pkg.manifest.title }}</ion-text
>
</h2>
<p>
<ion-text color="dark">
{{ dependentInfo.title }} requires an install of {{
pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
<br />
<br />
<span
*ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version"
class="recommendation-text"
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
displayEmver }} is compatible.</span
>
<span
*ngIf="!(pkg.manifest.version | satisfiesEmver: dependentInfo.version)"
class="recommendation-text recommendation-error"
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
displayEmver }} is NOT compatible.</span
>
</ion-text>
</p>
</ion-label>
</ion-item>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group>
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
<ion-button
[routerLink]="['notes']"
style="position: absolute; right: 10px"
fill="clear"
color="dark"
>
All Release Notes
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div
id="release-notes"
[innerHTML]="pkg.manifest['release-notes'] | markdown"
></div>
</ion-label>
</ion-item>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div id="release-notes" class="release-notes">
{{ pkg.manifest.description.long }}
</div>
</ion-label>
</ion-item>
<!-- dependencies -->
<ng-container *ngIf="!(pkg.manifest.dependencies | empty)">
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img
[src]="('data:image/png;base64,' + pkg['dependency-metadata'][dep.key].icon) | trust"
/>
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<span *ngIf="dep.value.requirement.type === 'required'">
(required)</span
>
<span *ngIf="dep.value.requirement.type === 'opt-out'">
(required by default)</span
>
<span *ngIf="dep.value.requirement.type === 'opt-in'">
(optional)</span
>
</h2>
<p style="font-size: small">
{{ dep.value.version | displayEmver }}
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
<marketplace-show-about [pkg]="pkg"></marketplace-show-about>
<marketplace-show-dependencies
[pkg]="pkg"
></marketplace-show-dependencies>
</ion-item-group>
<ion-item-divider>Additional Info</ion-item-divider>
<ion-card>
<ion-grid>
<ion-row>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item button detail="false" (click)="presentAlertVersions()">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('license')"
>
<ion-label>
<h2>License</h2>
<p>{{ pkg.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="pkg.manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ pkg.manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ pkg.manifest['support-site'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</ion-card>
<marketplace-show-additional
[pkg]="pkg"
(version)="loadVersion$.next($event)"
></marketplace-show-additional>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Package"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -1,41 +1,30 @@
.header {
font-family: 'Montserrat';
font-family: 'Montserrat', sans-serif;
padding: 2%;
img {
min-width: 15%;
max-width: 18%;
}
.header-text {
margin-left: 5%;
display: inline-block;
vertical-align: top;
.header-title {
margin: 0 0 0 -2px;
font-size: calc(20px + 3vw)
font-size: calc(20px + 3vw);
}
.header-version {
padding: 4px 0 12px 0;
margin: 0;
font-size: calc(10px + 1vw)
font-size: calc(10px + 1vw);
}
.header-status {
p {
margin: 0;
font-size: calc(16px + 1vw)
}
font-size: calc(16px + 1vw);
}
}
}
.recommendation-text {
font-style: italic;
}
.recommendation-error {
color: var(--ion-color-danger);
}
#release-notes {
overflow: auto;
max-height: 120px;
}

View File

@@ -1,223 +1,72 @@
import { Component, ViewChild } from '@angular/core'
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from '@start9labs/shared'
import {
AlertController,
IonContent,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import {
displayEmver,
Emver,
DependentInfo,
pauseFor,
PackageState,
} from '@start9labs/shared'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
LocalPkg,
MarketplacePkg,
AbstractMarketplaceService,
spreadProgress,
} from '@start9labs/marketplace'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MarketplaceService } from '../marketplace.service'
import { Subscription } from 'rxjs'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MarketplacePkg } from 'src/app/services/api/api.types'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { BehaviorSubject, defer, Observable, of } from 'rxjs'
import {
catchError,
filter,
shareReplay,
startWith,
switchMap,
tap,
} from 'rxjs/operators'
@Component({
selector: 'marketplace-show',
templateUrl: './marketplace-show.page.html',
styleUrls: ['./marketplace-show.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowPage {
@ViewChild(IonContent) content: IonContent
loading = true
pkgId: string
pkg: MarketplacePkg
localPkg: PackageDataEntry
PackageState = PackageState
dependentInfo: DependentInfo
subs: Subscription[] = []
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
readonly loadVersion$ = new BehaviorSubject<string>('*')
readonly localPkg$ = defer(() =>
this.patch.watch$('package-data', this.pkgId),
).pipe(
filter<LocalPkg>(Boolean),
tap(spreadProgress),
shareReplay({ bufferSize: 1, refCount: true }),
)
readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe(
switchMap(version =>
this.marketplaceService
.getPackage(this.pkgId, version)
.pipe(startWith(null)),
),
// TODO: Better fallback
catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)),
)
constructor(
private readonly route: ActivatedRoute,
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly wizardBaker: WizardBaker,
private readonly navCtrl: NavController,
private readonly emver: Emver,
private readonly patch: PatchDbService,
private readonly embassyApi: ApiService,
private readonly marketplaceService: MarketplaceService,
public readonly localStorageService: LocalStorageService,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
async ngOnInit() {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.dependentInfo =
history.state && (history.state.dependentInfo as DependentInfo)
this.subs = [
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
if (!pkg) return
this.localPkg = pkg
this.localPkg['install-progress'] = {
...this.localPkg['install-progress'],
}
}),
]
try {
if (!this.marketplaceService.pkgs.length) {
await this.marketplaceService.load()
}
this.pkg = this.marketplaceService.pkgs.find(
pkg => pkg.manifest.id === this.pkgId,
)
if (!this.pkg) {
throw new Error(`Service with ID "${this.pkgId}" not found.`)
}
} catch (e) {
this.errToast.present(e)
} finally {
this.loading = false
}
getIcon(icon: string): string {
return `data:image/png;base64,${icon}`
}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentAlertVersions() {
const alert = await this.alertCtrl.create({
header: 'Versions',
inputs: this.pkg.versions
.sort((a, b) => -1 * this.emver.compare(a, b))
.map(v => {
return {
name: v, // for CSS
type: 'radio',
label: displayEmver(v), // appearance on screen
value: v, // literal SEM version value
checked: this.pkg.manifest.version === v,
}
}),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Ok',
handler: (version: string) => {
this.getPkg(version)
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
async presentModalMd(title: string) {
const modal = await this.modalCtrl.create({
componentProps: {
title,
contentUrl: `/marketplace${this.pkg[title]}`,
},
component: MarkdownPage,
})
await modal.present()
}
async tryInstall() {
const { id, title, version, alerts } = this.pkg.manifest
if (!alerts.install) {
await this.install(id, version)
} else {
const alert = await this.alertCtrl.create({
header: title,
subHeader: version,
message: alerts.install,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Install',
handler: () => {
this.install(id, version)
},
},
],
})
await alert.present()
}
}
async presentModal(action: 'update' | 'downgrade') {
const { id, title, version, dependencies, alerts } = this.pkg.manifest
const value = {
id,
title,
version,
serviceRequirements: dependencies,
installAlert: alerts.install,
}
const { cancelled } = await wizardModal(
this.modalCtrl,
action === 'update'
? this.wizardBaker.update(value)
: this.wizardBaker.downgrade(value),
)
if (cancelled) return
await pauseFor(250)
this.navCtrl.back()
}
private async getPkg(version?: string): Promise<void> {
this.loading = true
try {
this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
} catch (e) {
this.errToast.present(e)
} finally {
await pauseFor(100)
this.loading = false
}
}
private async install(id: string, version?: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Beginning Installation',
cssClass: 'loader',
})
loader.present()
try {
await this.marketplaceService.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
})
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
// async getPkg(version: string): Promise<void> {
// this.loading = true
// try {
// this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
// } catch (e) {
// this.errToast.present(e)
// } finally {
// await pauseFor(100)
// this.loading = false
// }
// }
}

View File

@@ -0,0 +1,28 @@
<ng-container *ngIf="pkg; else none" [ngSwitch]="pkg.state">
<div *ngSwitchCase="PackageState.Installed">
<ion-text *ngIf="(version | compareEmver: version) === 0" color="success">
Installed
</ion-text>
<ion-text *ngIf="(version | compareEmver: version) === 1" color="warning">
Update Available
</ion-text>
</div>
<div *ngSwitchCase="PackageState.Removing">
<ion-text color="danger">
Removing
<span class="loading-dots"></span>
</ion-text>
</div>
<div *ngSwitchDefault>
<ion-text
*ngIf="pkg['install-progress'] | installProgress as progress"
color="primary"
>
Installing
<span class="loading-dots"></span>{{ progress }}
</ion-text>
</div>
</ng-container>
<ng-template #none>
<div>Not Installed</div>
</ng-template>

View File

@@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core'
import { LocalPkg } from '@start9labs/marketplace'
import { PackageState } from '@start9labs/shared'
@Component({
selector: 'marketplace-status',
templateUrl: 'marketplace-status.component.html',
})
export class MarketplaceStatusComponent {
@Input()
pkg?: LocalPkg
PackageState = PackageState
get version(): string {
return this.pkg?.manifest.version || ''
}
}

View File

@@ -0,0 +1,19 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule } from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { MarketplaceStatusComponent } from './marketplace-status.component'
@NgModule({
imports: [
CommonModule,
IonicModule,
EmverPipesModule,
MarketplacePipesModule,
],
declarations: [MarketplaceStatusComponent],
exports: [MarketplaceStatusComponent],
})
export class MarketplaceStatusModule {}

View File

@@ -1,161 +0,0 @@
import { Injectable } from '@angular/core'
import { Subscription } from 'rxjs'
import {
MarketplaceData,
MarketplacePkg,
RR,
} from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { Emver } from '@start9labs/shared'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Injectable({
providedIn: 'root',
})
export class MarketplaceService {
data: MarketplaceData
pkgs: MarketplacePkg[] = []
releaseNotes: {
[id: string]: {
[version: string]: string
}
} = {}
marketplace: {
url: string
name: string
}
constructor(
private readonly api: ApiService,
private readonly emver: Emver,
private readonly patch: PatchDbService,
private readonly config: ConfigService,
) {}
init(): Subscription {
return this.patch.watch$('ui', 'marketplace').subscribe(marketplace => {
if (!marketplace || !marketplace['selected-id']) {
this.marketplace = this.config.marketplace
} else {
this.marketplace =
marketplace['known-hosts'][marketplace['selected-id']]
}
})
}
async load(): Promise<void> {
try {
const [data, pkgs] = await Promise.all([
this.getMarketplaceData({
'server-id': this.patch.getData()['server-info'].id,
}),
this.getMarketplacePkgs({ page: 1, 'per-page': 100 }),
])
this.data = data
this.pkgs = pkgs
if (this.patch.getData().ui.marketplace?.['selected-id']) {
const { 'selected-id': selectedId, 'known-hosts': knownHosts } =
this.patch.getData().ui.marketplace
if (knownHosts[selectedId].name !== this.data.name) {
this.api.setDbValue({
pointer: `/marketplace/known-hosts/${selectedId}/name`,
value: this.data.name,
})
}
}
} catch (e) {
this.data = undefined
this.pkgs = []
throw e
}
}
async installPackage(req: Omit<RR.InstallPackageReq, 'marketplace-url'>) {
req['marketplace-url'] = this.marketplace.url
return this.api.installPackage(req as RR.InstallPackageReq)
}
async getUpdates(
localPkgs: Record<string, PackageDataEntry>,
): Promise<MarketplacePkg[]> {
const id = this.patch.getData().ui.marketplace?.['selected-id']
const url = id
? this.patch.getData().ui.marketplace['known-hosts'][id].url
: this.config.marketplace.url
const idAndCurrentVersions = Object.keys(localPkgs)
.map(key => ({
id: key,
version: localPkgs[key].manifest.version,
marketplaceUrl: localPkgs[key].installed['marketplace-url'],
}))
.filter(pkg => {
return pkg.marketplaceUrl === url
})
const latestPkgs = await this.getMarketplacePkgs({
ids: idAndCurrentVersions,
})
return latestPkgs.filter(latestPkg => {
const latestVersion = latestPkg.manifest.version
const curVersion = localPkgs[latestPkg.manifest.id]?.manifest.version
return !!curVersion && this.emver.compare(latestVersion, curVersion) === 1
})
}
async getPkg(id: string, version = '*'): Promise<MarketplacePkg> {
const pkgs = await this.getMarketplacePkgs({
ids: [{ id, version }],
})
const pkg = pkgs.find(pkg => pkg.manifest.id == id)
if (!pkg) {
throw new Error(`No results for ${id}${version ? ' ' + version : ''}`)
} else {
return pkg
}
}
async cacheReleaseNotes(id: string): Promise<void> {
this.releaseNotes[id] = await this.getReleaseNotes({ id })
}
async getMarketplaceData(
params: RR.GetMarketplaceDataReq,
url?: string,
): Promise<RR.GetMarketplaceDataRes> {
url = url || this.marketplace.url
return this.api.marketplaceProxy('/package/v0/info', params, url)
}
async getMarketplacePkgs(
params: Omit<RR.GetMarketplacePackagesReq, 'eos-version-compat'>,
): Promise<RR.GetMarketplacePackagesRes> {
if (params.query) delete params.category
if (params.ids) params.ids = JSON.stringify(params.ids) as any
const qp: RR.GetMarketplacePackagesReq = {
...params,
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
}
return this.api.marketplaceProxy(
'/package/v0/index',
qp,
this.marketplace.url,
)
}
async getReleaseNotes(
params: RR.GetReleaseNotesReq,
): Promise<RR.GetReleaseNotesRes> {
return this.api.marketplaceProxy(
`/package/v0/release-notes/${params.id}`,
{},
this.marketplace.url,
)
}
}

View File

@@ -0,0 +1,8 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="href"></ion-back-button>
</ion-buttons>
<ion-title>Release Notes</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
@Component({
selector: 'release-notes-header',
templateUrl: 'release-notes-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesHeaderComponent {
readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}`
constructor(private readonly route: ActivatedRoute) {}
}

View File

@@ -6,14 +6,17 @@ import {
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
ElementModule,
} from '@start9labs/shared'
import { AppReleaseNotes } from './app-release-notes.page'
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { ReleaseNotesPage } from './release-notes.page'
import { ReleaseNotesHeaderComponent } from './release-notes-header/release-notes-header.component'
const routes: Routes = [
{
path: '',
component: AppReleaseNotes,
component: ReleaseNotesPage,
},
]
@@ -26,7 +29,9 @@ const routes: Routes = [
EmverPipesModule,
MarkdownPipeModule,
MarketplacePipesModule,
ElementModule,
],
declarations: [AppReleaseNotes],
declarations: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
exports: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
})
export class ReleaseNotesModule {}
export class ReleaseNotesPageModule {}

View File

@@ -0,0 +1,34 @@
<release-notes-header></release-notes-header>
<ion-content>
<ng-container *ngIf="notes$ | async as notes else loading">
<div *ngFor="let note of notes | keyvalue : asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
elementRef
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
[innerHTML]="note.value | markdown"
></ion-text>
</ion-card>
</div>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -0,0 +1,19 @@
.panel {
margin: 0;
padding: 0 24px;
transition: max-height 0.2s ease-out;
}
.active {
border: 5px solid #4d4d4d;
}
.version-button {
height: 50px;
margin: 1px;
}
.version {
position: absolute;
left: 10px;
}

View File

@@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
@Component({
selector: 'release-notes',
templateUrl: './release-notes.page.html',
styleUrls: ['./release-notes.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesPage {
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
private selected: string | null = null
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -11,7 +11,7 @@ import {
ModalController,
} from '@ionic/angular'
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'

View File

@@ -20,16 +20,12 @@
</ion-item>
<ion-item
[button]="mp.id !== selectedId"
detail="false"
*ngFor="let mp of marketplaces"
detail="false"
[button]="mp.id !== selectedId"
(click)="presentAction(mp.id)"
>
<div
*ngIf="mp.id !== selectedId"
slot="start"
style="padding-right: 32px"
></div>
<div *ngIf="mp.id !== selectedId" slot="start" class="padding"></div>
<ion-icon
*ngIf="mp.id === selectedId"
slot="start"

View File

@@ -1,7 +1,12 @@
.skeleton-parts {
padding-bottom: 6px;
ion-button::part(native) {
padding-inline-start: 0;
padding-inline-end: 0;
};
padding-bottom: 6px;
}
}
}
.padding {
padding-right: 32px;
}

Some files were not shown because too many files have changed in this diff Show More