Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin management page #24

Merged
merged 7 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/echo-app/src/renderer/src/dev-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
* All of these plugins have HMR enabled, regardless of whether you only have the plugins folder open
*/

export const getDevPlugins = () => {
return [import('character-plugin'), import('stash-plugin'), import('poe-log-plugin')]
export const importDevPlugin = (pluginName: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need to do this, importing the plugin doesn't really enabled and disable them. We can import the plugins and just selectively call start on the enabled ones. This list of plugins will always just be these 3 example plugins since the list is really just here for people to add the plugin they are working on locally but that will never be committed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason to do this was that I need a way to get a handle on the module and it's entry so that I call it's methods on the plugin-settings-page. I'm sure there is another way to do this, but couldn't figure it out.

Copy link
Contributor

@C3ntraX C3ntraX Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't set a dynamic variable as import.

We may have to write the file to the system with the plugins we want to be in dev enviornment with HMR enabled. The HMR should regonize this and will rename the imports. This would be the way. Its a bit complicated but it should work, because the HMR is listening for file changes. We just have to trigger a file change with valid imports and everything should work.

Copy link
Contributor

@C3ntraX C3ntraX Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its because the HMR changes the module names with static replacement.
import('character-plugin') would be => require('entry-2416da1sd') or something like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW HMR still worked with the solution of importing then one by one.

The switch statement that maps the stored plug-in configs to imports allows us to pseudo dynamically import.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but its hardcoded. With my idea, we could use your plugin to dynamially enable and disable plguins in HMR without this hardcoded switch. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we would let your plugin to be static enabled and the others are controlled by your plugin

Copy link
Contributor

@C3ntraX C3ntraX Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its basically this:
https://github.com/PoeStack/poestack-sage/blob/main/src/echo-app/bump-version.js
But the plugins writes this dynamically. And we could read every plugin within the folder echo-plugins and echo-plugin-examples.
But we have to ensure, that the package.json in echo-app has set all plugins as dependency, or the HMR can not resolve the plugins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@C3ntraX I think I see what you mean, but I think I need a little more help with how that would be implemented. I think I need you to create a gist of how this would work, so I can understand the pattern better.

@zach-herridge a few options for this PR:

  • Merge as is, and iterate on it
  • I can revert the dev plugin enablement code, and merge just the production plugin bit, then follow up with the solution @C3ntraX mentioned
  • Keep it open

I think no matter what I'll probably need some assistance in implementing the dynamic imports

switch (pluginName) {
case 'character-plugin-dev':
return import('character-plugin')
case 'stash-plugin-dev':
return import('stash-plugin')
case 'poe-log-plugin-dev':
return import('poe-log-plugin')
default:
return
}
}

export const getDevPluginNames = () => {
return ['character-plugin-dev', 'stash-plugin-dev', 'poe-log-plugin-dev']
}
66 changes: 54 additions & 12 deletions src/echo-app/src/renderer/src/plugin-page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import { HomeIcon, UserCircleIcon } from '@heroicons/react/24/outline'
import { CpuChipIcon, HomeIcon, UserCircleIcon } from '@heroicons/react/24/outline'
import { bind } from '@react-rxjs/core'
import { ECHO_ROUTER, EchoPluginHook } from 'echo-common'
import { ECHO_PLUGIN_CONFIG, ECHO_ROUTER, EchoPluginHook } from 'echo-common'
import { EchoRoute } from 'echo-common/dist/cjs/echo-router'
import fs from 'fs'
import os from 'os'
import * as path from 'path'
import React, { useEffect } from 'react'
import { ProfilePage } from './profile-page'
import { getDevPlugins } from './dev-plugins'
import { getDevPluginNames, importDevPlugin } from './dev-plugins'
import { PluginSettingsPage } from './plugin-settings-page'

const [useCurrentRoute] = bind(ECHO_ROUTER.currentRoute$)
const [useCurrentRoutes] = bind(ECHO_ROUTER.routes$)
Expand Down Expand Up @@ -44,31 +45,72 @@ export const PluginPage: React.FC = () => {
path: 'profile',
plugin: 'sage'
})
ECHO_ROUTER.registerRoute({
navItems: [
{
location: 'l-sidebar-b',
icon: CpuChipIcon
}
],
page: PluginSettingsPage,
path: 'plugin-settings',
plugin: 'sage'
})
}, [])

