diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index aac64d95b..3abcbc968 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -2,12 +2,32 @@ const fs = require('fs-extra'); const fetch = require('node-fetch'); const path = require('path'); const pacote = require('pacote'); -const { pluginstmpdir } = require('../utility/directories'); +const { pluginstmpdir, pluginsdir } = require('../utility/directories'); +const socket = require('../utility/socket'); + +async function loadPackageInfo(dir) { + const readmeFile = path.join(dir, 'README.md'); + const packageFile = path.join(dir, 'package.json'); + + if (!(await fs.exists(packageFile))) { + return null; + } + + let readme = null; + let manifest = null; + if (await fs.exists(readmeFile)) readme = await fs.readFile(readmeFile, { encoding: 'utf-8' }); + if (await fs.exists(packageFile)) manifest = JSON.parse(await fs.readFile(packageFile, { encoding: 'utf-8' })); + return { + readme, + manifest, + }; +} module.exports = { script_meta: 'get', async script({ packageName }) { - const data = await fs.readFile('/home/jena/jenasoft/dbgate-plugin-csv/lib/frontend.js', { + const file = path.join(pluginsdir(), packageName, 'lib', 'frontend.js'); + const data = await fs.readFile(file, { encoding: 'utf-8', }); return data; @@ -15,21 +35,54 @@ module.exports = { search_meta: 'get', async search({ filter }) { - const response = await fetch(`https://api.npms.io/v2/search?q=keywords:dbgate ${encodeURIComponent(filter)}`); + // const response = await fetch(`https://api.npms.io/v2/search?q=keywords:dbgate ${encodeURIComponent(filter)}`); + // const json = await response.json(); + // const { results } = json || {}; + // return (results || []).map((x) => x.package); + + const response = await fetch( + `https://www.npmjs.com/search/suggestions?q=dbgate-plugin ${encodeURIComponent(filter)}` + ); const json = await response.json(); - console.log(json); - const { results } = json || {}; - return results || []; + return json || []; }, - readme_meta: 'get', - async readme({ packageName }) { + info_meta: 'get', + async info({ packageName }) { const dir = path.join(pluginstmpdir(), packageName); if (!(await fs.exists(dir))) { await pacote.extract(packageName, dir); } - const file = path.join(dir, 'README.md'); - if (await fs.exists(file)) return await fs.readFile(file, { encoding: 'utf-8' }); - return ''; + return await loadPackageInfo(dir); + // return await { + // ...loadPackageInfo(dir), + // installed: loadPackageInfo(path.join(pluginsdir(), packageName)), + // }; + }, + + installed_meta: 'get', + async installed() { + const files = await fs.readdir(pluginsdir()); + return await Promise.all( + files.map((packageName) => + fs.readFile(path.join(pluginsdir(), packageName, 'package.json')).then((x) => JSON.parse(x)) + ) + ); + }, + + install_meta: 'post', + async install({ packageName }) { + const dir = path.join(pluginsdir(), packageName); + if (!(await fs.exists(dir))) { + await pacote.extract(packageName, dir); + } + socket.emitChanged(`installed-plugins-changed`); + }, + + uninstall_meta: 'post', + async uninstall({ packageName }) { + const dir = path.join(pluginsdir(), packageName); + await fs.rmdir(dir, { recursive: true }); + socket.emitChanged(`installed-plugins-changed`); }, }; diff --git a/packages/web/public/unknown.svg b/packages/web/public/unknown.svg new file mode 100644 index 000000000..ff30dd8cf --- /dev/null +++ b/packages/web/public/unknown.svg @@ -0,0 +1,15 @@ + + + + + + + + image/svg+xml + + + + + + ? + \ No newline at end of file diff --git a/packages/web/src/plugins/PluginIcon.js b/packages/web/src/plugins/PluginIcon.js deleted file mode 100644 index bfc0f211e..000000000 --- a/packages/web/src/plugins/PluginIcon.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export default function PluginIcon({ plugin, className = undefined }) { - return ( - - ); -} diff --git a/packages/web/src/plugins/PluginsList.js b/packages/web/src/plugins/PluginsList.js index 608fa5541..41b6de3a6 100644 --- a/packages/web/src/plugins/PluginsList.js +++ b/packages/web/src/plugins/PluginsList.js @@ -3,7 +3,7 @@ import styled from 'styled-components'; import useTheme from '../theme/useTheme'; import { openNewTab } from '../utility/common'; import { useSetOpenedTabs } from '../utility/globalState'; -import PluginIcon from './PluginIcon'; +import { extractPluginIcon, extractPluginAuthor } from '../plugins/manifestExtractors'; const Wrapper = styled.div` margin: 1px 3px 10px 5px; @@ -26,7 +26,7 @@ const Line = styled.div` display: flex; `; -const Icon = styled(PluginIcon)` +const Icon = styled.img` width: 50px; height: 50px; `; @@ -43,33 +43,33 @@ const Version = styled.div` margin-left: 5px; `; -function openPlugin(setOpenedTabs, plugin) { +function openPlugin(setOpenedTabs, packageManifest) { openNewTab(setOpenedTabs, { - title: plugin.package.name, + title: packageManifest.name, icon: 'icon plugin', tabComponent: 'PluginTab', props: { - plugin, + packageName: packageManifest.name, }, }); } -function PluginsListItem({ plugin }) { +function PluginsListItem({ packageManifest }) { const setOpenedTabs = useSetOpenedTabs(); const theme = useTheme(); return ( - openPlugin(setOpenedTabs, plugin)} theme={theme}> - + openPlugin(setOpenedTabs, packageManifest)} theme={theme}> + - {plugin.package.name} - {plugin.package.version} + {packageManifest.name} + {packageManifest.version} - {plugin.package.description} + {packageManifest.description} - {plugin.package.author && plugin.package.author.name} + {extractPluginAuthor(packageManifest)} @@ -79,8 +79,8 @@ function PluginsListItem({ plugin }) { export default function PluginsList({ plugins }) { return ( <> - {plugins.map((plugin) => ( - + {plugins.map((packageManifest) => ( + ))} ); diff --git a/packages/web/src/plugins/PluginsProvider.js b/packages/web/src/plugins/PluginsProvider.js index cac3487db..762e0b546 100644 --- a/packages/web/src/plugins/PluginsProvider.js +++ b/packages/web/src/plugins/PluginsProvider.js @@ -1,23 +1,40 @@ import React from 'react'; +import _ from 'lodash'; import axios from '../utility/axios'; +import { useInstalledPlugins } from '../utility/metadataLoaders'; const PluginsContext = React.createContext(null); export default function PluginsProvider({ children }) { - const [plugins, setPlugins] = React.useState(null); - const handleLoadPlugin = async () => { - const resp = await axios.request({ - method: 'get', - url: 'plugins/script', - params: { - plugin: 'csv', - }, - }); - const module = eval(resp.data); - console.log('MODULE', module); + const installedPlugins = useInstalledPlugins(); + const [plugins, setPlugins] = React.useState({}); + const handleLoadPlugins = async () => { + setPlugins((x) => + _.pick( + x, + installedPlugins.map((y) => y.name) + ) + ); + for (const installed of installedPlugins) { + if (!_.keys(plugins).includes(installed.name)) { + console.log('Loading module', installed.name); + const resp = await axios.request({ + method: 'get', + url: 'plugins/script', + params: { + packageName: installed.name, + }, + }); + const module = eval(resp.data); + setPlugins((v) => ({ + ...v, + [installed.name]: module, + })); + } + } }; React.useEffect(() => { - handleLoadPlugin(); - }, []); - return {children}; + handleLoadPlugins(); + }, [installedPlugins]); + return {children}; } diff --git a/packages/web/src/plugins/manifestExtractors.js b/packages/web/src/plugins/manifestExtractors.js new file mode 100644 index 000000000..715f2db65 --- /dev/null +++ b/packages/web/src/plugins/manifestExtractors.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; + +export function extractPluginIcon(packageManifest) { + const { links } = packageManifest || {}; + const { repository, homepage } = links || {}; + const tested = repository || homepage || packageManifest.homepage; + + if (tested) { + const match = tested.match(/https:\/\/github.com\/([^/]*)\/([^/]*)/); + if (match) { + return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/master/icon.svg`; + } + } + // eslint-disable-next-line no-undef + return `${process.env.PUBLIC_URL}/unknown.svg`; +} + +export function extractPluginAuthor(packageManifest) { + return _.isPlainObject(packageManifest.author) ? packageManifest.author.name : packageManifest.author; +} diff --git a/packages/web/src/tabs/PluginTab.js b/packages/web/src/tabs/PluginTab.js index e3576531b..27698f72f 100644 --- a/packages/web/src/tabs/PluginTab.js +++ b/packages/web/src/tabs/PluginTab.js @@ -2,14 +2,13 @@ import React from 'react'; import styled from 'styled-components'; import _ from 'lodash'; import ReactMarkdown from 'react-markdown'; -import ObjectListControl from '../utility/ObjectListControl'; -import { TableColumn } from '../utility/TableControl'; -import columnAppObject from '../appobj/columnAppObject'; -import constraintAppObject from '../appobj/constraintAppObject'; -import { useTableInfo, useDbCore } from '../utility/metadataLoaders'; import useTheme from '../theme/useTheme'; import useFetch from '../utility/useFetch'; import LoadingInfo from '../widgets/LoadingInfo'; +import { extractPluginIcon, extractPluginAuthor } from '../plugins/manifestExtractors'; +import FormStyledButton from '../widgets/FormStyledButton'; +import axios from '../utility/axios'; +import { useInstalledPlugins } from '../utility/metadataLoaders'; const WhitePage = styled.div` position: absolute; @@ -22,23 +21,84 @@ const WhitePage = styled.div` padding: 10px; `; -const Title = styled.div` - font-size: 20pt; - border-bottom: 1px solid ${(props) => props.theme.border}; +const Icon = styled.img` + width: 80px; + height: 80px; `; -export default function PluginTab({ plugin }) { +const Header = styled.div` + display: flex; + border-bottom: 1px solid ${(props) => props.theme.border}; + margin-bottom: 20px; + padding-bottom: 20px; +`; + +const HeaderBody = styled.div` + margin-left: 10px; +`; + +const Title = styled.div` + font-size: 20pt; +`; + +const HeaderLine = styled.div` + margin-top: 5px; +`; + +const Author = styled.span` + font-weight: bold; +`; + +const Version = styled.span``; + +function Delimiter() { + return | ; +} + +export default function PluginTab({ packageName }) { const theme = useTheme(); - const packageName = plugin.package.name; - const readme = useFetch({ + const installed = useInstalledPlugins(); + const info = useFetch({ params: { packageName }, - url: 'plugins/readme', + url: 'plugins/info', defaultValue: null, }); + const { readme, manifest } = info || {}; + const handleInstall = async () => { + axios.post('plugins/install', { packageName }); + }; + const handleUninstall = async () => { + axios.post('plugins/uninstall', { packageName }); + }; + return ( - {packageName} - {readme == null ? : {readme}} + {info == null || manifest == null ? ( + + ) : ( + <> +
+ + + {packageName} + + {extractPluginAuthor(manifest)} + + {manifest.version && manifest.version} + + + {!installed.find((x) => x.name == packageName) && ( + + )} + {!!installed.find((x) => x.name == packageName) && ( + + )} + + +
+ {readme} + + )}
); } diff --git a/packages/web/src/utility/metadataLoaders.js b/packages/web/src/utility/metadataLoaders.js index 583075919..5ac8b597e 100644 --- a/packages/web/src/utility/metadataLoaders.js +++ b/packages/web/src/utility/metadataLoaders.js @@ -88,6 +88,12 @@ const connectionListLoader = () => ({ reloadTrigger: `connection-list-changed`, }); +const insttalledPluginsLoader = () => ({ + url: 'plugins/installed', + params: {}, + reloadTrigger: `installed-plugins-changed`, +}); + async function getCore(loader, args) { const { url, params, reloadTrigger, transform } = loader(args); const key = stableStringify({ url, ...params }); @@ -243,3 +249,10 @@ export function getArchiveFolders(args) { export function useArchiveFolders(args) { return useCore(archiveFoldersLoader, args); } + +export function getInstalledPlugins(args) { + return getCore(insttalledPluginsLoader, args) || []; +} +export function useInstalledPlugins(args) { + return useCore(insttalledPluginsLoader, args) || []; +} diff --git a/packages/web/src/widgets/PluginsWidget.js b/packages/web/src/widgets/PluginsWidget.js index 9796fdf05..6479cfa51 100644 --- a/packages/web/src/widgets/PluginsWidget.js +++ b/packages/web/src/widgets/PluginsWidget.js @@ -1,37 +1,17 @@ import React from 'react'; -import _ from 'lodash'; - -import { AppObjectList } from '../appobj/AppObjectList'; -import { useCurrentArchive, useSetCurrentArchive } from '../utility/globalState'; import { SearchBoxWrapper, WidgetsInnerContainer } from './WidgetStyles'; import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar'; -import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders'; -import archiveFolderAppObject from '../appobj/archiveFolderAppObject'; -import archiveFileAppObject from '../appobj/archiveFileAppObject'; +import { useInstalledPlugins } from '../utility/metadataLoaders'; import SearchInput from './SearchInput'; -import InlineButton from './InlineButton'; -import axios from '../utility/axios'; import useFetch from '../utility/useFetch'; import PluginsList from '../plugins/PluginsList'; function InstalledPluginsList() { - // const folders = useArchiveFolders(); - // const [filter, setFilter] = React.useState(''); - - // const setArchive = useSetCurrentArchive(); - - // const handleRefreshFolders = () => { - // axios.post('archive/refresh-folders', {}); - // }; + const plugins = useInstalledPlugins(); return ( - {/* setArchive(archive.name)} - filter={filter} - /> */} + ); }