basic info checkpoint (#1230)

* basic info

* preview manifest

* textarea not long

* show error message details always

* reinstall button in marketplace

Co-authored-by: Drew Ansbacher <drew@start9labs.com>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Drew Ansbacher
2022-02-21 12:44:25 -07:00
committed by GitHub
parent 6a7ab4d188
commit 9b4e5f0805
29 changed files with 956 additions and 293 deletions

View File

@@ -1,7 +0,0 @@
.centered{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: scale(2);
}

View File

@@ -75,9 +75,7 @@ export class DevConfigPage {
value: this.code,
})
} catch (e) {
this.errToast.present({
message: 'Auto save error: Your changes are not saved.',
} as any)
this.errToast.present(e)
} finally {
this.saving = false
}

View File

@@ -59,9 +59,7 @@ export class DevInstructionsPage {
value: this.code,
})
} catch (e) {
this.errToast.present({
message: 'Auto save error: Your changes are not saved.',
} as any)
this.errToast.present(e)
} finally {
this.saving = false
}

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevManifestPage } from './dev-manifest.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
import { FormsModule } from '@angular/forms'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
const routes: Routes = [
{
path: '',
component: DevManifestPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
BackupReportPageModule,
FormsModule,
MonacoEditorModule,
],
declarations: [DevManifestPage],
})
export class DevManifestPageModule {}

View File

@@ -0,0 +1,17 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button
[defaultHref]="'/developer/projects/' + projectId"
></ion-back-button>
</ion-buttons>
<ion-title> Manifest </ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ngx-monaco-editor
[options]="editorOptions"
[(ngModel)]="manifest"
></ngx-monaco-editor>
</ion-content>

View File

@@ -0,0 +1,32 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import * as yaml from 'js-yaml'
import { take } from 'rxjs/operators'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'dev-manifest',
templateUrl: 'dev-manifest.page.html',
styleUrls: ['dev-manifest.page.scss'],
})
export class DevManifestPage {
projectId: string
editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true }
manifest: string = ''
constructor(
private readonly route: ActivatedRoute,
private readonly patchDb: PatchDbService,
) {}
ngOnInit() {
this.projectId = this.route.snapshot.paramMap.get('projectId')
this.patchDb
.watch$('ui', 'dev', this.projectId)
.pipe(take(1))
.subscribe(devData => {
this.manifest = yaml.dump(devData['basic-info'])
})
}
}

View File

@@ -29,11 +29,9 @@
<ion-button
slot="end"
fill="clear"
color="danger"
(click)="presentAlertDelete(entry.key, $event)"
(click)="presentAction(entry.key, $event)"
>
<ion-icon slot="start" name="close"></ion-icon>
Delete
<ion-icon name="ellipsis-horizontal-outline"></ion-icon>
</ion-button>
</ion-item>
</ion-content>

View File