useEffect(() => {
if (import.meta.env.MODE === 'development') {
const imports = getDevPlugins()
imports.forEach((prom) => {
prom.then((entry) => {
const plugin: EchoPluginHook = entry.default()
plugin.start()
})
const pluginNames = getDevPluginNames()
const pluginConfigs = ECHO_PLUGIN_CONFIG.loadPluginConfigs()
pluginNames.forEach((pluginName) => {
const pluginConfig = pluginConfigs && pluginConfigs[pluginName]
if (pluginConfig) {
if (pluginConfig.enabled) {
const pluginImportPromise = importDevPlugin(pluginName)
pluginImportPromise?.then((entry) => {
const plugin: EchoPluginHook = entry.default()
plugin.start()
})
}
} else {
pluginConfigs[pluginName] = {
name: pluginName,
version: 'LOCAL',
enabled: false,
path: ''
}
}
})
ECHO_PLUGIN_CONFIG.writePluginConfigs(pluginConfigs)
} else {
function loadPlugins(baseDir: string) {
fs.readdir(baseDir, (err, files) => {
if (err) {
return console.log('Unable to scan directory: ' + err)
}
const pluginConfigs = ECHO_PLUGIN_CONFIG.loadPluginConfigs()
files.forEach(function (file) {
if (file.endsWith('.js')) {
const p = path.resolve(baseDir, file)
const entry = module.require(p)
const plugin: EchoPluginHook = entry?.()
plugin?.start()
const fileName = path.basename(p).replace(/\.[^/.]+$/, '')
const pluginConfig = fileName && pluginConfigs[fileName]
if (pluginConfig) {
if (pluginConfig.enabled) {
const entry = module.require(p)
const plugin: EchoPluginHook = entry()
plugin.start()
}
} else {
pluginConfigs[fileName] = {
name: fileName,
version: 'LOCAL',
enabled: false,
path: p
}
}
}
})
ECHO_PLUGIN_CONFIG.writePluginConfigs(pluginConfigs)
})
}
let pluginDir = path.resolve(os.homedir(), 'poestack-sage', 'plugins')
Expand Down
93 changes: 93 additions & 0 deletions src/echo-app/src/renderer/src/plugin-settings-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useEffect, useState } from 'react'

import {
ECHO_PLUGIN_CONFIG,
EchoPluginConfig,
EchoPluginConfigs,
EchoPluginHook
} from 'echo-common'
import * as path from 'path'
import { importDevPlugin } from './dev-plugins'

function filterPluginsByEnv(pluginConfigs: EchoPluginConfigs) {
return Object.values(pluginConfigs).filter(
(config) => (import.meta.env.MODE === 'development') === config.name.endsWith('-dev')
)
}

async function getPlugin(pluginConfig: EchoPluginConfig): Promise<EchoPluginHook> {
if (import.meta.env.MODE === 'development') {
const entry = await importDevPlugin(pluginConfig.name)
return entry.default()
} else {
const p = path.resolve(pluginConfig.path)
const entry = module.require(p)
const plugin: EchoPluginHook = entry()
return plugin
}
}

export const PluginSettingsPage: React.FC = () => {
useEffect(() => {
setAvailablePlugins(filterPluginsByEnv(ECHO_PLUGIN_CONFIG.loadPluginConfigs()))
}, [])

async function handleTogglePlugin(pluginConfig: EchoPluginConfig) {
const pluginConfigs = ECHO_PLUGIN_CONFIG.loadPluginConfigs()
const pluginEnabled = pluginConfigs[pluginConfig.name].enabled
const plugin = await getPlugin(pluginConfig)
if (!pluginEnabled) {
plugin.start()
} else {
plugin.destroy()
}
Comment on lines +41 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make await plugin.destroy() to give the plugin the chacne to write data before it gets destroyed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I see. destory calls unregister Route. Within the call the plugin could write the unregister after Promise.resolve()
Do you delete the plugin afterwards? Then I would wait for the plugin, if not it would make no difference I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there isn't any deletion in this PR. I wouldn't delete anything except by "uninstalling" it, which isn't implemented in this PR

pluginConfigs[pluginConfig.name].enabled = !pluginEnabled
const updatedConfigs = ECHO_PLUGIN_CONFIG.writePluginConfigs(pluginConfigs)
setAvailablePlugins(filterPluginsByEnv(updatedConfigs))
}

const [availablePlugins, setAvailablePlugins] = useState<EchoPluginConfig[]>([])

return (
<>
<div className="p-4 w-full h-full overflow-y-scroll">
<div className="flex flex-row">
<div className="flex flex-col">
<h1 className="font-semibold text-primary-accent">Plugins</h1>
</div>
</div>
<div className="pt-4 flex-row flex w-full">
<table className="bg-secondary-surface table-auto border-separate border-spacing-3 text-left">
<tr className="">
<th>Plugin Name</th>
<th>Version</th>
<th>Enabled</th>
</tr>
{availablePlugins?.length > 0 &&
availablePlugins.map((plugin) => (
<tr key={plugin.name}>
<td>{plugin.name}</td>
<td>{plugin.version}</td>
<td className="text-center">
<input
type="checkbox"
id={`${plugin.name}-enabled`}
name="enabled"
checked={plugin.enabled}
onChange={() => handleTogglePlugin(plugin)}
/>
</td>
</tr>
))}
{!availablePlugins?.length && (
<tr>
<td>No Plugins installed</td>
</tr>
)}
</table>
</div>
<div className="basis-1/4"></div>
</div>
</>
)
}
1 change: 1 addition & 0 deletions src/echo-common/src/echo-dir-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class EchoDirService {
const resolvedPath = path.resolve(this.homeDirPath, ...jsonPath)
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true })
fs.writeFileSync(resolvedPath + '.json', JSON.stringify(value))
return value
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/echo-common/src/echo-plugin-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ECHO_DIR } from './echo-dir-service'

export type EchoPluginConfig = {
name: string
version: string
enabled: boolean
path: string
}

export type EchoPluginConfigs = Record<string, EchoPluginConfig>

export class EchoPluginConfigService {
public loadPluginConfigs(): EchoPluginConfigs {
if (ECHO_DIR.existsJson('plugin-config')) {
const loadedPluginConfig: EchoPluginConfigs | null = ECHO_DIR.loadJson('plugin-config')
if (loadedPluginConfig) {
return loadedPluginConfig
}
}
return this.writePluginConfigs({})
}

public writePluginConfigs(pluginConfigObject: EchoPluginConfigs): EchoPluginConfigs {
return ECHO_DIR.writeJson(['plugin-config'], pluginConfigObject)
}
}

export const ECHO_PLUGIN_CONFIG = new EchoPluginConfigService()
6 changes: 5 additions & 1 deletion src/echo-common/src/echo-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export class EchoRouter {
}
}

public removeRoute(next: { plugin: string; path: string }) {}
public unregisterRoute(route: EchoRoute) {
this.routes$.next(
this.routes$.value.filter((r) => r.plugin !== route.plugin && r.path !== route.page)
)
}

public push(next: { plugin: string; path: string }) {
const nextRoute = this.routes$.value.find(
Expand Down
6 changes: 4 additions & 2 deletions src/echo-common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ECHO_ROUTER } from './echo-router'
import { ECHO_ROUTER, EchoRoute } from './echo-router'
import { CachedTask, CachedTaskEvent } from './cached-task'
import { ECHO_DIR } from './echo-dir-service'
import { EchoPluginHook } from './echo-plugin-hook'
Expand All @@ -21,13 +21,15 @@ import {
usePoeStashes
} from './poe-stash-service'
import { POE_LOG_SERVICE, PoeLogService } from './poe-log-service'
import { ECHO_PLUGIN_CONFIG, EchoPluginConfigs, EchoPluginConfig } from './echo-plugin-config'

export {
PoeStashService,
POE_STASH_SERVICE,
usePoeStashes,
usePoeStashItems,
ECHO_DIR,
ECHO_PLUGIN_CONFIG,
CachedTask,
ECHO_ROUTER,
PoeAccountService,
Expand All @@ -42,4 +44,4 @@ export {
POE_LOG_SERVICE
}

export type { EchoPluginHook, CachedTaskEvent }
export type { EchoPluginHook, CachedTaskEvent, EchoPluginConfigs, EchoPluginConfig, EchoRoute }
30 changes: 17 additions & 13 deletions src/echo-plugin-examples/character-plugin/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@

import App from './App'
import { UsersIcon } from '@heroicons/react/24/outline'
import { ECHO_ROUTER, EchoPluginHook } from 'echo-common'
import { ECHO_ROUTER, EchoPluginHook, EchoRoute } from 'echo-common'

const pluginRoute: EchoRoute = {
plugin: 'example-characters',
path: 'main',
page: App,
navItems: [
{
location: 'l-sidebar-m',
icon: UsersIcon
}
]
}

function start() {
ECHO_ROUTER.registerRoute({
plugin: 'example-characters',
path: 'main',
page: App,
navItems: [
{
location: 'l-sidebar-m',
icon: UsersIcon
}
]
})
ECHO_ROUTER.registerRoute(pluginRoute)
}

function destroy() {}
function destroy() {
ECHO_ROUTER.unregisterRoute(pluginRoute)
}

export default function (): EchoPluginHook {
return {
Expand Down
20 changes: 12 additions & 8 deletions src/echo-plugin-examples/poe-log-plugin/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

import App from './App'
import { DocumentTextIcon } from '@heroicons/react/24/outline'
import { ECHO_ROUTER, EchoPluginHook } from 'echo-common'
import { ECHO_ROUTER, EchoPluginHook, EchoRoute } from 'echo-common'

const pluginRoute: EchoRoute = {
plugin: 'example-log-plugin-stash',
path: 'main',
page: App,
navItems: [{ location: 'l-sidebar-m', icon: DocumentTextIcon }]
}

function start() {
ECHO_ROUTER.registerRoute({
plugin: 'example-log-plugin-stash',
path: 'main',
page: App,
navItems: [{ location: 'l-sidebar-m', icon: DocumentTextIcon }]
})
ECHO_ROUTER.registerRoute(pluginRoute)
}

function destroy() {}
function destroy() {
ECHO_ROUTER.unregisterRoute(pluginRoute)
}

export default function (): EchoPluginHook {
return {
Expand Down
20 changes: 12 additions & 8 deletions src/echo-plugin-examples/stash-plugin/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

import App from './App'
import { ArchiveBoxIcon } from '@heroicons/react/24/outline'
import { ECHO_ROUTER, EchoPluginHook } from 'echo-common'
import { ECHO_ROUTER, EchoPluginHook, EchoRoute } from 'echo-common'

const pluginRoute: EchoRoute = {
plugin: 'example-stash',
path: 'main',
page: App,
navItems: [{ location: 'l-sidebar-m', icon: ArchiveBoxIcon }]
}

function start() {
ECHO_ROUTER.registerRoute({
plugin: 'example-stash',
path: 'main',
page: App,
navItems: [{ location: 'l-sidebar-m', icon: ArchiveBoxIcon }]
})
ECHO_ROUTER.registerRoute(pluginRoute)
}

function destroy() {}
function destroy() {
ECHO_ROUTER.unregisterRoute(pluginRoute)
}

export default function (): EchoPluginHook {
return {
Expand Down