Trying code editor (#1173)

Implement hidden dev service packaging tools.

Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Drew Ansbacher
2022-02-08 13:00:40 -07:00
committed by GitHub
parent 79e4c6880a
commit 9d3f0a9d2b
29 changed files with 1644 additions and 877 deletions

View File

@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevConfigPage } from './dev-config.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.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: DevConfigPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
SharingModule,
BackupReportPageModule,
FormsModule,
MonacoEditorModule,
],
declarations: [DevConfigPage],
})
export class DevConfigPageModule {}

View File

@@ -0,0 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons>
<ion-title>Config</ion-title>
<ion-buttons slot="end">
<ion-button (click)="submit()">Preview</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ngx-monaco-editor
[options]="editorOptions"
[(ngModel)]="code"
></ngx-monaco-editor>
</ion-content>

View File

@@ -0,0 +1,106 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import * as yaml from 'js-yaml'
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
import { ConfigSpec } from '../../../pkg-config/config-types'
import { ErrorToastService } from '../../../services/error-toast.service'
@Component({
selector: 'dev-config',
templateUrl: 'dev-config.page.html',
styleUrls: ['dev-config.page.scss'],
})
export class DevConfigPage {
editorOptions = { theme: 'vs-dark', language: 'yaml' }
code: string
constructor(
private readonly errToast: ErrorToastService,
private readonly modalCtrl: ModalController,
) {}
ngOnInit() {
this.code = yaml
.dump(SAMPLE_CODE)
.replace(/warning:/g, '# Optional\n warning:')
}
async submit() {
let doc: any
try {
doc = yaml.load(this.code)
} catch (e) {
this.errToast.present(e)
}
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'Config Sample',
spec: JSON.parse(JSON.stringify(doc, null, 2)),
buttons: [
{
text: 'OK',
handler: () => {
return
},
isSubmit: true,
},
],
},
})
await modal.present()
}
}
const SAMPLE_CODE: ConfigSpec = {
'sample-string': {
type: 'string',
name: 'Example String Input',
nullable: false,
masked: false,
copyable: false,
// optional
warning: null,
description: 'Example description for required string input.',
default: null,
placeholder: 'Enter string value',
pattern: '^[a-zA-Z0-9! _]+$',
'pattern-description': 'Must be alphanumeric (may contain underscore).',
},
'sample-number': {
type: 'number',
name: 'Example Number Input',
nullable: false,
range: '[5,1000000]',
integral: true,
// optional
warning: 'Example warning to display when changing this number value.',
units: 'ms',
description: 'Example description for optional number input.',
default: null,
placeholder: 'Enter number value',
},
'sample-boolean': {
type: 'boolean',
name: 'Example Boolean Toggle',
// optional
warning: null,
description: 'Example description for boolean toggle',
default: true,
},
'sample-enum': {
type: 'enum',
name: 'Example Enum Select',
values: ['red', 'blue', 'green'],
'value-names': {
red: 'Red',
blue: 'Blue',
green: 'Green',
},
// optional
warning: 'Example warning to display when changing this enum value.',
description: 'Example description for enum select',
default: 'red',
},
}

View File

@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevInstructionsPage } from './dev-instructions.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.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: DevInstructionsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
SharingModule,
BackupReportPageModule,
FormsModule,
MonacoEditorModule,
],
declarations: [DevInstructionsPage],
})
export class DevInstructionsPageModule {}

View File

@@ -0,0 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons>
<ion-title>Instructions</ion-title>
<ion-buttons slot="end">
<ion-button (click)="submit()">Preview</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ngx-monaco-editor
[options]="editorOptions"
[(ngModel)]="code"
></ngx-monaco-editor>
</ion-content>

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
@Component({
selector: 'dev-instructions',
templateUrl: 'dev-instructions.page.html',
styleUrls: ['dev-instructions.page.scss'],
})
export class DevInstructionsPage {
editorOptions = { theme: 'vs-dark', language: 'markdown' }
code: string = `# Create Instructions using Markdown! :)`
constructor(private readonly modalCtrl: ModalController) {}
async submit() {
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Instructions Sample',
content: this.code,
},
component: MarkdownPage,
})
await modal.present()
}
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DeveloperPage } from './developer-list.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
const routes: Routes = [
{
path: '',
component: DeveloperPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
SharingModule,
BackupReportPageModule,
],
declarations: [DeveloperPage],
})
export class DeveloperPageModule {}

View File

