mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
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:
@@ -38,8 +38,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles/variables.scss",
|
"projects/shared/styles/variables.scss",
|
||||||
"styles/global.scss",
|
"projects/shared/styles/global.scss",
|
||||||
|
"projects/shared/styles/shared.scss",
|
||||||
"projects/ui/src/styles.scss"
|
"projects/ui/src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
@@ -157,8 +158,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles/variables.scss",
|
"projects/shared/styles/variables.scss",
|
||||||
"styles/global.scss",
|
"projects/shared/styles/global.scss",
|
||||||
"projects/setup-wizard/src/styles.scss"
|
"projects/setup-wizard/src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
@@ -276,8 +277,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles/variables.scss",
|
"projects/shared/styles/variables.scss",
|
||||||
"styles/global.scss",
|
"projects/shared/styles/global.scss",
|
||||||
"projects/diagnostic-ui/src/styles.scss"
|
"projects/diagnostic-ui/src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"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": {
|
"shared": {
|
||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"root": "projects/shared",
|
"root": "projects/shared",
|
||||||
|
|||||||
7
frontend/projects/marketplace/ng-package.json
Normal file
7
frontend/projects/marketplace/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||||
|
"dest": "../../dist/marketplace",
|
||||||
|
"lib": {
|
||||||
|
"entryFile": "src/public-api.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/projects/marketplace/package.json
Normal file
13
frontend/projects/marketplace/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { InstallProgressPipe } from './install-progress.pipe'
|
import { InstallProgressPipe } from './install-progress.pipe'
|
||||||
import { TrustPipe } from './trust.pipe'
|
import { TrustPipe } from './trust.pipe'
|
||||||
|
import { FilterPackagesPipe } from './filter-packages.pipe'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [InstallProgressPipe, TrustPipe],
|
declarations: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
|
||||||
exports: [InstallProgressPipe, TrustPipe],
|
exports: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
|
||||||
})
|
})
|
||||||
export class MarketplacePipesModule {}
|
export class MarketplacePipesModule {}
|
||||||
17
frontend/projects/marketplace/src/public-api.ts
Normal file
17
frontend/projects/marketplace/src/public-api.ts
Normal 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'
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
8
frontend/projects/marketplace/src/types/local-pkg.ts
Normal file
8
frontend/projects/marketplace/src/types/local-pkg.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { PackageState } from '@start9labs/shared'
|
||||||
|
|
||||||
|
import { MarketplaceManifest } from './marketplace-manifest'
|
||||||
|
|
||||||
|
export interface LocalPkg {
|
||||||
|
state: PackageState
|
||||||
|
manifest: MarketplaceManifest
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface MarketplaceData {
|
||||||
|
categories: string[]
|
||||||
|
name: string
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/projects/marketplace/src/types/marketplace-pkg.ts
Normal file
17
frontend/projects/marketplace/src/types/marketplace-pkg.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/projects/marketplace/src/types/marketplace.ts
Normal file
4
frontend/projects/marketplace/src/types/marketplace.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Marketplace {
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { LocalPkg } from '../types/local-pkg'
|
||||||
|
|
||||||
|
export function spreadProgress(pkg: LocalPkg) {
|
||||||
|
pkg['install-progress'] = { ...pkg['install-progress'] }
|
||||||
|
}
|
||||||
13
frontend/projects/marketplace/tsconfig.json
Normal file
13
frontend/projects/marketplace/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
10
frontend/projects/marketplace/tsconfig.prod.json
Normal file
10
frontend/projects/marketplace/tsconfig.prod.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NavController } from '@ionic/angular'
|
import { NavController } from '@ionic/angular'
|
||||||
import { ApiService } from './services/api/api.service'
|
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'
|
import { StateService } from './services/state.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -30,7 +30,7 @@ export class AppComponent {
|
|||||||
await this.navCtrl.navigateForward(`/recover`)
|
await this.navCtrl.navigateForward(`/recover`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(e.message)
|
this.errorToastService.present(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DiskInfo,
|
DiskInfo,
|
||||||
DiskRecoverySource,
|
DiskRecoverySource,
|
||||||
} from 'src/app/services/api/api.service'
|
} 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 { StateService } from 'src/app/services/state.service'
|
||||||
import { PasswordPage } from '../../modals/password/password.page'
|
import { PasswordPage } from '../../modals/password/password.page'
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export class EmbassyPage {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(e.message)
|
this.errorToastService.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -142,9 +142,9 @@ export class EmbassyPage {
|
|||||||
await this.navCtrl.navigateForward(`/success`)
|
await this.navCtrl.navigateForward(`/success`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(
|
this.errorToastService.present({
|
||||||
`${e.message}\n\nRestart Embassy to try again.`,
|
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||||
)
|
})
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
loader.dismiss()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
||||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
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 { StateService } from 'src/app/services/state.service'
|
||||||
import { PasswordPage } from '../../modals/password/password.page'
|
import { PasswordPage } from '../../modals/password/password.page'
|
||||||
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||||
@@ -120,7 +120,7 @@ export class RecoverPage {
|
|||||||
this.hasShownGuidAlert = true
|
this.hasShownGuidAlert = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(e.message)
|
this.errorToastService.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ export class RecoverPage {
|
|||||||
await this.stateService.importDrive(guid)
|
await this.stateService.importDrive(guid)
|
||||||
await this.navCtrl.navigateForward(`/success`)
|
await this.navCtrl.navigateForward(`/success`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(e.message)
|
this.errorToastService.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
loader.dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Output } from '@angular/core'
|
import { Component, EventEmitter, Output } from '@angular/core'
|
||||||
import { ToastController } from '@ionic/angular'
|
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'
|
import { StateService } from 'src/app/services/state.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
CifsRecoverySource,
|
CifsRecoverySource,
|
||||||
DiskRecoverySource,
|
DiskRecoverySource,
|
||||||
} from './api/api.service'
|
} from './api/api.service'
|
||||||
import { ErrorToastService } from './error-toast.service'
|
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||||
import { pauseFor } from '@start9labs/shared'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -51,9 +50,9 @@ export class StateService {
|
|||||||
try {
|
try {
|
||||||
progress = await this.apiService.getRecoveryStatus()
|
progress = await this.apiService.getRecoveryStatus()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToastService.present(
|
this.errorToastService.present({
|
||||||
`${e.message}\n\nRestart Embassy to try again.`,
|
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
if (progress) {
|
if (progress) {
|
||||||
this.dataTransferProgress = {
|
this.dataTransferProgress = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||||
"dest": "../../dist/shared",
|
"dest": "../../dist/shared",
|
||||||
|
"assets": ["styles"],
|
||||||
"lib": {
|
"lib": {
|
||||||
"entryFile": "src/public-api.ts"
|
"entryFile": "src/public-api.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^13.2.0",
|
"@angular/common": "^13.2.0",
|
||||||
"@angular/core": "^13.2.0",
|
"@angular/core": "^13.2.0",
|
||||||
|
"@ionic/angular": "^6.0.3",
|
||||||
"@start9labs/emver": "^0.1.5"
|
"@start9labs/emver": "^0.1.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
|
||||||
|
import { ElementDirective } from './element.directive'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ElementDirective],
|
||||||
|
exports: [ElementDirective],
|
||||||
|
})
|
||||||
|
export class ElementModule {}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
export * from './components/text-spinner/text-spinner.component.module'
|
export * from './components/text-spinner/text-spinner.component.module'
|
||||||
export * from './components/text-spinner/text-spinner.component'
|
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.module'
|
||||||
export * from './pipes/emver/emver.pipe'
|
export * from './pipes/emver/emver.pipe'
|
||||||
export * from './pipes/markdown/markdown.module'
|
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/destroy.service'
|
||||||
export * from './services/emver.service'
|
export * from './services/emver.service'
|
||||||
|
export * from './services/error-toast.service'
|
||||||
|
|
||||||
export * from './types/dependent-info'
|
export * from './types/dependent-info'
|
||||||
export * from './types/install-progress'
|
export * from './types/install-progress'
|
||||||
export * from './types/package-state'
|
export * from './types/package-state'
|
||||||
export * from './types/progress-data'
|
export * from './types/progress-data'
|
||||||
|
export * from './types/url'
|
||||||
export * from './types/workspace-config'
|
export * from './types/workspace-config'
|
||||||
|
|
||||||
export * from './util/misc.util'
|
export * from './util/misc.util'
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { IonicSafeString, ToastController } from '@ionic/angular'
|
import { IonicSafeString, ToastController } from '@ionic/angular'
|
||||||
import { RequestError } from './http.service'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -10,7 +9,7 @@ export class ErrorToastService {
|
|||||||
|
|
||||||
constructor(private readonly toastCtrl: ToastController) {}
|
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)
|
console.error(e)
|
||||||
|
|
||||||
if (this.toast) return
|
if (this.toast) return
|
||||||
@@ -43,18 +42,16 @@ export class ErrorToastService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getErrorMessage(
|
export function getErrorMessage(
|
||||||
e: RequestError,
|
{ message }: { message: string },
|
||||||
link?: string,
|
link?: string,
|
||||||
): string | IonicSafeString {
|
): string | IonicSafeString {
|
||||||
let message: string | IonicSafeString = e.message
|
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
message = 'Unknown Error.'
|
message = 'Unknown Error.'
|
||||||
link = 'https://start9.com/latest/support/FAQ'
|
link = 'https://start9.com/latest/support/FAQ'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
message = new IonicSafeString(
|
return new IonicSafeString(
|
||||||
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
|
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
1
frontend/projects/shared/src/types/url.ts
Normal file
1
frontend/projects/shared/src/types/url.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type Url = string
|
||||||
@@ -56,8 +56,7 @@ export function isObject(val: any): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isEmptyObject(obj: object): boolean {
|
export function isEmptyObject(obj: object): boolean {
|
||||||
if (obj === undefined) return true
|
return obj === undefined || !Object.keys(obj).length
|
||||||
return !Object.keys(obj).length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pauseFor(ms: number): Promise<void> {
|
export function pauseFor(ms: number): Promise<void> {
|
||||||
|
|||||||
@@ -24,53 +24,3 @@
|
|||||||
@import "~@ionic/angular/css/text-alignment.css";
|
@import "~@ionic/angular/css/text-alignment.css";
|
||||||
@import "~@ionic/angular/css/text-transformation.css";
|
@import "~@ionic/angular/css/text-transformation.css";
|
||||||
@import "~@ionic/angular/css/flex-utils.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
frontend/projects/shared/styles/shared.scss
Normal file
85
frontend/projects/shared/styles/shared.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
import { Component, HostListener, NgZone } from '@angular/core'
|
import { Component, HostListener, Inject, 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 { Router, RoutesRecognized } from '@angular/router'
|
import { Router, RoutesRecognized } from '@angular/router'
|
||||||
import {
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
take,
|
|
||||||
} from 'rxjs/operators'
|
|
||||||
import {
|
import {
|
||||||
AlertController,
|
AlertController,
|
||||||
IonicSafeString,
|
IonicSafeString,
|
||||||
@@ -16,21 +7,33 @@ import {
|
|||||||
ModalController,
|
ModalController,
|
||||||
ToastController,
|
ToastController,
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { SplitPaneTracker } from './services/split-pane.service'
|
|
||||||
import { ToastButton } from '@ionic/core'
|
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 { PatchDbService } from './services/patch-db/patch-db.service'
|
||||||
import {
|
import {
|
||||||
ConnectionFailure,
|
ConnectionFailure,
|
||||||
ConnectionService,
|
ConnectionService,
|
||||||
} from './services/connection.service'
|
} from './services/connection.service'
|
||||||
import { ConfigService } from './services/config.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 { 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 { LocalStorageService } from './services/local-storage.service'
|
||||||
import { EOSService } from './services/eos.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 { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
|
||||||
import { SnakePage } from './modals/snake/snake.page'
|
import { SnakePage } from './modals/snake/snake.page'
|
||||||
|
|
||||||
@@ -128,7 +131,6 @@ export class AppComponent {
|
|||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
private readonly connectionService: ConnectionService,
|
private readonly connectionService: ConnectionService,
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly marketplaceService: MarketplaceService,
|
|
||||||
private readonly toastCtrl: ToastController,
|
private readonly toastCtrl: ToastController,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -190,8 +192,6 @@ export class AppComponent {
|
|||||||
this.watchVersion(),
|
this.watchVersion(),
|
||||||
// watch unread notification count to display toast
|
// watch unread notification count to display toast
|
||||||
this.watchNotifications(),
|
this.watchNotifications(),
|
||||||
// watch marketplace URL for changes
|
|
||||||
this.marketplaceService.init(),
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
// UNVERIFIED
|
// UNVERIFIED
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { MockApiService } from './services/api/embassy-mock-api.service'
|
|||||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||||
import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared'
|
import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared'
|
||||||
|
import { MarketplaceModule } from './marketplace.module'
|
||||||
|
|
||||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
|||||||
GenericInputComponentModule,
|
GenericInputComponentModule,
|
||||||
MonacoEditorModule,
|
MonacoEditorModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
|
MarketplaceModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
@@ -79,4 +81,4 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
|||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.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 { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { IonicSafeString } from '@ionic/core'
|
import { IonicSafeString } from '@ionic/core'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
|
||||||
import {
|
import {
|
||||||
BackupTarget,
|
BackupTarget,
|
||||||
CifsBackupTarget,
|
CifsBackupTarget,
|
||||||
DiskBackupTarget,
|
DiskBackupTarget,
|
||||||
} from 'src/app/services/api/api.types'
|
} from 'src/app/services/api/api.types'
|
||||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||||
import { Emver } from '@start9labs/shared'
|
import { getErrorMessage, Emver } from '@start9labs/shared'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
|||||||
@@ -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 { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { Breakages } from 'src/app/services/api/api.types'
|
import { Breakages } from 'src/app/services/api/api.types'
|
||||||
import { exists } from '@start9labs/shared'
|
|
||||||
import { ApiService } from '../../services/api/embassy-api.service'
|
import { ApiService } from '../../services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
InstallWizardComponent,
|
InstallWizardComponent,
|
||||||
@@ -9,13 +10,14 @@ import {
|
|||||||
TopbarParams,
|
TopbarParams,
|
||||||
} from './install-wizard.component'
|
} from './install-wizard.component'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class WizardBaker {
|
export class WizardBaker {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
|
@Inject(AbstractMarketplaceService)
|
||||||
private readonly marketplaceService: MarketplaceService,
|
private readonly marketplaceService: MarketplaceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -77,10 +79,12 @@ export class WizardBaker {
|
|||||||
verb: 'beginning update for',
|
verb: 'beginning update for',
|
||||||
title,
|
title,
|
||||||
executeAction: () =>
|
executeAction: () =>
|
||||||
this.marketplaceService.installPackage({
|
this.marketplaceService
|
||||||
id,
|
.installPackage({
|
||||||
'version-spec': version ? `=${version}` : undefined,
|
id,
|
||||||
}),
|
'version-spec': version ? `=${version}` : undefined,
|
||||||
|
})
|
||||||
|
.toPromise(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -202,10 +206,12 @@ export class WizardBaker {
|
|||||||
verb: 'beginning downgrade for',
|
verb: 'beginning downgrade for',
|
||||||
title,
|
title,
|
||||||
executeAction: () =>
|
executeAction: () =>
|
||||||
this.marketplaceService.installPackage({
|
this.marketplaceService
|
||||||
id,
|
.installPackage({
|
||||||
'version-spec': version ? `=${version}` : undefined,
|
id,
|
||||||
}),
|
'version-spec': version ? `=${version}` : undefined,
|
||||||
|
})
|
||||||
|
.toPromise(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input, ViewChild } from '@angular/core'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { IonContent } from '@ionic/angular'
|
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'
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
var Convert = require('ansi-to-html')
|
var Convert = require('ansi-to-html')
|
||||||
var convert = new Convert({
|
var convert = new Convert({
|
||||||
@@ -14,7 +14,11 @@ var convert = new Convert({
|
|||||||
})
|
})
|
||||||
export class LogsPage {
|
export class LogsPage {
|
||||||
@ViewChild(IonContent) private content: IonContent
|
@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
|
loading = true
|
||||||
loadingMore = false
|
loadingMore = false
|
||||||
logs: string
|
logs: string
|
||||||
@@ -25,15 +29,13 @@ export class LogsPage {
|
|||||||
scrollToBottomButton = false
|
scrollToBottomButton = false
|
||||||
isOnBottom = true
|
isOnBottom = true
|
||||||
|
|
||||||
constructor (
|
constructor(private readonly errToast: ErrorToastService) {}
|
||||||
private readonly errToast: ErrorToastService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit() {
|
||||||
this.getLogs()
|
this.getLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch (isBefore: boolean = true) {
|
async fetch(isBefore: boolean = true) {
|
||||||
try {
|
try {
|
||||||
const cursor = isBefore ? this.startCursor : this.endCursor
|
const cursor = isBefore ? this.startCursor : this.endCursor
|
||||||
const logsRes = await this.fetchLogs({
|
const logsRes = await this.fetchLogs({
|
||||||
@@ -57,7 +59,7 @@ export class LogsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs () {
|
async getLogs() {
|
||||||
try {
|
try {
|
||||||
// get logs
|
// get logs
|
||||||
const logs = await this.fetch()
|
const logs = await this.fetch()
|
||||||
@@ -65,47 +67,60 @@ export class LogsPage {
|
|||||||
|
|
||||||
const container = document.getElementById('container')
|
const container = document.getElementById('container')
|
||||||
const beforeContainerHeight = container.scrollHeight
|
const beforeContainerHeight = container.scrollHeight
|
||||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
const newLogs = document
|
||||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
.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)
|
container.prepend(newLogs)
|
||||||
const afterContainerHeight = container.scrollHeight
|
const afterContainerHeight = container.scrollHeight
|
||||||
|
|
||||||
// scroll down
|
// scroll down
|
||||||
scrollBy(0, afterContainerHeight - beforeContainerHeight)
|
scrollBy(0, afterContainerHeight - beforeContainerHeight)
|
||||||
this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
|
this.content.scrollToPoint(
|
||||||
|
0,
|
||||||
|
afterContainerHeight - beforeContainerHeight,
|
||||||
|
)
|
||||||
|
|
||||||
if (logs.length < this.limit) {
|
if (logs.length < this.limit) {
|
||||||
this.needInfinite = false
|
this.needInfinite = false
|
||||||
}
|
}
|
||||||
|
} catch (e) {}
|
||||||
} catch (e) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMore () {
|
async loadMore() {
|
||||||
try {
|
try {
|
||||||
this.loadingMore = true
|
this.loadingMore = true
|
||||||
const logs = await this.fetch(false)
|
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 container = document.getElementById('container')
|
||||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
const newLogs = document
|
||||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
.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)
|
container.append(newLogs)
|
||||||
this.loadingMore = false
|
this.loadingMore = false
|
||||||
this.scrollEvent()
|
this.scrollEvent()
|
||||||
} catch (e) { }
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollEvent () {
|
scrollEvent() {
|
||||||
const buttonDiv = document.getElementById('button-div')
|
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)
|
this.content.scrollToBottom(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData (e: any): Promise<void> {
|
async loadData(e: any): Promise<void> {
|
||||||
await this.getLogs()
|
await this.getLogs()
|
||||||
e.target.complete()
|
e.target.complete()
|
||||||
}
|
}
|
||||||
|
|||||||
13
frontend/projects/ui/src/app/marketplace.module.ts
Normal file
13
frontend/projects/ui/src/app/marketplace.module.ts
Normal 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 {}
|
||||||
@@ -7,16 +7,18 @@ import {
|
|||||||
IonicSafeString,
|
IonicSafeString,
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
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 { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
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 { FormGroup } from '@angular/forms'
|
||||||
import {
|
import {
|
||||||
convertValuesRecursive,
|
convertValuesRecursive,
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
|
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { Emver } from '@start9labs/shared'
|
import { getErrorMessage, Emver } from '@start9labs/shared'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input, ViewChild } from '@angular/core'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
import { getErrorMessage } from '@start9labs/shared'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'generic-input',
|
selector: 'generic-input',
|
||||||
@@ -14,11 +14,9 @@ export class GenericInputComponent {
|
|||||||
unmasked = false
|
unmasked = false
|
||||||
error: string | IonicSafeString
|
error: string | IonicSafeString
|
||||||
|
|
||||||
constructor (
|
constructor(private readonly modalCtrl: ModalController) {}
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit() {
|
||||||
const defaultOptions: Partial<GenericInputOptions> = {
|
const defaultOptions: Partial<GenericInputOptions> = {
|
||||||
buttonText: 'Submit',
|
buttonText: 'Submit',
|
||||||
placeholder: 'Enter value',
|
placeholder: 'Enter value',
|
||||||
@@ -34,19 +32,19 @@ export class GenericInputComponent {
|
|||||||
this.value = this.options.initialValue
|
this.value = this.options.initialValue
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit() {
|
||||||
setTimeout(() => this.elem.setFocus(), 400)
|
setTimeout(() => this.elem.setFocus(), 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMask () {
|
toggleMask() {
|
||||||
this.unmasked = !this.unmasked
|
this.unmasked = !this.unmasked
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel () {
|
cancel() {
|
||||||
this.modalCtrl.dismiss()
|
this.modalCtrl.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit () {
|
async submit() {
|
||||||
const value = this.value.trim()
|
const value = this.value.trim()
|
||||||
|
|
||||||
if (!value && !this.options.nullable) return
|
if (!value && !this.options.nullable) return
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ModalController, IonicSafeString } from '@ionic/angular'
|
import { ModalController, IonicSafeString } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
import { getErrorMessage, pauseFor } from '@start9labs/shared'
|
||||||
import { pauseFor } from '../../../../../shared/src/util/misc.util'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'markdown',
|
selector: 'markdown',
|
||||||
|
|||||||
@@ -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 { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||||
import { isEmptyObject } from '@start9labs/shared'
|
|
||||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { AlertController } from '@ionic/angular'
|
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 { 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 { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
|
||||||
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
|
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
|
||||||
import { RecoveredInfo } from 'src/app/util/parse-data-model'
|
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({
|
@Component({
|
||||||
selector: 'app-list-rec',
|
selector: 'app-list-rec',
|
||||||
@@ -33,17 +35,14 @@ export class AppListRecComponent {
|
|||||||
readonly installing$ = this.install$.pipe(
|
readonly installing$ = this.install$.pipe(
|
||||||
switchMap(({ id, version }) =>
|
switchMap(({ id, version }) =>
|
||||||
// Mapping each installation to API request
|
// Mapping each installation to API request
|
||||||
from(
|
this.marketplaceService.installPackage({
|
||||||
this.marketplaceService.installPackage({
|
id,
|
||||||
id,
|
'version-spec': `>=${version}`,
|
||||||
'version-spec': `>=${version}`,
|
'version-priority': 'min',
|
||||||
'version-priority': 'min',
|
}),
|
||||||
}),
|
|
||||||
).pipe(
|
|
||||||
// Mapping operation to true/false loading indication
|
|
||||||
loading(this.errToast),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
// Mapping operation to true/false loading indication
|
||||||
|
loading(this.errToast),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deleting package
|
// Deleting package
|
||||||
@@ -66,6 +65,7 @@ export class AppListRecComponent {
|
|||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
|
@Inject(AbstractMarketplaceService)
|
||||||
private readonly marketplaceService: MarketplaceService,
|
private readonly marketplaceService: MarketplaceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import { IonContent } from '@ionic/angular'
|
|||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { Metric } from 'src/app/services/api/api.types'
|
import { Metric } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
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 { MainStatus } from 'src/app/services/patch-db/data-model'
|
||||||
import { pauseFor } from '@start9labs/shared'
|
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-metrics',
|
selector: 'app-metrics',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util'
|
|||||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
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'
|
import { getValueByPointer } from 'fast-json-patch'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import {
|
|||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
Status,
|
Status,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} 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 { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import {
|
import {
|
||||||
AlertController,
|
AlertController,
|
||||||
LoadingController,
|
LoadingController,
|
||||||
ModalController,
|
ModalController,
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
|
|||||||
@@ -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 { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
||||||
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
|
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
|
||||||
import { ErrorToastService } from '../../../services/error-toast.service'
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dev-config',
|
selector: 'dev-config',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'
|
|||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { take } from 'rxjs/operators'
|
import { take } from 'rxjs/operators'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.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 { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
||||||
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
|
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
|||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { DevData } from 'src/app/services/patch-db/data-model'
|
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 { ActivatedRoute } from '@angular/router'
|
||||||
import { DestroyService } from '@start9labs/shared'
|
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
||||||
import { takeUntil } from 'rxjs/operators'
|
import { takeUntil } from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
|||||||
import { BasicInfo, getBasicInfoSpec } from './form-info'
|
import { BasicInfo, getBasicInfoSpec } from './form-info'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.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 { takeUntil } from 'rxjs/operators'
|
||||||
import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
||||||
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'
|
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.panel {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
border: 5px solid #4d4d4d;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<badge-menu-button></badge-menu-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
@@ -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 {}
|
||||||
@@ -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>
|
||||||
@@ -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 {}
|
||||||
@@ -8,9 +8,14 @@ import {
|
|||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
|
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
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 { 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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -29,8 +34,20 @@ const routes: Routes = [
|
|||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MarketplacePipesModule,
|
MarketplacePipesModule,
|
||||||
|
MarketplaceStatusModule,
|
||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [MarketplaceListPage],
|
declarations: [
|
||||||
|
MarketplaceListPage,
|
||||||
|
MarketplaceListHeaderComponent,
|
||||||
|
MarketplaceListContentComponent,
|
||||||
|
MarketplaceListSkeletonComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MarketplaceListPage,
|
||||||
|
MarketplaceListHeaderComponent,
|
||||||
|
MarketplaceListContentComponent,
|
||||||
|
MarketplaceListSkeletonComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class MarketplaceListPageModule {}
|
export class MarketplaceListPageModule {}
|
||||||
|
|||||||
@@ -1,175 +1,15 @@
|
|||||||
<ion-header>
|
<marketplace-list-header></marketplace-list-header>
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<badge-menu-button></badge-menu-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<!-- loading -->
|
<marketplace-list-content
|
||||||
<text-spinner
|
*ngIf="loaded else loading"
|
||||||
*ngIf="!patch.loaded else data"
|
[localPkgs]="localPkgs$ | async"
|
||||||
text="Connecting to Embassy"
|
[pkgs]="pkgs$ | async"
|
||||||
></text-spinner>
|
[categories]="categories$ | async"
|
||||||
|
[name]="name$ | async"
|
||||||
|
></marketplace-list-content>
|
||||||
|
|
||||||
<!-- not loading -->
|
<ng-template #loading>
|
||||||
<ng-template #data>
|
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||||
<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>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,146 +1,53 @@
|
|||||||
import { Component, ViewChild } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { MarketplacePkg } from 'src/app/services/api/api.types'
|
import { defer, Observable } from 'rxjs'
|
||||||
import { IonContent } from '@ionic/angular'
|
import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators'
|
||||||
import { Subscription } from 'rxjs'
|
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import {
|
||||||
import { MarketplaceService } from '../marketplace.service'
|
AbstractMarketplaceService,
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
LocalPkg,
|
||||||
import Fuse from 'fuse.js/dist/fuse.min.js'
|
MarketplacePkg,
|
||||||
import { exists, isEmptyObject, PackageState } from '@start9labs/shared'
|
spreadProgress,
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
} from '@start9labs/marketplace'
|
||||||
import { filter, first } from 'rxjs/operators'
|
|
||||||
|
|
||||||
const defaultOps = {
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
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',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-list',
|
selector: 'marketplace-list',
|
||||||
templateUrl: './marketplace-list.page.html',
|
templateUrl: './marketplace-list.page.html',
|
||||||
styleUrls: ['./marketplace-list.page.scss'],
|
|
||||||
})
|
})
|
||||||
export class MarketplaceListPage {
|
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[] = []
|
readonly pkgs$: Observable<MarketplacePkg[]> = defer(() =>
|
||||||
categories: string[]
|
this.patch.watch$('server-info'),
|
||||||
localPkgs: Record<string, PackageDataEntry> = {}
|
).pipe(
|
||||||
category = 'featured'
|
filter(data => exists(data) && !isEmptyObject(data)),
|
||||||
query: string
|
first(),
|
||||||
loading = true
|
switchMapTo(this.marketplaceService.getPackages()),
|
||||||
|
)
|
||||||
|
|
||||||
subs: Subscription[] = []
|
readonly name$: Observable<string> = this.marketplaceService
|
||||||
|
.getMarketplace()
|
||||||
|
.pipe(map(({ name }) => name))
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly patch: PatchDbService,
|
||||||
public readonly patch: PatchDbService,
|
private readonly marketplaceService: AbstractMarketplaceService,
|
||||||
public readonly marketplaceService: MarketplaceService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
get loaded(): boolean {
|
||||||
this.subs = [
|
return this.patch.loaded
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,24 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'browse',
|
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',
|
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',
|
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)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class MarketplaceRoutingModule { }
|
export class MarketplaceRoutingModule {}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.all-notes {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 120px;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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 {}
|
||||||
@@ -2,15 +2,23 @@ import { NgModule } from '@angular/core'
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
|
||||||
import {
|
import {
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MarkdownPipeModule,
|
MarkdownPipeModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
|
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -29,8 +37,26 @@ const routes: Routes = [
|
|||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MarkdownPipeModule,
|
MarkdownPipeModule,
|
||||||
MarketplacePipesModule,
|
MarketplacePipesModule,
|
||||||
|
MarketplaceStatusModule,
|
||||||
InstallWizardComponentModule,
|
InstallWizardComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [MarketplaceShowPage],
|
declarations: [
|
||||||
|
MarketplaceShowPage,
|
||||||
|
MarketplaceShowHeaderComponent,
|
||||||
|
MarketplaceShowControlsComponent,
|
||||||
|
MarketplaceShowDependentComponent,
|
||||||
|
MarketplaceShowAboutComponent,
|
||||||
|
MarketplaceShowDependenciesComponent,
|
||||||
|
MarketplaceShowAdditionalComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MarketplaceShowPage,
|
||||||
|
MarketplaceShowHeaderComponent,
|
||||||
|
MarketplaceShowControlsComponent,
|
||||||
|
MarketplaceShowDependentComponent,
|
||||||
|
MarketplaceShowAboutComponent,
|
||||||
|
MarketplaceShowDependenciesComponent,
|
||||||
|
MarketplaceShowAdditionalComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class MarketplaceShowPageModule {}
|
export class MarketplaceShowPageModule {}
|
||||||
|
|||||||
@@ -1,68 +1,21 @@
|
|||||||
<ion-header>
|
<marketplace-show-header></marketplace-show-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>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<text-spinner
|
<ng-container *ngIf="pkg$ | async as pkg else loading">
|
||||||
*ngIf="loading; else loaded"
|
|
||||||
text="Loading Package"
|
|
||||||
></text-spinner>
|
|
||||||
|
|
||||||
<ng-template #loaded>
|
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
|
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img [src]="('data:image/png;base64,' + pkg.icon) | trust" />
|
<img alt="" [src]="getIcon(pkg.icon) | trust" />
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
|
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
|
||||||
<p class="header-version">
|
<p class="header-version">
|
||||||
{{ pkg.manifest.version | displayEmver }}
|
{{ pkg.manifest.version | displayEmver }}
|
||||||
</p>
|
</p>
|
||||||
<div class="header-status">
|
<marketplace-status
|
||||||
<!-- no localPkg -->
|
class="header-status"
|
||||||
<p *ngIf="!localPkg; else local">Not Installed</p>
|
[pkg]="localPkg$ | async"
|
||||||
<!-- localPkg -->
|
></marketplace-status>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
@@ -74,41 +27,13 @@
|
|||||||
sizeXs="12"
|
sizeXs="12"
|
||||||
class="ion-align-self-center"
|
class="ion-align-self-center"
|
||||||
>
|
>
|
||||||
<!-- no localPkg -->
|
<marketplace-show-controls
|
||||||
<ion-button *ngIf="!localPkg" expand="block" (click)="tryInstall()">
|
[pkg]="pkg"
|
||||||
Install
|
[localPkg]="localPkg$ | async"
|
||||||
</ion-button>
|
></marketplace-show-controls>
|
||||||
<!-- 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>
|
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
<ion-row *ngIf="localPkg">
|
<ion-row *ngIf="localPkg$ | async">
|
||||||
<ion-col
|
<ion-col
|
||||||
sizeXl="3"
|
sizeXl="3"
|
||||||
sizeLg="3"
|
sizeLg="3"
|
||||||
@@ -129,189 +54,23 @@
|
|||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|
||||||
<!-- auto-config -->
|
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
|
||||||
<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>
|
|
||||||
|
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<!-- release notes -->
|
<marketplace-show-about [pkg]="pkg"></marketplace-show-about>
|
||||||
<ion-item-divider>
|
|
||||||
New in {{ pkg.manifest.version | displayEmver }}
|
<marketplace-show-dependencies
|
||||||
<ion-button
|
[pkg]="pkg"
|
||||||
[routerLink]="['notes']"
|
></marketplace-show-dependencies>
|
||||||
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>
|
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
<ion-item-divider>Additional Info</ion-item-divider>
|
<marketplace-show-additional
|
||||||
<ion-card>
|
[pkg]="pkg"
|
||||||
<ion-grid>
|
(version)="loadVersion$.next($event)"
|
||||||
<ion-row>
|
></marketplace-show-additional>
|
||||||
<ion-col sizeSm="12" sizeMd="6">
|
</ng-container>
|
||||||
<ion-item-group>
|
|
||||||
<ion-item button detail="false" (click)="presentAlertVersions()">
|
<ng-template #loading>
|
||||||
<ion-label>
|
<text-spinner text="Loading Package"></text-spinner>
|
||||||
<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>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,41 +1,30 @@
|
|||||||
.header {
|
.header {
|
||||||
font-family: 'Montserrat';
|
font-family: 'Montserrat', sans-serif;
|
||||||
padding: 2%;
|
padding: 2%;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
min-width: 15%;
|
min-width: 15%;
|
||||||
max-width: 18%;
|
max-width: 18%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
margin: 0 0 0 -2px;
|
margin: 0 0 0 -2px;
|
||||||
font-size: calc(20px + 3vw)
|
font-size: calc(20px + 3vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-version {
|
.header-version {
|
||||||
padding: 4px 0 12px 0;
|
padding: 4px 0 12px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: calc(10px + 1vw)
|
font-size: calc(10px + 1vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-status {
|
.header-status {
|
||||||
p {
|
font-size: calc(16px + 1vw);
|
||||||
margin: 0;
|
|
||||||
font-size: calc(16px + 1vw)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommendation-text {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommendation-error {
|
|
||||||
color: var(--ion-color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
#release-notes {
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 120px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,223 +1,72 @@
|
|||||||
import { Component, ViewChild } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
AlertController,
|
LocalPkg,
|
||||||
IonContent,
|
MarketplacePkg,
|
||||||
LoadingController,
|
AbstractMarketplaceService,
|
||||||
ModalController,
|
spreadProgress,
|
||||||
NavController,
|
} from '@start9labs/marketplace'
|
||||||
} 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'
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { BehaviorSubject, defer, Observable, of } from 'rxjs'
|
||||||
import { MarketplaceService } from '../marketplace.service'
|
import {
|
||||||
import { Subscription } from 'rxjs'
|
catchError,
|
||||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
filter,
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
shareReplay,
|
||||||
import { MarketplacePkg } from 'src/app/services/api/api.types'
|
startWith,
|
||||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
switchMap,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-show',
|
selector: 'marketplace-show',
|
||||||
templateUrl: './marketplace-show.page.html',
|
templateUrl: './marketplace-show.page.html',
|
||||||
styleUrls: ['./marketplace-show.page.scss'],
|
styleUrls: ['./marketplace-show.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MarketplaceShowPage {
|
export class MarketplaceShowPage {
|
||||||
@ViewChild(IonContent) content: IonContent
|
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||||
loading = true
|
|
||||||
pkgId: string
|
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||||
pkg: MarketplacePkg
|
|
||||||
localPkg: PackageDataEntry
|
readonly localPkg$ = defer(() =>
|
||||||
PackageState = PackageState
|
this.patch.watch$('package-data', this.pkgId),
|
||||||
dependentInfo: DependentInfo
|
).pipe(
|
||||||
subs: Subscription[] = []
|
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(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
private readonly loadingCtrl: LoadingController,
|
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly wizardBaker: WizardBaker,
|
|
||||||
private readonly navCtrl: NavController,
|
|
||||||
private readonly emver: Emver,
|
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly marketplaceService: AbstractMarketplaceService,
|
||||||
private readonly marketplaceService: MarketplaceService,
|
|
||||||
public readonly localStorageService: LocalStorageService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
getIcon(icon: string): string {
|
||||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
return `data:image/png;base64,${icon}`
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
// async getPkg(version: string): Promise<void> {
|
||||||
this.content.scrollToPoint(undefined, 1)
|
// this.loading = true
|
||||||
}
|
// try {
|
||||||
|
// this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
|
||||||
ngOnDestroy() {
|
// } catch (e) {
|
||||||
this.subs.forEach(sub => sub.unsubscribe())
|
// this.errToast.present(e)
|
||||||
}
|
// } finally {
|
||||||
|
// await pauseFor(100)
|
||||||
async presentAlertVersions() {
|
// this.loading = false
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -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) {}
|
||||||
|
}
|
||||||
@@ -6,14 +6,17 @@ import {
|
|||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MarkdownPipeModule,
|
MarkdownPipeModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
|
ElementModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { AppReleaseNotes } from './app-release-notes.page'
|
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||||
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
|
|
||||||
|
import { ReleaseNotesPage } from './release-notes.page'
|
||||||
|
import { ReleaseNotesHeaderComponent } from './release-notes-header/release-notes-header.component'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AppReleaseNotes,
|
component: ReleaseNotesPage,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,7 +29,9 @@ const routes: Routes = [
|
|||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MarkdownPipeModule,
|
MarkdownPipeModule,
|
||||||
MarketplacePipesModule,
|
MarketplacePipesModule,
|
||||||
|
ElementModule,
|
||||||
],
|
],
|
||||||
declarations: [AppReleaseNotes],
|
declarations: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
|
||||||
|
exports: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
|
||||||
})
|
})
|
||||||
export class ReleaseNotesModule {}
|
export class ReleaseNotesPageModule {}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ModalController,
|
ModalController,
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
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 { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
|
|
||||||
|
|||||||
@@ -20,16 +20,12 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item
|
<ion-item
|
||||||
[button]="mp.id !== selectedId"
|
|
||||||
detail="false"
|
|
||||||
*ngFor="let mp of marketplaces"
|
*ngFor="let mp of marketplaces"
|
||||||
|
detail="false"
|
||||||
|
[button]="mp.id !== selectedId"
|
||||||
(click)="presentAction(mp.id)"
|
(click)="presentAction(mp.id)"
|
||||||
>
|
>
|
||||||
<div
|
<div *ngIf="mp.id !== selectedId" slot="start" class="padding"></div>
|
||||||
*ngIf="mp.id !== selectedId"
|
|
||||||
slot="start"
|
|
||||||
style="padding-right: 32px"
|
|
||||||
></div>
|
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="mp.id === selectedId"
|
*ngIf="mp.id === selectedId"
|
||||||
slot="start"
|
slot="start"
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
.skeleton-parts {
|
.skeleton-parts {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
|
||||||
ion-button::part(native) {
|
ion-button::part(native) {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
padding-inline-end: 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
Reference in New Issue
Block a user