@@ -1,5 +1,7 @@
import { Component } from '@angular/core'
import {
ActionSheetButton,
ActionSheetController,
AlertController,
LoadingController,
ModalController,
@@ -15,7 +17,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
import * as yaml from 'js-yaml'
import { v4 } from 'uuid'
import { DevData } from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ActivatedRoute } from '@angular/router'
import { DestroyService } from '@start9labs/shared'
@@ -40,6 +41,7 @@ export class DeveloperListPage {
private readonly route: ActivatedRoute,
private readonly destroy$: DestroyService,
private readonly patch: PatchDbService,
private readonly actionCtrl: ActionSheetController,
) {}
ngOnInit() {
@@ -75,6 +77,60 @@ export class DeveloperListPage {
await modal.present()
}
async presentAction(id: string, event: Event) {
event.stopPropagation()
const buttons: ActionSheetButton[] = [
{
text: 'Edit Name',
icon: 'pencil',
handler: () => {
this.openEditNameModal(id)
},
},
{
text: 'Delete',
icon: 'trash',
role: 'destructive',
handler: () => {
this.presentAlertDelete(id)
},
},
]
const action = await this.actionCtrl.create({
header: this.devData[id].name,
subHeader: 'Manage project',
mode: 'ios',
buttons,
})
await action.present()
}
async openEditNameModal(id: string) {
const curName = this.devData[id].name
const options: GenericInputOptions = {
title: 'Edit Name',
message: 'Edit the name of your project.',
label: 'Name',
useMask: false,
placeholder: curName,
nullable: true,
initialValue: curName,
buttonText: 'Save',
submitFn: (value: string) => this.editName(id, value),
}
const modal = await this.modalCtrl.create({
componentProps: { options },
cssClass: 'alertlike-modal',
presentingElement: await this.modalCtrl.getTop(),
component: GenericInputComponent,
})
await modal.present()
}
async createProject(name: string) {
// fail silently if duplicate project name
if (
@@ -104,14 +160,13 @@ export class DeveloperListPage {
await this.api.setDbValue({ pointer: `/dev`, value: { [id]: def } })
}
} catch (e) {
this.errToast.present({ message: `Error saving project data` } as any)
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async presentAlertDelete(id: string, event: Event) {
event.stopPropagation()
async presentAlertDelete(id: string) {
const alert = await this.alertCtrl.create({
header: 'Caution',
message: `Are you sure you want to delete this project?`,
@@ -132,6 +187,23 @@ export class DeveloperListPage {
await alert.present()
}
async editName(id: string, newName: string) {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async delete(id: string) {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
@@ -145,7 +217,7 @@ export class DeveloperListPage {
delete devDataToSave[id]
await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave })
} catch (e) {
this.errToast.present({ message: `Error deleting project data` } as any)
this.errToast.present(e)
} finally {
loader.dismiss()
}

View File

@@ -5,6 +5,10 @@ import { RouterModule, Routes } from '@angular/router'
import { DeveloperMenuPage } from './developer-menu.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
import { FormsModule } from '@angular/forms'
import { SharedPipesModule } from '../../../../../../shared/src/pipes/shared/shared.module'
const routes: Routes = [
{
@@ -20,6 +24,10 @@ const routes: Routes = [
RouterModule.forChild(routes),
BadgeMenuComponentModule,
BackupReportPageModule,
GenericFormPageModule,
FormsModule,
MonacoEditorModule,
SharedPipesModule,
],
declarations: [DeveloperMenuPage],
})

View File

@@ -4,11 +4,32 @@
<ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons>
<ion-title>{{ patchDb.data.ui.dev[projectId].name}}</ion-title>
<ion-buttons slot="end">
<ion-button routerLink="manifest">View Manifest</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-divider>Components</ion-item-divider>
<ion-item button (click)="openBasicInfoModal()">
<ion-icon slot="start" name="information-circle-outline"></ion-icon>
<ion-label>
<h2>Basic Info</h2>
<p>Complete basic info for your package</p>
</ion-label>
<ion-icon
slot="end"
color="success"
name="checkmark"
*ngIf="!(projectData['basic-info'] | empty)"
></ion-icon>
<ion-icon
slot="end"
color="warning"
name="remove-outline"
*ngIf="projectData['basic-info'] | empty"
></ion-icon>
</ion-item>
<ion-item button detail routerLink="instructions" routerDirection="forward">
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>

View File

@@ -1,20 +1,90 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { LoadingController, ModalController } from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { BasicInfo, getBasicInfoSpec } from './form-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { takeUntil } from 'rxjs/operators'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'
import * as yaml from 'js-yaml'
@Component({
selector: 'developer-menu',
templateUrl: 'developer-menu.page.html',
styleUrls: ['developer-menu.page.scss'],
providers: [DestroyService],
})
export class DeveloperMenuPage {
projectId: string
projectData: DevProjectData
constructor(
private readonly route: ActivatedRoute,
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
private readonly errToast: ErrorToastService,
private readonly destroy$: DestroyService,
public readonly patchDb: PatchDbService,
) {}
ngOnInit() {
this.projectId = this.route.snapshot.paramMap.get('projectId')
this.patchDb
.watch$('ui', 'dev', this.projectId)
.pipe(takeUntil(this.destroy$))
.subscribe(pd => {
this.projectData = pd
})
}
async openBasicInfoModal() {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'Basic Info',
spec: getBasicInfoSpec(this.projectData),
buttons: [
{
text: 'Save',
handler: basicInfo => {
basicInfo.description = {
short: basicInfo.short,
long: basicInfo.long,
}
delete basicInfo.short
delete basicInfo.long
this.saveBasicInfo(basicInfo as BasicInfo)
},
isSubmit: true,
},
],
},
})
await modal.present()
}
async saveBasicInfo(basicInfo: BasicInfo) {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.setDbValue({
pointer: `/dev/${this.projectId}/basic-info`,
value: basicInfo,
})
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
}

View File

@@ -0,0 +1,164 @@
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
export type BasicInfo = {
id: string
title: string
'service-version-number': string
'release-notes': string
license: string
'wrapper-repo': string
'upstream-repo'?: string
'support-site'?: string
'marketing-site'?: string
description: {
short: string
long: string
}
}
export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
const basicInfo = devData['basic-info']
return {
id: {
type: 'string',
name: 'ID',
description: 'The package identifier used by the OS',
placeholder: 'e.g. bitcoind',
nullable: false,
masked: false,
copyable: true,
pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
'pattern-description': 'Must be kebab case',
default: basicInfo?.id,
},
title: {
type: 'string',
name: 'Title',
description: 'A human readable service title',
placeholder: 'e.g. Bitcoin Core',
nullable: false,
masked: false,
copyable: true,
default: basicInfo ? basicInfo.title : devData.name,
},
'service-version-number': {
type: 'string',
name: 'Service Version Number',
description:
'Service version - accepts up to four digits, where the last confirms to revisions necessary for EmbassyOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service',
placeholder: 'e.g. 0.1.2.3',
nullable: false,
masked: false,
copyable: true,
pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
'pattern-description': 'Must be valid Emver version',
default: basicInfo?.['service-version-number'],
},
'release-notes': {
type: 'string',
name: 'Release Notes',
description: 'A human readable service title',
placeholder: 'e.g. Bitcoin Core',
nullable: false,
masked: false,
copyable: true,
textarea: true,
default: basicInfo?.['release-notes'],
},
license: {
type: 'enum',
name: 'License',
values: [
'gnu-agpl-v3',
'gnu-gpl-v3',
'gnu-lgpl-v3',
'mozilla-public-license-2.0',
'apache-license-2.0',
'mit',
'boost-software-license-1.0',
'the-unlicense',
'custom',
],
'value-names': {
'gnu-agpl-v3': 'GNU AGPLv3',
'gnu-gpl-v3': 'GNU GPLv3',
'gnu-lgpl-v3': 'GNU LGPLv3',
'mozilla-public-license-2.0': 'Mozilla Public License 2.0',
'apache-license-2.0': 'Apache License 2.0',
mit: 'mit',
'boost-software-license-1.0': 'Boost Software License 1.0',
'the-unlicense': 'The Unlicense',
custom: 'Custom',
},
description: 'Example description for enum select',
default: 'mit',
},
'wrapper-repo': {
type: 'string',
name: 'Wrapper Repo',
description:
'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks',
placeholder: 'e.g. www.github.com/example',
nullable: false,
masked: false,
copyable: true,
default: basicInfo?.['wrapper-repo'],
},
'upstream-repo': {
type: 'string',
name: 'Upstream Repo',
description: 'The original project repository URL',
placeholder: 'e.g. www.github.com/example',
nullable: true,
masked: false,
copyable: true,
default: basicInfo?.['upstream-repo'],
},
'support-site': {
type: 'string',
name: 'Support Site',
description: 'URL to the support site / channel for the project',
placeholder: 'e.g. www.start9labs.com',
nullable: true,
masked: false,
copyable: true,
default: basicInfo?.['support-site'],
},
'marketing-site': {
type: 'string',
name: 'Marketing Site',
description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. www.start9labs.com',
nullable: true,
masked: false,
copyable: true,
default: basicInfo?.['marketing-site'],
},
short: {
type: 'string',
name: 'Short Description',
description:
'This is the first description visible to the user in the marketplace',
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.short,
pattern: '^.{1,320}$',
'pattern-description': 'Must be shorter than 320 characters',
},
long: {
type: 'string',
name: 'Long Description',
description: `This description will display with additional details in the service's individual marketplace page`,
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.long,
pattern: '^.{1,5000}$',
'pattern-description': 'Must be shorter than 5000 characters',
},
}
}

View File

@@ -33,6 +33,13 @@ const routes: Routes = [
m => m.DevInstructionsPageModule,
),
},
{
path: 'projects/:projectId/manifest',
loadChildren: () =>
import('./dev-manifest/dev-manifest.module').then(
m => m.DevManifestPageModule,
),
},
]
@NgModule({

View File

@@ -89,6 +89,13 @@
>
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"

View File

@@ -24,6 +24,7 @@ import { Subscription } from 'rxjs'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MarketplacePkg } from 'src/app/services/api/api.types'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@Component({
selector: 'marketplace-show',
@@ -52,6 +53,7 @@ export class MarketplaceShowPage {
private readonly patch: PatchDbService,
private readonly embassyApi: ApiService,
private readonly marketplaceService: MarketplaceService,
public readonly localStorageService: LocalStorageService,
) {}
async ngOnInit() {

View File

@@ -142,9 +142,7 @@ export class MarketplacesPage {
try {
await this.marketplaceService.getMarketplaceData({}, url)
} catch (e) {
this.errToast.present({
message: `Could not connect to ${url}`,
} as any)
this.errToast.present(e)
loader.dismiss()
return
}
@@ -164,9 +162,7 @@ export class MarketplacesPage {
try {
await this.marketplaceService.load()
} catch (e) {
this.errToast.present({
message: `Error syncing marketplace data`,
} as any)
this.errToast.present(e)
} finally {
loader.dismiss()
}
@@ -219,7 +215,7 @@ export class MarketplacesPage {
const { name } = await this.marketplaceService.getMarketplaceData({}, url)
marketplace['known-hosts'][id] = { name, url }
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)
this.errToast.present(e)
loader.dismiss()
return
}
@@ -229,7 +225,7 @@ export class MarketplacesPage {
try {
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present({ message: `Error saving marketplace data` } as any)
this.errToast.present(e)
} finally {
loader.dismiss()
}
@@ -259,7 +255,7 @@ export class MarketplacesPage {
marketplace['known-hosts'][id] = { name, url }
marketplace['selected-id'] = id
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)
this.errToast.present(e)
loader.dismiss()
return
}
@@ -269,7 +265,7 @@ export class MarketplacesPage {
try {
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present({ message: `Error saving marketplace data` } as any)
this.errToast.present(e)
loader.dismiss()
return
}
@@ -279,9 +275,7 @@ export class MarketplacesPage {
try {
await this.marketplaceService.load()
} catch (e) {
this.errToast.present({
message: `Error syncing marketplace data`,
} as any)
this.errToast.present(e)
} finally {
loader.dismiss()
}