diff --git a/cli/src/commands/plugin/create.ts b/cli/src/commands/plugin/create.ts index c18b3fb066..cd0466d58f 100644 --- a/cli/src/commands/plugin/create.ts +++ b/cli/src/commands/plugin/create.ts @@ -74,6 +74,21 @@ export default class Create extends Command { let repoUrl; + const commonHygenArgs = [ + 'plugin', + 'new', + '--name', + `${args.plugin_name}`, + '--type', + `${type}`, + '--display_name', + `${name}`, + '--plugins_path', + `${pluginsPath}`, + ]; + + const hygenArgs = !marketplace ? [...commonHygenArgs, '--docs_path', `${docsPath}`] : commonHygenArgs; + if (marketplace) { const buffer = fs.readFileSync(path.join('server', 'src', 'assets', 'marketplace', 'plugins.json'), 'utf8'); const pluginsJson = JSON.parse(buffer); @@ -89,21 +104,6 @@ export default class Create extends Command { }); } - const hygenArgs = [ - 'plugin', - 'new', - '--name', - `${args.plugin_name}`, - '--type', - `${type}`, - '--display_name', - `${name}`, - '--plugins_path', - `${pluginsPath}`, - '--docs_path', - `${docsPath}`, - ]; - CliUx.ux.action.start('creating plugin'); await runner(hygenArgs, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab6b1ad90a..d3379eb0a7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "date-fns": "^2.29.3", "deep-object-diff": "^1.1.9", "dompurify": "^3.0.0", + "dotenv": "^16.0.3", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", "driver.js": "^0.9.8", @@ -31098,6 +31099,14 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/draft-js": { "version": "0.11.7", "license": "MIT", @@ -68445,6 +68454,11 @@ "tslib": "^2.0.3" } }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, "draft-js": { "version": "0.11.7", "requires": { diff --git a/frontend/package.json b/frontend/package.json index 9de83a2717..be7d04f9de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "date-fns": "^2.29.3", "deep-object-diff": "^1.1.9", "dompurify": "^3.0.0", + "dotenv": "^16.0.3", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", "driver.js": "^0.9.8", diff --git a/frontend/src/MarketplacePage/InstalledPlugins.jsx b/frontend/src/MarketplacePage/InstalledPlugins.jsx index d2e9cfa417..c1b96ad1f1 100644 --- a/frontend/src/MarketplacePage/InstalledPlugins.jsx +++ b/frontend/src/MarketplacePage/InstalledPlugins.jsx @@ -1,41 +1,53 @@ import React from 'react'; import cx from 'classnames'; -import { marketplaceService, pluginsService } from '@/_services'; +import { pluginsService } from '@/_services'; import { toast } from 'react-hot-toast'; import Spinner from '@/_ui/Spinner'; import { capitalizeFirstLetter } from './utils'; -export const InstalledPlugins = ({ isActive }) => { - const [plugins, setPlugins] = React.useState([]); +export const InstalledPlugins = ({ + allPlugins = [], + installedPlugins, + fetching, + fetchPlugins, + ENABLE_MARKETPLACE_DEV_MODE, +}) => { + return ( +
+ {fetching && ( +
+ +
+ )} + {!fetching && allPlugins.length > 0 && ( +
+ {installedPlugins?.map((plugin) => { + const marketplacePlugin = allPlugins?.find((m) => m.id === plugin.pluginId); + const isUpdateAvailable = marketplacePlugin?.version !== plugin.version; + return ( + + ); + })} + {!fetching && installedPlugins?.length === 0 && ( +
+

No results found

+
+ )} +
+ )} +
+ ); +}; + +const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMode, isUpdateAvailable }) => { const [updating, setUpdating] = React.useState(false); - const [marketplacePlugins, setMarketplacePlugins] = React.useState([]); - const [fetching, setFetching] = React.useState(false); - - React.useEffect(() => { - marketplaceService - .findAll() - .then(({ data = [] }) => setMarketplacePlugins(data)) - .catch((error) => { - toast.error(error?.message || 'something went wrong'); - }); - }, [isActive]); - - const fetchPlugins = async () => { - setFetching(true); - const { data, error } = await pluginsService.findAll(); - setFetching(false); - - if (error) { - toast.error(error?.message || 'something went wrong'); - return; - } - - setPlugins(data); - }; - - React.useEffect(() => { - fetchPlugins(); - }, [isActive]); const deletePlugin = async ({ id, name }) => { var result = confirm('Are you sure you want to delete ' + name + '?'); @@ -70,77 +82,97 @@ export const InstalledPlugins = ({ isActive }) => { fetchPlugins(); }; + const reloadPlugin = async ({ id, name }) => { + setUpdating(true); + const { error } = await pluginsService.reloadPlugin(id); + setUpdating(false); + + if (error) { + toast.error(error?.message || `Unable to reload ${name}`); + return; + } + toast.success(`${name} reloaded`); + }; + return ( -
- {fetching && ( -
- -
- )} - {!fetching && ( -
- {plugins?.map((plugin) => { - const marketplacePlugin = marketplacePlugins.find((m) => m.id === plugin.pluginId); - const isUpdateAvailable = marketplacePlugin?.version !== plugin.version; - return ( -
-
-
-
-
- - - -
-
-
{plugin.name}
-
{plugin.description}
-
-
-
-
-
- - v{plugin.version}{' '} - {isUpdateAvailable && ( - updatePlugin(plugin, marketplacePlugin.version)} - > - - (click to update to v{marketplacePlugin.version}) - - - )} - -
-
-
deletePlugin(plugin)} - > - Remove -
-
-
-
-
- {updating && ( -
-
-
+
+
+
+
+
+ + + +
+
+
{plugin.name}
+
{plugin.description}
+
+
+ {isDevMode && ( + + )} +
+
+
+
+
+ + v{plugin.version}{' '} + {isUpdateAvailable && ( + updatePlugin(plugin, marketplacePlugin?.version)} + > + (click to update to v{marketplacePlugin?.version}) + )} + +
+
+
deletePlugin(plugin)} + > + Remove
- ); - })} - {!fetching && plugins?.length === 0 && ( -
-

No results found

- )} +
- )} + {updating && ( +
+
+
+ )} +
); }; + +InstalledPlugins.Plugin = InstalledPluginCard; diff --git a/frontend/src/MarketplacePage/MarketplacePlugins.jsx b/frontend/src/MarketplacePage/MarketplacePlugins.jsx index 2cf708f72f..97cda233ee 100644 --- a/frontend/src/MarketplacePage/MarketplacePlugins.jsx +++ b/frontend/src/MarketplacePage/MarketplacePlugins.jsx @@ -1,19 +1,10 @@ import React from 'react'; import { toast } from 'react-hot-toast'; import { MarketplaceCard } from './MarketplaceCard'; -import { marketplaceService, pluginsService } from '@/_services'; +import { pluginsService } from '@/_services'; -export const MarketplacePlugins = ({ isActive }) => { - const [plugins, setPlugins] = React.useState([]); +export const MarketplacePlugins = ({ allPlugins = [] }) => { const [installedPlugins, setInstalledPlugins] = React.useState({}); - React.useEffect(() => { - marketplaceService - .findAll() - .then(({ data = [] }) => setPlugins(data)) - .catch((error) => { - toast.error(error?.message || 'something went wrong'); - }); - }, [isActive]); React.useEffect(() => { pluginsService @@ -28,12 +19,16 @@ export const MarketplacePlugins = ({ isActive }) => { .catch((error) => { toast.error(error?.message || 'something went wrong'); }); + + return () => { + setInstalledPlugins({}); + }; }, []); return (
- {plugins?.map(({ id, name, repo, version, description }) => { + {allPlugins?.map(({ id, name, repo, version, description }) => { return ( { const [active, setActive] = React.useState('installed'); + const [marketplacePlugins, setMarketplacePlugins] = React.useState([]); + const [installedPlugins, setInstalledPlugins] = React.useState([]); + const [fetchingInstalledPlugins, setFetching] = React.useState(false); + + const ENABLE_MARKETPLACE_DEV_MODE = config.ENABLE_MARKETPLACE_DEV_MODE === 'true'; + + React.useEffect(() => { + marketplaceService + .findAll() + .then(({ data = [] }) => setMarketplacePlugins(data)) + .catch((error) => { + toast.error(error?.message || 'something went wrong'); + }); + + fetchPlugins(); + + () => { + setMarketplacePlugins([]); + setInstalledPlugins([]); + }; + }, [active]); + + const fetchPlugins = async () => { + setFetching(true); + const { data, error } = await pluginsService.findAll(); + setFetching(false); + + if (error) { + toast.error(error?.message || 'something went wrong'); + return; + } + + setInstalledPlugins(data); + }; return ( @@ -30,9 +67,15 @@ const MarketplacePage = ({ darkMode, switchDarkMode }) => {
{active === 'installed' ? ( - + ) : ( - + )}
diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index 01a450b634..0f4cbd9b8d 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -40,29 +40,30 @@ const DynamicForm = ({ React.useEffect(() => { const { properties } = schema; - if (isEmpty(properties)) return null; + if (!isEmpty(properties)) { + let fields = {}; + let encrpytedFieldsProps = {}; + const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']); - let fields = {}; - let encrpytedFieldsProps = {}; - const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']); + if (flipComponentDropdown) { + const selector = options?.[flipComponentDropdown?.key]?.value; + fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] }; + } else { + fields = { ...properties }; + } - if (flipComponentDropdown) { - const selector = options?.[flipComponentDropdown?.key]?.value; - fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] }; - } else { - fields = { ...properties }; + Object.keys(fields).map((key) => { + const { type, encrypted } = fields[key]; + if ((type === 'password' || encrypted) && !(key in computedProps)) { + //Editable encrypted fields only if datasource doesn't exists + encrpytedFieldsProps[key] = { + disabled: !!selectedDataSource?.id, + }; + } + }); + setComputedProps({ ...computedProps, ...encrpytedFieldsProps }); } - Object.keys(fields).map((key) => { - const { type, encrypted } = fields[key]; - if ((type === 'password' || encrypted) && !(key in computedProps)) { - //Editable encrypted fields only if datasource doesn't exists - encrpytedFieldsProps[key] = { - disabled: !!selectedDataSource?.id, - }; - } - }); - setComputedProps({ ...computedProps, ...encrpytedFieldsProps }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options]); diff --git a/frontend/src/_services/plugins.service.js b/frontend/src/_services/plugins.service.js index ba91693b04..14437b1c97 100644 --- a/frontend/src/_services/plugins.service.js +++ b/frontend/src/_services/plugins.service.js @@ -18,9 +18,14 @@ function deletePlugin(id) { return adapter.delete(`/plugins/${id}`); } +function reloadPlugin(id) { + return adapter.post(`/plugins/${id}/reload`); +} + export const pluginsService = { findAll, installPlugin, updatePlugin, deletePlugin, + reloadPlugin, }; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index f8e0cf51bc..cedc4c41c9 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -3,6 +3,7 @@ const webpack = require('webpack'); const path = require('path'); const CompressionPlugin = require('compression-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); +require('dotenv').config({ path: '../.env' }); const hash = require('string-hash'); const environment = process.env.NODE_ENV === 'production' ? 'production' : 'development'; @@ -169,6 +170,7 @@ module.exports = { COMMENT_FEATURE_ENABLE: process.env.COMMENT_FEATURE_ENABLE ?? true, ENABLE_TOOLJET_DB: process.env.ENABLE_TOOLJET_DB ?? true, ENABLE_MULTIPLAYER_EDITING: true, + ENABLE_MARKETPLACE_DEV_MODE: process.env.ENABLE_MARKETPLACE_DEV_MODE, TOOLJET_MARKETPLACE_URL: process.env.TOOLJET_MARKETPLACE_URL || 'https://tooljet-plugins-production.s3.us-east-2.amazonaws.com', }), diff --git a/marketplace/_templates/plugin/new/docs.ejs.t b/marketplace/_templates/plugin/new/docs.ejs.t deleted file mode 100644 index 8abaa751c0..0000000000 --- a/marketplace/_templates/plugin/new/docs.ejs.t +++ /dev/null @@ -1,16 +0,0 @@ ---- -to: <%= docs_path %>/docs/data-sources/<%= name %>.md ---- -<% - Display_name = h.capitalize(display_name) -%> -# <%= name %> - -ToolJet can connect to <%= Display_name %> datasource to read and write data. - -- [Connection](#connection) -- [Getting Started](#querying-<%= name %>) - -## Connection - -## Querying <%= Display_name %> operations \ No newline at end of file diff --git a/marketplace/_templates/plugin/new/packagejson.ejs.t b/marketplace/_templates/plugin/new/packagejson.ejs.t index 38de1878e3..aa2768d483 100644 --- a/marketplace/_templates/plugin/new/packagejson.ejs.t +++ b/marketplace/_templates/plugin/new/packagejson.ejs.t @@ -15,7 +15,8 @@ to: <%= plugins_path %>/plugins/<%= name %>/package.json ], "scripts": { "test": "echo \"Error: run tests from root\" && exit 1", - "build": "ncc build lib/index.ts -o dist" + "build": "ncc build lib/index.ts -o dist", + "watch": "ncc build lib/index.ts -o dist --watch" }, "homepage": "https://github.com/tooljet/tooljet#readme", "dependencies": { diff --git a/marketplace/lerna.json b/marketplace/lerna.json new file mode 100644 index 0000000000..aebebbab22 --- /dev/null +++ b/marketplace/lerna.json @@ -0,0 +1,5 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useWorkspaces": true, + "version": "0.0.0" +} diff --git a/marketplace/package.json b/marketplace/package.json index 2de149b6e5..d5b3775b9b 100644 --- a/marketplace/package.json +++ b/marketplace/package.json @@ -7,7 +7,13 @@ "version": "1.0.0", "devDependencies": { "aws-sdk": "^2.1326.0", + "lerna": "^6.4.1", "mime-types": "^2.1.35", "recursive-readdir": "^2.2.3" + }, + "scripts": { + "start:watch": "lerna run watch --stream --parallel", + "build": "npm run build --workspaces", + "start:dev": "npm run build && npm run start:watch" } } diff --git a/marketplace/plugins/github/lib/index.ts b/marketplace/plugins/github/lib/index.ts index 2c01e10efd..efe163588a 100644 --- a/marketplace/plugins/github/lib/index.ts +++ b/marketplace/plugins/github/lib/index.ts @@ -8,15 +8,9 @@ export default class Github implements QueryService { async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise { const operation: Operation = queryOptions.operation; - console.log('---octakit operation', { - queryOptions - - }); - const octokit:Octokit = await this.getConnection(sourceOptions); let result = {}; - try { switch (operation) { case Operation.GetUserInfo: diff --git a/marketplace/plugins/github/package.json b/marketplace/plugins/github/package.json index f578479171..cb8abf9256 100644 --- a/marketplace/plugins/github/package.json +++ b/marketplace/plugins/github/package.json @@ -12,7 +12,8 @@ ], "scripts": { "test": "echo \"Error: run tests from root\" && exit 1", - "build": "ncc build lib/index.ts -o dist" + "build": "ncc build lib/index.ts -o dist", + "watch": "ncc build lib/index.ts -o dist --watch" }, "homepage": "https://github.com/tooljet/tooljet#readme", "dependencies": { diff --git a/marketplace/plugins/plivo/package.json b/marketplace/plugins/plivo/package.json index c8a01e3b22..757a1ca092 100644 --- a/marketplace/plugins/plivo/package.json +++ b/marketplace/plugins/plivo/package.json @@ -12,7 +12,8 @@ ], "scripts": { "test": "echo \"Error: run tests from root\" && exit 1", - "build": "ncc build lib/index.ts -o dist" + "build": "ncc build lib/index.ts -o dist", + "watch": "ncc build lib/index.ts -o dist --watch" }, "homepage": "https://github.com/tooljet/tooljet#readme", "dependencies": { diff --git a/marketplace/plugins/s3/package.json b/marketplace/plugins/s3/package.json index 71ab13278f..a37407ed00 100644 --- a/marketplace/plugins/s3/package.json +++ b/marketplace/plugins/s3/package.json @@ -12,7 +12,8 @@ ], "scripts": { "test": "echo \"Error: run tests from root\" && exit 1", - "build": "ncc build lib/index.ts -o dist" + "build": "ncc build lib/index.ts -o dist", + "watch": "ncc build lib/index.ts -o dist --watch" }, "homepage": "https://github.com/tooljet/tooljet#readme", "dependencies": { diff --git a/server/src/controllers/plugins.controller.ts b/server/src/controllers/plugins.controller.ts index f78e2d10f5..39ffac4e59 100644 --- a/server/src/controllers/plugins.controller.ts +++ b/server/src/controllers/plugins.controller.ts @@ -77,4 +77,10 @@ export class PluginsController { return this.pluginsService.remove(id); } + + @Post(':id/reload') + @UseGuards(JwtAuthGuard) + async reload(@Param('id') id: string) { + return this.pluginsService.reload(id); + } } diff --git a/server/src/helpers/plugins.helper.ts b/server/src/helpers/plugins.helper.ts index cc01ad5fd8..a7e977e0dc 100644 --- a/server/src/helpers/plugins.helper.ts +++ b/server/src/helpers/plugins.helper.ts @@ -24,10 +24,13 @@ export class PluginsHelper { } async getService(pluginId: string, kind: string) { + const isMarketPlaceDev = process.env.ENABLE_MARKETPLACE_DEV_MODE === 'true'; + try { if (pluginId) { let decoded: string; - if (this.plugins[pluginId]) { + + if (!isMarketPlaceDev && this.plugins[pluginId]) { decoded = this.plugins[pluginId]; } else { const plugin = await this.pluginsRepository.findOne({ where: { id: pluginId }, relations: ['indexFile'] }); diff --git a/server/src/services/plugins.service.ts b/server/src/services/plugins.service.ts index be9f1cdb84..eb5b4867b0 100644 --- a/server/src/services/plugins.service.ts +++ b/server/src/services/plugins.service.ts @@ -14,6 +14,7 @@ import { dbTransactionWrap } from 'src/helpers/utils.helper'; import { UpdateFileDto } from '@dto/update-file.dto'; const jszipInstance = new jszip(); +const fs = require('fs'); @Injectable() export class PluginsService { @@ -194,23 +195,39 @@ export class PluginsService { return [indexFile, operationsFile, iconFile, manifestFile]; } - const fs = require('fs/promises'); + async function readFile(filePath) { + return new Promise((resolve, reject) => { + const readStream = fs.createReadStream(filePath, { encoding: 'utf8' }); + let fileContent = ''; + + readStream.on('data', (chunk) => { + fileContent += chunk; + }); + + readStream.on('error', (err) => { + reject(err); + }); + + readStream.on('end', () => { + resolve(fileContent); + }); + }); + } - // NOTE: big files are going to have a major impact on the memory consumption and speed of execution of the program - // as `fs.readFile` read the full content of the file in memory before returning the data. - // In this case, a better option is to read the file content using streams. const [indexFile, operationsFile, iconFile, manifestFile] = await Promise.all([ - fs.readFile(`../marketplace/plugins/${id}/dist/index.js`, 'utf8'), - fs.readFile(`../marketplace/plugins/${id}/lib/operations.json`, 'utf8'), - fs.readFile(`../marketplace/plugins/${id}/lib/icon.svg`, 'utf8'), - fs.readFile(`../marketplace/plugins/${id}/lib/manifest.json`, 'utf8'), + readFile(`../marketplace/plugins/${id}/dist/index.js`), + readFile(`../marketplace/plugins/${id}/lib/operations.json`), + readFile(`../marketplace/plugins/${id}/lib/icon.svg`), + readFile(`../marketplace/plugins/${id}/lib/manifest.json`), ]); return [indexFile, operationsFile, iconFile, manifestFile]; } fetchPluginFiles(id: string, repo: string) { - if (repo) return this.fetchPluginFilesFromRepo(repo); + if (repo && repo.length > 0) { + return this.fetchPluginFilesFromRepo(repo); + } return this.fetchPluginFilesFromS3(id); } @@ -230,4 +247,47 @@ export class PluginsService { async remove(id: string) { return await this.pluginsRepository.delete(id); } + + async reload(id: string) { + const queryRunner = this.connection.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const plugin = await this.findOne(id); + const { pluginId, repo, version } = plugin; + + const [index, operations, icon, manifest] = await this.fetchPluginFiles(pluginId, repo); + + const files = { index, operations, icon, manifest }; + + const uploadedFiles: { index?: File; operations?: File; icon?: File; manifest?: File } = {}; + await Promise.all( + Object.keys(files).map(async (key) => { + return await dbTransactionWrap(async (manager: EntityManager) => { + const file = files[key]; + const fileDto = new UpdateFileDto(); + fileDto.data = encode(file); + fileDto.filename = key; + uploadedFiles[key] = await this.filesService.update(plugin[`${key}FileId`], fileDto, manager); + }); + }) + ); + + const updatedPlugin = new Plugin(); + + updatedPlugin.id = plugin.id; + updatedPlugin.repo = repo || ''; + updatedPlugin.version = version; + + return this.pluginsRepository.save(updatedPlugin); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(error); + } finally { + await queryRunner.commitTransaction(); + await queryRunner.release(); + } + } }