werks here
This commit is contained in:
@@ -112,7 +112,7 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
|
||||
//translate RPCs to socket-bound function metadata
|
||||
const loadedPlugins = this.pm.getLoadedPlugins()
|
||||
for(const name in loadedPlugins){
|
||||
loadedPlugins[name].exportRPCs().forEach(rpc => {
|
||||
loadedPlugins[name].backend.exportRPCs().forEach(rpc => {
|
||||
const info = this.rpcToRpcInfo(name, rpc)
|
||||
rpcInfos.push(info)
|
||||
})
|
||||
@@ -170,6 +170,13 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
|
||||
this.express = express()
|
||||
this.express.use(express.static('static'))
|
||||
|
||||
this.express.get('/plugins/:id'+".js", (request, response) => {
|
||||
let frontend = this.pm.getFrontend(request.params.id)
|
||||
response.status(200)
|
||||
response.set('Content-Type', 'application/javascript')
|
||||
response.send(frontend)
|
||||
})
|
||||
|
||||
this.httpServer = new http.Server(this.express)
|
||||
this.httpServer.listen(port, () => {
|
||||
logger.info('Admin panel listening for HTTP on *'+port)
|
||||
@@ -257,6 +264,4 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
|
||||
}
|
||||
}
|
||||
|
||||
(async() => {
|
||||
new FrontblockAdmin()
|
||||
})()
|
||||
new FrontblockAdmin()
|
||||
|
||||
@@ -22,7 +22,7 @@ export class PluginManagerPlugin extends PluginManager implements Plugin{
|
||||
constructor(){
|
||||
super()
|
||||
|
||||
this.loadedPlugins[this.name] = this
|
||||
this.loadedPlugins[this.name] = {backend:this, frontend: this.loadFrontend(this.name)}
|
||||
}
|
||||
|
||||
exportRPCs(): import("frontblock-generic/Plugin").socketioRPC[] {
|
||||
@@ -44,8 +44,12 @@ export class PluginManagerPlugin extends PluginManager implements Plugin{
|
||||
visibility: "private",
|
||||
rpc: async () => { return this.getLoadedPlugins() },
|
||||
type: 'call'
|
||||
},
|
||||
{
|
||||
},{
|
||||
name: "getLoadedPluginNames",
|
||||
visibility: "private",
|
||||
rpc: async () => { return this.getLoadedPluginNames() },
|
||||
type: 'call'
|
||||
},{
|
||||
name: "installPlugin",
|
||||
visibility: "private",
|
||||
rpc: async(pluginName:string) => { return await this.installPlugin(pluginName) },
|
||||
|
||||
@@ -29,7 +29,7 @@ type PluginVersion = {
|
||||
export type PluginVersioning = {
|
||||
[pluginName in string]?: PluginVersion | string
|
||||
}
|
||||
export type PluginMap = {[pluginName:string]: Plugin}
|
||||
export type PluginMap = {[pluginName:string]: {backend: Plugin, frontend:any}}
|
||||
|
||||
export type PluginManagerConfig = {
|
||||
resourceLocation: string,
|
||||
@@ -45,11 +45,13 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
super("PluginManager")
|
||||
this.cacheDir = path.join(this.conf.installDir, '.cache')
|
||||
this.initialize()
|
||||
this.loadPlugin("../../../../../../apiclient")
|
||||
|
||||
this.installPlugin("ApiClient")
|
||||
|
||||
}
|
||||
|
||||
getDefaultConfig(): PluginManagerConfig&PluginVersioning{
|
||||
return {resourceLocation: "https://gitea.frontblock.me/api/v1/repos/fb-vendor/", installDir: "./plugins"}
|
||||
return {resourceLocation: "https://gitea.frontblock.me/api/v1/repos/fb-vendor/", installDir: "plugins"}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +93,10 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
}
|
||||
}
|
||||
|
||||
public getLoadedPluginNames(): string[] {
|
||||
return Object.keys(this.loadedPlugins)
|
||||
}
|
||||
|
||||
public getLoadedPlugins(): PluginMap {
|
||||
return this.loadedPlugins
|
||||
}
|
||||
@@ -98,12 +104,6 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
|
||||
private async downloadFile(url: URL, dest: string) {
|
||||
try {
|
||||
fs.access(path.dirname(dest), err => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
fs.mkdirSync(path.dirname(dest), {recursive: true})
|
||||
}
|
||||
})
|
||||
|
||||
const file = fs.createWriteStream(dest)
|
||||
file.on('error', err => {throw new Error(err)})
|
||||
const response = await fetch(url)
|
||||
@@ -119,6 +119,7 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
try {
|
||||
logger.info("Downloading Plugin")
|
||||
|
||||
logger.error(this.conf.resourceLocation + pluginName + '/releases')
|
||||
const response = await fetch(this.conf.resourceLocation + pluginName + '/releases')
|
||||
if (!response.ok) throw new Error(response.statusText)
|
||||
|
||||
@@ -127,27 +128,44 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
|
||||
const latest = releases[0]
|
||||
const version = latest.tag_name
|
||||
const archiveURL = latest.assets.filter(asset => asset.name === pluginName + '.zip')[0].browser_download_url
|
||||
const archiveURL = latest.assets.filter(asset => asset.name === 'plugin.zip')[0].browser_download_url
|
||||
const checksumURL = latest.assets.filter(asset => asset.name === 'md5sum.txt')[0].browser_download_url
|
||||
if (!version || ! archiveURL || !checksumURL) throw new Error('Malformed response')
|
||||
|
||||
await this.downloadFile(archiveURL, path.join(this.cacheDir, pluginName, version, 'plugin.zip'))
|
||||
await this.downloadFile(checksumURL, path.join(this.cacheDir, pluginName, version, 'md5sum.txt'))
|
||||
await fs.access(path.join(this.cacheDir, pluginName), err => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
fs.mkdirSync(path.join(this.cacheDir, pluginName), {recursive: true})
|
||||
}
|
||||
})
|
||||
|
||||
await this.downloadFile(archiveURL, path.join(this.cacheDir, pluginName, 'plugin.zip'))
|
||||
await this.downloadFile(checksumURL, path.join(this.cacheDir, pluginName, 'md5sum.txt'))
|
||||
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private extractPlugin(pluginName: string){
|
||||
fs
|
||||
.createReadStream(path.join(this.cacheDir, pluginName, "plugin.zip"))
|
||||
.pipe(unzip.Extract({ path: path.join(this.conf.installDir,pluginName) }));
|
||||
}
|
||||
|
||||
public async installPlugin(pluginName: string) {
|
||||
try {
|
||||
await this.downloadPlugin(pluginName)
|
||||
//TODO do something
|
||||
await this.extractPlugin(pluginName)
|
||||
await this.loadPlugin(pluginName)
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
public getFrontend(pluginName: string){
|
||||
return this.loadedPlugins[pluginName].frontend.toString()
|
||||
}
|
||||
|
||||
public async uninstallPlugin(pluginName: string) {
|
||||
//TODO check for exists
|
||||
logger.info("Deleting plugin", pluginName)
|
||||
@@ -156,12 +174,18 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
|
||||
|
||||
public async loadPlugin(pluginName:string) {
|
||||
const pth = path.join(this.conf.installDir, pluginName, "Plugin")
|
||||
logger.info("Loading plugin from fs", pth)
|
||||
const clazz = await import(pth)
|
||||
const pth = path.join("..", "..", this.conf.installDir, pluginName, "static")
|
||||
logger.info("Loading plugin from fs", pth+"/Plugin")
|
||||
const clazz = await import(pth+"/Plugin")
|
||||
let obj:Plugin = new clazz.default()
|
||||
await obj.start()
|
||||
this.loadedPlugins[pluginName] = obj
|
||||
this.loadedPlugins[pluginName] = {backend: obj, frontend: this.loadFrontend(pluginName)}
|
||||
}
|
||||
|
||||
public loadFrontend(pluginName:string){
|
||||
const pth = path.join("..", "..", this.conf.installDir, pluginName, "static")
|
||||
logger.info("Loading frontend plugin from fs", path.join(__dirname,pth,"FrontendPlugin.js"))
|
||||
return fs.readFileSync(path.join(__dirname, pth,"FrontendPlugin.js"))
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +195,7 @@ export default class PluginManager extends ConfigLoader<PluginManagerConfig&Plug
|
||||
return
|
||||
}
|
||||
logger.info("Unloading plugin ", pluginName)
|
||||
this.loadedPlugins[pluginName].stop()
|
||||
this.loadedPlugins[pluginName].backend.stop()
|
||||
delete this.loadedPlugins[pluginName]
|
||||
}
|
||||
}
|
||||
@@ -2650,9 +2650,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.211",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.211.tgz",
|
||||
"integrity": "sha512-GZAiK3oHrs0K+LwH+HD+bdjZ17v40oQQdXbbd3dgrwgbENvazrGpcuIADSAREWnxzo9gADB1evuizrbXsnoU2Q==",
|
||||
"version": "1.3.212",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.212.tgz",
|
||||
"integrity": "sha512-H8z5Smi1s1u1zGegEBfbxUAzrxyk1JoRHHHrlNGfhxv3sTb+p/Jz7JDvrR4196Q/Ip8r4+XwWcLvKrUjFKoJAg==",
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
declare const fb
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
constructor() { }
|
||||
|
||||
getWidgetConfigs(): Promise<string[]> {
|
||||
return fb.PluginManager.getLoadedPluginNames()
|
||||
}
|
||||
|
||||
}
|
||||
+11
-9
@@ -27,7 +27,6 @@ SystemJS.config({ meta: { '*': { authorization: true } } });
|
||||
|
||||
import { AfterViewInit, Component, Compiler, Injector, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { DashboardService } from '../dashboard.service';
|
||||
import { WidgetConfig } from '../widget-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
@@ -42,29 +41,32 @@ export class DashboardComponent implements AfterViewInit {
|
||||
private injector: Injector) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.loadWidgets();
|
||||
setTimeout( () => {
|
||||
this.loadWidgets();
|
||||
}, 250)
|
||||
}
|
||||
|
||||
private async loadWidgets() {
|
||||
const widgets = await this.dashboardService.getWidgetConfigs().toPromise();
|
||||
const widgets = await this.dashboardService.getWidgetConfigs()
|
||||
widgets.forEach((widget) => this.createWidget(widget));
|
||||
}
|
||||
|
||||
private async createWidget(widget: WidgetConfig) {
|
||||
private async createWidget(pluginName: string) {
|
||||
// import external module bundle
|
||||
console.log(`Importing module bundle: ${widget.moduleBundlePath}`);
|
||||
const module = await SystemJS.import(widget.moduleBundlePath);
|
||||
|
||||
const module = await SystemJS.import("plugins/"+pluginName+".js");
|
||||
|
||||
console.log(module)
|
||||
// compile module
|
||||
const moduleFactory = await this.compiler.compileModuleAsync(module[widget.moduleName]);
|
||||
const moduleFactory = await this.compiler.compileModuleAsync(module["PluginModule"]);
|
||||
|
||||
// resolve component factory
|
||||
const moduleRef = moduleFactory.create(this.injector);
|
||||
const componentProvider = moduleRef.injector.get(widget.name);
|
||||
const componentProvider = moduleRef.injector.get("provider");
|
||||
const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(componentProvider);
|
||||
|
||||
// compile component
|
||||
console.log(`Creating widget: ${widget.name}`);
|
||||
console.log(`Creating widget: ${pluginName}`);
|
||||
this.content.createComponent(componentFactory);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -1,33 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { WidgetConfig } from './widget-config.model';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
private readonly url = 'widgets-repo/widgets.config.json';
|
||||
|
||||
getWidgetConfigs(): Observable<WidgetConfig[]> {
|
||||
return this.http.get<WidgetConfig[]>(this.url);
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
/* SystemJS module definition */
|
||||
declare var module: NodeModule;
|
||||
interface NodeModule {
|
||||
id: string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PluginModule } from './widget/module';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'exclamations',
|
||||
template: `
|
||||
<span style="color: blue">!!!</span>
|
||||
`
|
||||
})
|
||||
export class ANestedComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'APICLIENT', //!!!!
|
||||
template: `
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<div class="card-title">
|
||||
APICLIENT ${Object.keys(fb.ApiClient).join("<br>")}
|
||||
</div>
|
||||
<div class="card-text">
|
||||
Hello World <exclamations></exclamations>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class PluginComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PluginComponent } from './component';
|
||||
import { ANestedComponent } from './a-nested.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [
|
||||
PluginComponent,
|
||||
ANestedComponent
|
||||
],
|
||||
entryComponents: [PluginComponent],
|
||||
providers: [{
|
||||
provide: 'provider',
|
||||
useValue: PluginComponent
|
||||
}]
|
||||
})
|
||||
export class PluginModule { }
|
||||
Reference in New Issue
Block a user