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 @@
+
+
+
\ 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}
- /> */}
+
);
}