@@ -0,0 +1,26 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Developer Tools</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-divider>Components</ion-item-divider>
<ion-item button detail (click)="navToInstructions()">
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>
<h2>Instructions Generator</h2>
<p>Create instructions and see how they will appear to the end user</p>
</ion-label>
</ion-item>
<ion-item button detail (click)="navToConfig()">
<ion-icon slot="start" name="construct-outline"></ion-icon>
<ion-label>
<h2>Config Generator</h2>
<p>Edit the config with YAML and see it in real time</p>
</ion-label>
</ion-item>
</ion-content>

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { NavController } from '@ionic/angular'
@Component({
selector: 'developer-list',
templateUrl: 'developer-list.page.html',
styleUrls: ['developer-list.page.scss'],
})
export class DeveloperPage {
constructor(
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
) {}
navToConfig() {
this.navCtrl.navigateForward(['config'], { relativeTo: this.route })
}
navToInstructions() {
this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route })
}
}

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./developer-list/developer-list.module').then(
m => m.DeveloperPageModule,
),
},
{
path: 'config',
loadChildren: () =>
import('./dev-config/dev-config.module').then(m => m.DevConfigPageModule),
},
{
path: 'instructions',
loadChildren: () =>
import('./dev-instructions/dev-instructions.module').then(
m => m.DevInstructionsPageModule,
),
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DeveloperRoutingModule {}

View File

@@ -3,22 +3,26 @@
<ion-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<ion-title>Preferences</ion-title>
<ion-title (click)="addClick()">Preferences</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group *ngIf="patch.data['server-info'] as server">
<ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName()">
<ion-label>{{ fields['name'].name }}</ion-label>
<ion-label>Device Name</ion-label>
<ion-note slot="end">{{ patch.data.ui.name || defaultName }}</ion-note>
</ion-item>
<ion-item button (click)="serverConfig.presentAlert('share-stats', server['share-stats'])">
<ion-item
button
(click)="serverConfig.presentAlert('share-stats', server['share-stats'])"
>
<ion-label>Auto Report Bugs</ion-label>
<ion-note slot="end">{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note>
<ion-note slot="end"
>{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note
>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('password')">
@@ -27,15 +31,15 @@
</ion-item> -->
<ion-item-divider>Marketplace</ion-item-divider>
<ion-item button (click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])">
<ion-item
button
(click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])"
>
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}</ion-note>
<ion-note slot="end"
>{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled'
}}</ion-note
>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', server['package-marketplace'])">
<ion-label>Package Marketplace</ion-label>
<ion-note slot="end">{{ server['package-marketplace'] }}</ion-note>
</ion-item> -->
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -1,10 +1,18 @@
import { Component, ViewChild } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { IonContent, LoadingController, ModalController } from '@ionic/angular'
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import {
IonContent,
LoadingController,
ModalController,
ToastController,
} from '@ionic/angular'
import {
GenericInputComponent,
GenericInputOptions,
} from 'src/app/modals/generic-input/generic-input.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { LocalStorageService } from '../../../services/local-storage.service'
@Component({
selector: 'preferences',
@@ -13,26 +21,28 @@ import { ServerConfigService } from 'src/app/services/server-config.service'
})
export class PreferencesPage {
@ViewChild(IonContent) content: IonContent
fields = fields
defaultName: string
clicks = 0
constructor (
constructor(
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly api: ApiService,
public readonly serverConfig: ServerConfigService,
private readonly toastCtrl: ToastController,
private readonly localStorageService: LocalStorageService,
public readonly patch: PatchDbService,
) { }
) {}
ngOnInit () {
ngOnInit() {
this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}`
}
ngAfterViewInit () {
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
async presentModalName (): Promise<void> {
async presentModalName(): Promise<void> {
const options: GenericInputOptions = {
title: 'Edit Device Name',
message: 'This is for your reference only.',
@@ -42,7 +52,8 @@ export class PreferencesPage {
nullable: true,
initialValue: this.patch.getData().ui.name,
buttonText: 'Save',
submitFn: (value: string) => this.setDbValue('name', value || this.defaultName),
submitFn: (value: string) =>
this.setDbValue('name', value || this.defaultName),
}
const modal = await this.modalCtrl.create({
@@ -55,7 +66,7 @@ export class PreferencesPage {
await modal.present()
}
private async setDbValue (key: string, value: string): Promise<void> {
async setDbValue(key: string, value: any): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
@@ -69,14 +80,22 @@ export class PreferencesPage {
loader.dismiss()
}
}
}
const fields: ConfigSpec = {
'name': {
name: 'Device Name',
type: 'string',
nullable: false,
masked: false,
copyable: false,
},
async addClick() {
this.clicks++
if (this.clicks >= 5) {
this.clicks = 0
const newVal = await this.localStorageService.toggleShowDevTools()
const toast = await this.toastCtrl.create({
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
position: 'bottom',
duration: 1000,
})
await toast.present()
}
setTimeout(() => {
this.clicks = Math.max(this.clicks - 1, 0)
}, 10000)
}
}