Merge pull request #1344 from ethereum/react-plugin-manager

React Plugin Manager
pull/1525/head
bunsenstraat 3 years ago committed by GitHub
commit 5b546a73c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 2
      apps/remix-ide-e2e/src/commands/removeFile.ts
  3. 48
      apps/remix-ide-e2e/src/tests/pluginManager.spec.ts
  4. 5
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  5. 3
      apps/remix-ide-e2e/src/tests/verticalIconsPanel.spec.ts
  6. 129
      apps/remix-ide/src/app/components/local-plugin.js
  7. 292
      apps/remix-ide/src/app/components/plugin-manager-component.js
  8. 13
      apps/remix-ide/src/app/components/plugin-manager-settings.js
  9. 6
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  10. 4
      libs/remix-ui/plugin-manager/.babelrc
  11. 19
      libs/remix-ui/plugin-manager/.eslintrc
  12. 7
      libs/remix-ui/plugin-manager/README.md
  13. 1
      libs/remix-ui/plugin-manager/src/index.ts
  14. 54
      libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx
  15. 36
      libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCardContainer.tsx
  16. 59
      libs/remix-ui/plugin-manager/src/lib/components/InactivePluginCard.tsx
  17. 47
      libs/remix-ui/plugin-manager/src/lib/components/InactivePluginCardContainer.tsx
  18. 218
      libs/remix-ui/plugin-manager/src/lib/components/LocalPluginForm.tsx
  19. 20
      libs/remix-ui/plugin-manager/src/lib/components/moduleHeading.tsx
  20. 144
      libs/remix-ui/plugin-manager/src/lib/components/permissionsSettings.tsx
  21. 66
      libs/remix-ui/plugin-manager/src/lib/components/rootView.tsx
  22. 78
      libs/remix-ui/plugin-manager/src/lib/custom-hooks/useLocalStorage.ts
  23. 14
      libs/remix-ui/plugin-manager/src/lib/reducers/pluginManagerReducer.ts
  24. 96
      libs/remix-ui/plugin-manager/src/lib/remix-ui-plugin-manager.css
  25. 29
      libs/remix-ui/plugin-manager/src/lib/remix-ui-plugin-manager.tsx
  26. 208
      libs/remix-ui/plugin-manager/src/types.d.ts
  27. 17
      libs/remix-ui/plugin-manager/tsconfig.json
  28. 13
      libs/remix-ui/plugin-manager/tsconfig.lib.json
  29. 5
      nx.json
  30. 221
      package-lock.json
  31. 3
      package.json
  32. 1
      tsconfig.base.json
  33. 12
      workspace.json

1
.gitignore vendored

@ -51,4 +51,3 @@ testem.log
# System Files
.DS_Store
Thumbs.db

@ -34,7 +34,7 @@ function removeFile (browser: NightwatchBrowser, path: string, workspace: string
contextMenuClick(document.querySelector('[data-path="' + path + '"]'))
}, [path], function () {
browser
.waitForElementVisible('#menuitemdelete')
.waitForElementVisible('#menuitemdelete', 60000)
.click('#menuitemdelete')
.pause(2000)
.perform(() => {

@ -2,6 +2,10 @@
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
declare global {
interface Window { testmode: boolean; }
}
const testData = {
pluginName: 'remixIde',
pluginDisplayName: 'Remix IDE',
@ -17,6 +21,7 @@ module.exports = {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.pause(3000)
.click('*[plugin="pluginManager"]')
.pause(3000)
.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.assert.containsText('*[data-id="sidePanelSwapitTitle"]', 'PLUGIN MANAGER')
},
@ -36,7 +41,8 @@ module.exports = {
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtonZoKrates"]')
.clearValue('*[data-id="pluginManagerComponentSearchInput"]')
.click('*[data-id="pluginManagerComponentSearchInput"]')
.keys(browser.Keys.ENTER)
.keys(browser.Keys.SPACE)
.keys(browser.Keys.BACK_SPACE)
},
'Should activate plugins': function (browser: NightwatchBrowser) {
@ -46,7 +52,7 @@ module.exports = {
.pause(2000)
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtondebugger"]', 60000)
.scrollAndClick('*[data-id="pluginManagerComponentActivateButtonvyper"]')
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonvyper"]', 60000)
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonvyper"]', 70000)
.scrollAndClick('*[data-id="pluginManagerComponentActivateButtonZoKrates"]')
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonZoKrates"]', 60000)
},
@ -103,37 +109,42 @@ module.exports = {
'Should connect a local plugin': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.execute(function () {
window.testmode = true
})
.click('*[data-id="pluginManagerComponentPluginSearchButton"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.click('*[data-id="modalDialogModalBody"]')
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalBody-react"]')
.waitForElementVisible('*[data-id="localPluginName"]')
.setValue('*[data-id="localPluginName"]', testData.pluginName)
.setValue('*[data-id="localPluginDisplayName"]', testData.pluginDisplayName)
.setValue('*[data-id="localPluginUrl"]', testData.pluginUrl)
.clearValue('*[data-id="localPluginName"]').setValue('*[data-id="localPluginName"]', testData.pluginName)
.clearValue('*[data-id="localPluginDisplayName"]').setValue('*[data-id="localPluginDisplayName"]', testData.pluginDisplayName)
.clearValue('*[data-id="localPluginUrl"]').setValue('*[data-id="localPluginUrl"]', testData.pluginUrl)
.click('*[data-id="localPluginRadioButtoniframe"]')
.click('*[data-id="localPluginRadioButtonsidePanel"]')
.click('*[data-id="modalDialogModalFooter"]')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonremixIde"]', 100000)
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalFooter-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react')
// .modalFooterOKClick()
// .waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonremixIde"]', 60000)
},
'Should display error message for creating already existing plugin': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.click('*[data-id="pluginManagerComponentPluginSearchButton"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.click('*[data-id="modalDialogModalBody"]')
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalBody-react"]')
.waitForElementVisible('*[data-id="localPluginName"]')
.clearValue('*[data-id="localPluginName"]').setValue('*[data-id="localPluginName"]', testData.pluginName)
.clearValue('*[data-id="localPluginDisplayName"]').setValue('*[data-id="localPluginDisplayName"]', testData.pluginDisplayName)
.clearValue('*[data-id="localPluginUrl"]').setValue('*[data-id="localPluginUrl"]', testData.pluginUrl)
.click('*[data-id="localPluginRadioButtoniframe"]')
.click('*[data-id="localPluginRadioButtonsidePanel"]')
.click('*[data-id="modalDialogModalFooter"]')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react"]', 60000)
.click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react"]')
// .modalFooterOKClick()
// .pause(2000)
.waitForElementVisible('*[data-shared="tooltipPopup"]', 60000)
.pause(5000)
.waitForElementVisible('*[data-shared="tooltipPopup"]:nth-last-of-type(1)')
.pause(2000)
.assert.containsText('*[data-shared="tooltipPopup"]:nth-last-of-type(1)', 'Cannot create Plugin : This name has already been used')
.assert.containsText('*[data-shared="tooltipPopup"]', 'Cannot create Plugin : This name has already been used')
},
'Should load back installed plugins after reload': function (browser: NightwatchBrowser) {
@ -143,8 +154,9 @@ module.exports = {
.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.pause(3000)
.perform((done) => {
// const filtered = plugins.filter(plugin => plugin !== 'testremixIde') // remove this when localplugin bug is resolved
plugins.forEach(plugin => {
if (plugin !== testData.pluginName) {
if (plugin !== testData.pluginName && plugin !== 'testremixIde') {
browser.waitForElementVisible(`[plugin="${plugin}"`)
}
})

@ -104,7 +104,7 @@ module.exports = {
'Close Remixd': function (browser) {
browser
.clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_remixd"] button')
.scrollAndClick('#pluginManager *[data-id="pluginManagerComponentDeactivateButtonremixd"]')
.end()
}
}
@ -121,10 +121,11 @@ function runTests (browser: NightwatchBrowser) {
.waitForElementVisible('#icon-panel', 2000)
.clickLaunchIcon('filePanel')
.clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_remixd"] button')
.scrollAndClick('#pluginManager *[data-id="pluginManagerComponentActivateButtonremixd"]')
.waitForElementVisible('#modal-footer-ok', 2000)
.pause(2000)
.click('#modal-footer-ok')
// .click('*[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-path="folder1"]')
.click('[data-path="folder1"]')

@ -27,7 +27,6 @@ module.exports = {
.click('*[id="menuitemdeactivate"]')
.click('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindpluginManager"]')
.scrollInto('*[data-id="pluginManagerComponentActivateButtondebugger"]')
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtondebugger"]')
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtondebugPlugin"]')
}
}

@ -1,129 +0,0 @@
/* global localStorage */
const yo = require('yo-yo')
const modalDialog = require('../ui/modaldialog')
const defaultProfile = {
methods: [],
location: 'sidePanel',
type: 'iframe'
}
module.exports = class LocalPlugin {
/**
* Open a modal to create a local plugin
* @param {Profile[]} plugins The list of the plugins in the store
* @returns {Promise<{api: any, profile: any}>} A promise with the new plugin profile
*/
open (plugins) {
this.profile = JSON.parse(localStorage.getItem('plugins/local')) || defaultProfile
return new Promise((resolve, reject) => {
const onValidation = () => {
try {
const profile = this.create()
resolve(profile)
} catch (err) {
reject(err)
}
}
modalDialog('Local Plugin', this.form(),
{ fn: () => onValidation() },
{ fn: () => resolve() }
)
})
}
/**
* Create the object to add to the plugin-list
*/
create () {
const profile = {
icon: 'assets/img/localPlugin.webp',
methods: [],
location: 'sidePanel',
type: 'iframe',
...this.profile,
hash: `local-${this.profile.name}`
}
if (!profile.location) throw new Error('Plugin should have a location')
if (!profile.name) throw new Error('Plugin should have a name')
if (!profile.url) throw new Error('Plugin should have an URL')
localStorage.setItem('plugins/local', JSON.stringify(profile))
return profile
}
updateName ({ target }) {
this.profile.name = target.value
}
updateUrl ({ target }) {
this.profile.url = target.value
}
updateDisplayName ({ target }) {
this.profile.displayName = target.value
}
updateProfile (key, e) {
this.profile[key] = e.target.value
}
updateMethods ({ target }) {
if (target.value) {
try {
this.profile.methods = target.value.split(',')
} catch (e) {}
}
}
/** The form to create a local plugin */
form () {
const name = this.profile.name || ''
const url = this.profile.url || ''
const displayName = this.profile.displayName || ''
const methods = (this.profile.methods && this.profile.methods.join(',')) || ''
const radioSelection = (key, label, message) => {
return this.profile[key] === label
? yo`<div class="radio">
<input class="form-check-input" type="radio" name="${key}" onclick="${e => this.updateProfile(key, e)}" value="${label}" id="${label}" data-id="localPluginRadioButton${label}" checked="checked" />
<label class="form-check-label" for="${label}">${message}</label>
</div>`
: yo`<div class="radio">
<input class="form-check-input" type="radio" name="${key}" onclick="${e => this.updateProfile(key, e)}" value="${label}" id="${label}" data-id="localPluginRadioButton${label}" />
<label class="form-check-label" for="${label}">${message}</label>
</div>`
}
return yo`
<form id="local-plugin-form">
<div class="form-group">
<label for="plugin-name">Plugin Name <small>(required)</small></label>
<input class="form-control" onchange="${e => this.updateName(e)}" value="${name}" id="plugin-name" data-id="localPluginName" placeholder="Should be camelCase">
</div>
<div class="form-group">
<label for="plugin-displayname">Display Name</label>
<input class="form-control" onchange="${e => this.updateDisplayName(e)}" value="${displayName}" id="plugin-displayname" data-id="localPluginDisplayName" placeholder="Name in the header">
</div>
<div class="form-group">
<label for="plugin-methods">Api (comma separated list of methods name)</label>
<input class="form-control" onchange="${e => this.updateMethods(e)}" value="${methods}" id="plugin-methods" data-id="localPluginMethods" placeholder="Name in the header">
</div>
<div class="form-group">
<label for="plugin-url">Url <small>(required)</small></label>
<input class="form-control" onchange="${e => this.updateUrl(e)}" value="${url}" id="plugin-url" data-id="localPluginUrl" placeholder="ex: https://localhost:8000">
</div>
<h6>Type of connection <small>(required)</small></h6>
<div class="form-check form-group">
${radioSelection('type', 'iframe', 'Iframe')}
${radioSelection('type', 'ws', 'Websocket')}
</div>
<h6>Location in remix <small>(required)</small></h6>
<div class="form-check form-group">
${radioSelection('location', 'sidePanel', 'Side Panel')}
${radioSelection('location', 'mainPanel', 'Main Panel')}
${radioSelection('location', 'none', 'None')}
</div>
</form>`
}
}

@ -1,73 +1,11 @@
import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web'
import { ViewPlugin } from '@remixproject/engine-web'
import { PluginManagerSettings } from './plugin-manager-settings'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import {RemixUiPluginManager} from '@remix-ui/plugin-manager' // eslint-disable-line
import * as packageJson from '../../../../../package.json'
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const EventEmitter = require('events')
const LocalPlugin = require('./local-plugin')
const addToolTip = require('../ui/tooltip')
const _paq = window._paq = window._paq || []
const css = csjs`
.pluginSearch {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--light);
padding: 10px;
position: sticky;
top: 0;
z-index: 2;
margin-bottom: 0px;
}
.pluginSearchInput {
height: 38px;
}
.pluginSearchButton {
font-size: 13px;
}
.displayName {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.pluginIcon {
height: 0.7rem;
width: 0.7rem;
filter: invert(0.5);
}
.description {
font-size: 13px;
line-height: 18px;
}
.descriptiontext {
display: block;
}
.descriptiontext:first-letter {
text-transform: uppercase;
}
.row {
display: flex;
flex-direction: row;
}
.isStuck {
background-color: var(--primary);
color:
}
.versionWarning {
padding: 4px;
margin: 0 8px;
font-weight: 700;
font-size: 9px;
line-height: 12px;
text-transform: uppercase;
cursor: default;
border: 1px solid;
border-radius: 2px;
}
`
const profile = {
name: 'pluginManager',
displayName: 'Plugin manager',
@ -84,112 +22,86 @@ const profile = {
class PluginManagerComponent extends ViewPlugin {
constructor (appManager, engine) {
super(profile)
this.event = new EventEmitter()
this.appManager = appManager
this.views = {
root: null,
items: {}
}
this.localPlugin = new LocalPlugin()
this.filter = ''
this.appManager.event.on('activate', () => { this.reRender() })
this.appManager.event.on('deactivate', () => { this.reRender() })
this.engine = engine
this.engine.event.on('onRegistration', () => { this.reRender() })
this.pluginManagerSettings = new PluginManagerSettings()
this.htmlElement = document.createElement('div')
this.htmlElement.setAttribute('id', 'pluginManager')
this.filter = ''
this.activePlugins = []
this.inactivePlugins = []
this.activeProfiles = this.appManager.actives
this._paq = _paq
this.listenOnEvent()
}
/**
* Checks and returns true or false if plugin name
* passed in exists in the actives string array in
* RemixAppManager
* @param {string} name name of Plugin
*/
isActive (name) {
return this.appManager.actives.includes(name)
}
/**
* Delegates to method activatePlugin in
* RemixAppManager to enable plugin activation
* @param {string} name name of Plugin
*/
activateP (name) {
this.appManager.activatePlugin(name)
_paq.push(['trackEvent', 'manager', 'activate', name])
}
/**
* Takes the name of a local plugin and does both
* activation and registration
* @param {Profile} pluginName
* @returns {void}
*/
async activateAndRegisterLocalPlugin (localPlugin) {
if (localPlugin) {
this.engine.register(localPlugin)
this.appManager.activatePlugin(localPlugin.profile.name)
this.getAndFilterPlugins()
localStorage.setItem('plugins/local', JSON.stringify(localPlugin.profile))
}
}
/**
* Calls and triggers the event deactivatePlugin
* with with manager permission passing in the name
* of the plugin
* @param {string} name name of Plugin
*/
deactivateP (name) {
this.call('manager', 'deactivatePlugin', name)
_paq.push(['trackEvent', 'manager', 'deactivate', name])
}
renderItem (profile) {
const displayName = (profile.displayName) ? profile.displayName : profile.name
const doclink = profile.documentation ? yo`<a href="${profile.documentation}" class="px-1" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>`
: yo``
// Check version of the plugin
let versionWarning
// Alpha
if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) {
versionWarning = yo`<small title="Version Alpha" class="${css.versionWarning} plugin-version">alpha</small>`
}
// Beta
if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) {
versionWarning = yo`<small title="Version Beta" class="${css.versionWarning} plugin-version">beta</small>`
}
const activationButton = this.isActive(profile.name)
? yo`
<button
onclick="${() => this.deactivateP(profile.name)}"
class="btn btn-secondary btn-sm" data-id="pluginManagerComponentDeactivateButton${profile.name}"
>
Deactivate
</button>
`
: yo`
<button
onclick="${() => this.activateP(profile.name)}"
class="btn btn-success btn-sm" data-id="pluginManagerComponentActivateButton${profile.name}"
>
Activate
</button>`
return yo`
<article id="remixPluginManagerListItem_${profile.name}" class="list-group-item py-1 mb-1 plugins-list-group-item" title="${displayName}" >
<div class="${css.row} justify-content-between align-items-center mb-2">
<h6 class="${css.displayName} plugin-name">
<div>
${displayName}
${doclink}
${versionWarning}
</div>
${activationButton}
</h6>
</div>
<div class="${css.description} d-flex text-body plugin-text mb-2">
<img src="${profile.icon}" class="mr-1 mt-1 ${css.pluginIcon}" />
<span class="${css.descriptiontext}">${profile.description}</span>
</div>
</article>
`
onActivation () {
this.renderComponent()
}
/***************
* SUB-COMPONENT
*/
/**
* Add a local plugin to the list of plugins
*/
async openLocalPlugin () {
try {
const profile = await this.localPlugin.open(this.appManager.getAll())
if (!profile) return
if (this.appManager.getIds().includes(profile.name)) {
throw new Error('This name has already been used')
}
const plugin = profile.type === 'iframe' ? new IframePlugin(profile) : new WebsocketPlugin(profile)
this.engine.register(plugin)
await this.appManager.activatePlugin(plugin.name)
} catch (err) {
// TODO : Use an alert to handle this error instead of a console.log
console.log(`Cannot create Plugin : ${err.message}`)
addToolTip(`Cannot create Plugin : ${err.message}`)
}
renderComponent () {
ReactDOM.render(
<RemixUiPluginManager
pluginComponent={this}
pluginManagerSettings={this.pluginManagerSettings}
/>,
this.htmlElement)
}
render () {
// Filtering helpers
return this.htmlElement
}
getAndFilterPlugins (filter) {
this.filter = typeof filter === 'string' ? filter.toLowerCase() : this.filter
const isFiltered = (profile) => (profile.displayName ? profile.displayName : profile.name).toLowerCase().includes(this.filter)
const isNotRequired = (profile) => !this.appManager.isRequired(profile.name)
const isNotDependent = (profile) => !this.appManager.isDependent(profile.name)
@ -199,71 +111,35 @@ class PluginManagerComponent extends ViewPlugin {
const nameB = ((profileB.displayName) ? profileB.displayName : profileB.name).toUpperCase()
return (nameA < nameB) ? -1 : (nameA > nameB) ? 1 : 0
}
// Filter all active and inactive modules that are not required
const { actives, inactives } = this.appManager.getAll()
const activatedPlugins = []
const deactivatedPlugins = []
const tempArray = this.appManager.getAll()
.filter(isFiltered)
.filter(isNotRequired)
.filter(isNotDependent)
.filter(isNotHome)
.sort(sortByName)
.reduce(({ actives, inactives }, profile) => {
return this.isActive(profile.name)
? { actives: [...actives, profile], inactives }
: { inactives: [...inactives, profile], actives }
}, { actives: [], inactives: [] })
const activeTile = actives.length !== 0
? yo`
<nav class="plugins-list-header justify-content-between navbar navbar-expand-lg bg-light navbar-light align-items-center">
<span class="navbar-brand plugins-list-title">Active Modules</span>
<span class="badge badge-primary" data-id="pluginManagerComponentActiveTilesCount">${actives.length}</span>
</nav>`
: ''
const inactiveTile = inactives.length !== 0
? yo`
<nav class="plugins-list-header justify-content-between navbar navbar-expand-lg bg-light navbar-light align-items-center">
<span class="navbar-brand plugins-list-title h6 mb-0 mr-2">Inactive Modules</span>
<span class="badge badge-primary" style = "cursor: default;" data-id="pluginManagerComponentInactiveTilesCount">${inactives.length}</span>
</nav>`
: ''
const settings = new PluginManagerSettings().render()
const rootView = yo`
<div id='pluginManager' data-id="pluginManagerComponentPluginManager">
<header class="form-group ${css.pluginSearch} plugins-header py-3 px-4 border-bottom" data-id="pluginManagerComponentPluginManagerHeader">
<input onkeyup="${e => this.filterPlugins(e)}" class="${css.pluginSearchInput} form-control" placeholder="Search" data-id="pluginManagerComponentSearchInput">
<button onclick="${_ => this.openLocalPlugin()}" class="${css.pluginSearchButton} btn bg-transparent text-dark border-0 mt-2 text-underline" data-id="pluginManagerComponentPluginSearchButton">
Connect to a Local Plugin
</button>
</header>
<section data-id="pluginManagerComponentPluginManagerSection">
${activeTile}
<div class="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentActiveTile">
${actives.map(profile => this.renderItem(profile))}
</div>
${inactiveTile}
<div class="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentInactiveTile">
${inactives.map(profile => this.renderItem(profile))}
</div>
</section>
${settings}
</div>
`
if (!this.views.root) this.views.root = rootView
return rootView
}
reRender () {
if (this.views.root) {
yo.update(this.views.root, this.render())
}
}
filterPlugins ({ target }) {
this.filter = target.value.toLowerCase()
this.reRender()
tempArray.forEach(profile => {
if (this.appManager.actives.includes(profile.name)) {
activatedPlugins.push(profile)
} else {
deactivatedPlugins.push(profile)
}
})
this.activePlugins = activatedPlugins
this.inactivePlugins = deactivatedPlugins
this.renderComponent()
}
listenOnEvent () {
this.engine.event.on('onRegistration', () => this.renderComponent())
this.appManager.event.on('activate', () => {
this.getAndFilterPlugins()
})
this.appManager.event.on('deactivate', () => {
this.getAndFilterPlugins()
})
}
}

@ -2,8 +2,8 @@ const yo = require('yo-yo')
const csjs = require('csjs-inject')
const modalDialog = require('../ui/modaldialog')
const css = csjs`
.permissions {
const css = csjs`
.remixui_permissions {
position: sticky;
bottom: 0;
display: flex;
@ -44,9 +44,12 @@ const css = csjs`
`
export class PluginManagerSettings {
openDialog () {
constructor () {
const fromLocal = window.localStorage.getItem('plugins/permissions')
this.permissions = JSON.parse(fromLocal || '{}')
}
openDialog () {
this.currentSetting = this.settings()
modalDialog('Plugin Manager Permissions', this.currentSetting,
{ fn: () => this.onValidation() }
@ -60,6 +63,8 @@ export class PluginManagerSettings {
/** Clear one permission from a plugin */
clearPersmission (from, to, method) {
// eslint-disable-next-line no-debugger
debugger
if (this.permissions[to] && this.permissions[to][method]) {
delete this.permissions[to][method][from]
if (Object.keys(this.permissions[to][method]).length === 0) {
@ -74,6 +79,8 @@ export class PluginManagerSettings {
/** Clear all persmissions from a plugin */
clearAllPersmission (to) {
// eslint-disable-next-line no-debugger
debugger
if (!this.permissions[to]) return
delete this.permissions[to]
yo.update(this.currentSetting, this.settings())

@ -3,6 +3,10 @@ import { ModalDialogProps } from './types' // eslint-disable-line
import './remix-ui-modal-dialog.css'
declare global {
interface Window { testmode: boolean; }
}
export const ModalDialog = (props: ModalDialogProps) => {
const [state, setState] = useState({
toggleBtn: true
@ -21,7 +25,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
e.stopPropagation()
if (document.activeElement !== this) {
handleHide()
!window.testmode && handleHide()
}
}
}

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

@ -0,0 +1,7 @@
# remix-ui-plugin-manager
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-plugin-manager` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1 @@
export * from './lib/remix-ui-plugin-manager'

@ -0,0 +1,54 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react'
import '../remix-ui-plugin-manager.css'
interface PluginCardProps {
profile: any
buttonText: string
deactivatePlugin: (pluginName: string) => void
}
function ActivePluginCard ({
profile,
buttonText,
deactivatePlugin
}: PluginCardProps) {
return (
<div className="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentActiveTile">
<article className="list-group-item py-1 mb-1 plugins-list-group-item" title={profile.displayName || profile.name}>
<div className="remixui_row justify-content-between align-items-center mb-2">
<h6 className="remixui_displayName plugin-name">
<div>
{ profile.displayName || profile.name }
{ profile.documentation &&
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer">
<i aria-hidden="true" className="fas fa-book"/>
</a>
}
{ profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)
? <small title="Version Alpha" className="remixui_versionWarning plugin-version">alpha</small>
: profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)
? <small title="Version Beta" className="remixui_versionWarning plugin-version">beta</small>
: null
}
</div>
{<button
onClick={() => {
deactivatePlugin(profile.name)
} }
className="btn btn-secondary btn-sm"
data-id={`pluginManagerComponentDeactivateButton${profile.name}`}
>
{buttonText}
</button>}
</h6>
</div>
<div className="remixui_description d-flex text-body plugin-text mb-2">
{profile.icon ? <img src={profile.icon} className="mr-1 mt-1 remixui_pluginIcon" alt="profile icon" /> : null}
<span className="remixui_descriptiontext">{profile.description}</span>
</div>
</article>
</div>
)
}
export default ActivePluginCard

@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Profile } from '@remixproject/plugin-utils'
import React from 'react'
import { PluginManagerComponent } from '../../types'
import ActivePluginCard from './ActivePluginCard'
import ModuleHeading from './moduleHeading'
interface ActivePluginCardContainerProps {
pluginComponent: PluginManagerComponent
setActiveProfiles: React.Dispatch<React.SetStateAction<Profile<any>[]>>
activeProfiles: Profile[]
}
function ActivePluginCardContainer ({ pluginComponent }: ActivePluginCardContainerProps) {
const deactivatePlugin = (pluginName: string) => {
pluginComponent.deactivateP(pluginName)
}
return (
<React.Fragment>
{(pluginComponent.activePlugins && pluginComponent.activePlugins.length) ? <ModuleHeading headingLabel="Active Modules" count={pluginComponent.activePlugins.length} /> : null}
{pluginComponent.activePlugins && pluginComponent.activePlugins.map((profile, idx) => {
return (
<ActivePluginCard
buttonText="Deactivate"
profile={profile}
deactivatePlugin={deactivatePlugin}
key={idx}
/>
)
})
}
</React.Fragment>
)
}
export default ActivePluginCardContainer

@ -0,0 +1,59 @@
import { Profile } from '@remixproject/plugin-utils'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react'
import '../remix-ui-plugin-manager.css'
interface PluginCardProps {
profile: Profile & {
icon?: string
}
buttonText: string
activatePlugin: (plugin: string) => void
}
function InactivePluginCard ({
profile,
buttonText,
activatePlugin
}: PluginCardProps) {
return (
<div className="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentActiveTile">
<article className="list-group-item py-1 mb-1 plugins-list-group-item" title={profile.displayName || profile.name}>
<div className="remixui_row justify-content-between align-items-center mb-2">
<h6 className="remixui_displayName plugin-name">
<div>
{ profile.displayName || profile.name }
{ profile.documentation &&
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer">
<i aria-hidden="true" className="fas fa-book"/>
</a>
}
{ profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)
? <small title="Version Alpha" className="remixui_versionWarning plugin-version">alpha</small>
: profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)
? <small title="Version Beta" className="remixui_versionWarning plugin-version">beta</small>
: null
}
</div>
{
<button
onClick={() => {
activatePlugin(profile.name)
}}
className="btn btn-success btn-sm"
data-id={`pluginManagerComponentActivateButton${profile.name}`}
>
{buttonText}
</button>
}
</h6>
</div>
<div className="remixui_description d-flex text-body plugin-text mb-2">
{ profile.icon ? <img src={profile.icon} className="mr-1 mt-1 remixui_pluginIcon" alt="profile icon"/> : null }
<span className="remixui_descriptiontext">{profile.description}</span>
</div>
</article>
</div>
)
}
export default InactivePluginCard

@ -0,0 +1,47 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Profile } from '@remixproject/plugin-utils'
import React from 'react'
import { PluginManagerComponent, PluginManagerProfile } from '../../types'
import InactivePluginCard from './InactivePluginCard'
import ModuleHeading from './moduleHeading'
interface InactivePluginCardContainerProps {
pluginComponent: PluginManagerComponent
setInactiveProfiles: React.Dispatch<React.SetStateAction<Profile<any>[]>>
inactiveProfiles: Profile<any>[]
}
interface LocalPluginInterface {
profile: Partial<PluginManagerProfile>
activateService: {}
requestQueue: []
options: { queueTimeout: number }
id: number
pendingRequest: {}
listener: []
iframe: {}
}
function InactivePluginCardContainer ({ pluginComponent }: InactivePluginCardContainerProps) {
const activatePlugin = (pluginName: string) => {
pluginComponent.activateP(pluginName)
}
return (
<React.Fragment>
{(pluginComponent.inactivePlugins && pluginComponent.inactivePlugins.length) ? <ModuleHeading headingLabel="Inactive Modules" count={pluginComponent.inactivePlugins.length} /> : null}
{pluginComponent.inactivePlugins && pluginComponent.inactivePlugins.map((profile, idx) => {
return (
<InactivePluginCard
buttonText="Activate"
profile={profile}
key={idx}
activatePlugin={activatePlugin}
/>
)
})
}
</React.Fragment>
)
}
export default InactivePluginCardContainer

@ -0,0 +1,218 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useEffect, useReducer, useState } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog'
import { Toaster } from '@remix-ui/toaster'
import { IframePlugin, WebsocketPlugin } from '@remixproject/engine-web'
import { localPluginReducerActionType, localPluginToastReducer } from '../reducers/pluginManagerReducer'
import { FormStateProps, PluginManagerComponent } from '../../types'
interface LocalPluginFormProps {
closeModal: () => void
visible: boolean
pluginManager: PluginManagerComponent
}
const initialState: FormStateProps = {
name: '',
displayName: '',
url: '',
type: 'iframe',
hash: '',
methods: [],
location: 'sidePanel'
}
const defaultProfile = {
methods: [],
location: 'sidePanel',
type: 'iframe',
name: '',
displayName: '',
url: '',
hash: ''
}
function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFormProps) {
const [errorMsg, dispatchToastMsg] = useReducer(localPluginToastReducer, '')
const [name, setName] = useState<string>('')
const [displayName, setDisplayName] = useState<string>('')
const [url, setUrl] = useState<string>('')
const [type, setType] = useState<'iframe' | 'ws'>('iframe')
const [location, setLocation] = useState<'sidePanel' | 'mainPanel' | 'none'>('sidePanel')
const [methods, setMethods] = useState<string>('')
useEffect(() => {
const storagePlugin:FormStateProps = localStorage.getItem('plugins/local') ? JSON.parse(localStorage.getItem('plugins/local')) : defaultProfile
setName(storagePlugin.name)
setUrl(storagePlugin.url)
setLocation(storagePlugin.location as 'sidePanel' | 'mainPanel' | 'none')
setMethods(storagePlugin.methods)
setType(storagePlugin.type)
setDisplayName(storagePlugin.displayName)
}, [])
const handleModalOkClick = async () => {
try {
if (!name) throw new Error('Plugin should have a name')
if (pluginManager.appManager.getIds().includes(name)) {
throw new Error('This name has already been used')
}
if (!location) throw new Error('Plugin should have a location')
if (!url) throw new Error('Plugin should have an URL')
const newMethods = typeof methods === 'string' ? methods.split(',').filter(val => val) : []
const targetPlugin = {
name: name,
displayName: displayName,
description: '',
documentation: '',
events: [],
hash: '',
kind: '',
methods: newMethods,
url: url,
type: type,
location: location,
icon: 'assets/img/localPlugin.webp'
}
const localPlugin = type === 'iframe' ? new IframePlugin(initialState) : new WebsocketPlugin(initialState)
localPlugin.profile.hash = `local-${name}`
targetPlugin.description = localPlugin.profile.description !== undefined ? localPlugin.profile.description : ''
targetPlugin.events = localPlugin.profile.events !== undefined ? localPlugin.profile.events : []
targetPlugin.kind = localPlugin.profile.kind !== undefined ? localPlugin.profile.kind : ''
localPlugin.profile = { ...localPlugin.profile, ...targetPlugin }
pluginManager.activateAndRegisterLocalPlugin(localPlugin)
} catch (error) {
const action: localPluginReducerActionType = { type: 'show', payload: `${error.message}` }
dispatchToastMsg(action)
console.log(error)
}
}
return (
<><ModalDialog
handleHide={closeModal}
id="pluginManagerLocalPluginModalDialog"
hide={visible}
title="Local Plugin"
okLabel="OK"
okFn={ handleModalOkClick }
cancelLabel="Cancel"
cancelFn={closeModal}
>
<form id="local-plugin-form">
<div className="form-group">
<label htmlFor="plugin-name">Plugin Name <small>(required)</small></label>
<input
className="form-control"
onChange={e => setName(e.target.value)}
value={ name}
id="plugin-name"
data-id="localPluginName"
placeholder="Should be camelCase" />
</div>
<div className="form-group">
<label htmlFor="plugin-displayname">Display Name</label>
<input
className="form-control"
onChange={e => setDisplayName(e.target.value)}
value={ displayName }
id="plugin-displayname"
data-id="localPluginDisplayName"
placeholder="Name in the header" />
</div>
<div className="form-group">
<label htmlFor="plugin-methods">Api (comma separated list of methods name)</label>
<input
className="form-control"
onChange={e => setMethods(e.target.value)}
value={ methods }
id="plugin-methods"
data-id="localPluginMethods"
placeholder="Name in the header" />
</div>
<div className="form-group">
<label htmlFor="plugin-url">Url <small>(required)</small></label>
<input
className="form-control"
onChange={e => setUrl(e.target.value)}
value={ url }
id="plugin-url"
data-id="localPluginUrl"
placeholder="ex: https://localhost:8000" />
</div>
<h6>Type of connection <small>(required)</small></h6>
<div className="form-check form-group">
<div className="radio">
<input
className="form-check-input"
type="radio"
name="type"
value="iframe"
id="iframe"
data-id='localPluginRadioButtoniframe'
checked={type === 'iframe'}
onChange={(e) => setType(e.target.value as 'iframe' | 'ws')} />
<label className="form-check-label" htmlFor="iframe">Iframe</label>
</div>
<div className="radio">
<input
className="form-check-input"
type="radio"
name="type"
value="ws"
id="ws"
data-id='localPluginRadioButtonws'
checked={type === 'ws'}
onChange={(e) => setType(e.target.value as 'iframe' | 'ws')} />
<label className="form-check-label" htmlFor="ws">Websocket</label>
</div>
</div>
<h6>Location in remix <small>(required)</small></h6>
<div className="form-check form-group">
<div className="radio">
<input
className="form-check-input"
type="radio"
name="location"
value="sidePanel"
id="sidePanel"
data-id='localPluginRadioButtonsidePanel'
checked={location === 'sidePanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />
<label className="form-check-label" htmlFor="sidePanel">Side Panel</label>
</div>
<div className="radio">
<input
className="form-check-input"
type="radio"
name="location"
value="mainPanel"
id="mainPanel"
data-id='localPluginRadioButtonmainPanel'
checked={location === 'mainPanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />
<label className="form-check-label" htmlFor="mainPanel">Main Panel</label>
</div>
<div className="radio">
<input
className="form-check-input"
type="radio"
name="location"
value="none"
id="none"
data-id='localPluginRadioButtonnone'
checked={location === 'none'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />
<label className="form-check-label" htmlFor="none">None</label>
</div>
</div>
</form>
</ModalDialog>
{errorMsg ? <Toaster message={errorMsg} /> : null}
</>
)
}
export default LocalPluginForm

@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from 'react'
interface ModuleHeadingProps {
headingLabel: string
count: number
}
function ModuleHeading ({ headingLabel, count }: ModuleHeadingProps) {
return (
<nav className="plugins-list-header justify-content-between navbar navbar-expand-lg bg-light navbar-light align-items-center">
<span className="navbar-brand plugins-list-title h6 mb-0 mr-2">{headingLabel}</span>
<span className="badge badge-primary" style={{ cursor: 'default' }} data-id="pluginManagerComponentInactiveTilesCount">
{count}
</span>
</nav>
)
}
export default ModuleHeading

@ -0,0 +1,144 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { Fragment, useState } from 'react'
/* eslint-disable-line */
import { ModalDialog } from '@remix-ui/modal-dialog'
import useLocalStorage from '../custom-hooks/useLocalStorage'
import { PluginPermissions } from '../../types'
interface PermissionSettingsProps {
pluginSettings: any
}
function PermisssionsSettings ({ pluginSettings }: PermissionSettingsProps) {
const [modalVisibility, setModalVisibility] = useState<boolean>(true)
const [permissions, setPermissions] = useLocalStorage<PluginPermissions>('plugins/permissions', {} as PluginPermissions)
const [permissionCache, setpermissionCache] = useState<PluginPermissions>()
const closeModal = () => setModalVisibility(true)
const openModal = () => {
const currentValue = JSON.parse(window.localStorage.getItem('plugins/permissions') || '{}')
setpermissionCache(currentValue)
setPermissions(currentValue)
setModalVisibility(!modalVisibility)
}
const cancel = () => {
setPermissions(permissionCache)
}
const getState = (targetPlugin:string, funcName:string, pluginName :string) => {
return permissions[targetPlugin][funcName][pluginName].allow
}
const handleCheckboxClick = (targetPlugin:string, funcName:string, pluginName :string) => {
setPermissions((permissions) => {
permissions[targetPlugin][funcName][pluginName].allow = !permissions[targetPlugin][funcName][pluginName].allow
return permissions
})
}
function clearFunctionPermission (targetPlugin:string, funcName:string, pluginName :string) {
setPermissions((permissions) => {
delete permissions[targetPlugin][funcName][pluginName]
if (Object.keys(permissions[targetPlugin][funcName]).length === 0) delete permissions[targetPlugin][funcName]
if (Object.keys(permissions[targetPlugin]).length === 0) delete permissions[targetPlugin]
return permissions
})
}
function clearTargetPermission (targetPlugin: string) {
setPermissions((permissions) => {
delete permissions[targetPlugin]
return permissions
})
}
function RenderPluginHeader ({ headingName }) {
return (
<div className="pb-2 remixui_permissionKey">
<h3>{headingName} permissions:</h3>
<i
onClick={() => {
clearTargetPermission(headingName)
}}
className="far fa-trash-alt"
data-id={`pluginManagerSettingsClearAllPermission-${headingName}`}>
</i>
</div>
)
}
function RenderPermissions ({ targetPlugin }) {
return <>{Object.keys(permissions[targetPlugin]).map(funcName => {
return Object.keys(permissions[targetPlugin][funcName]).map((pluginName, index) => (
<div className="form-group remixui_permissionKey" key={pluginName}>
{ permissions && Object.keys(permissions).length > 0
? (
<><div className="remixui_checkbox">
<span className="mr-2">
<input
type="checkbox"
onChange={() => handleCheckboxClick(targetPlugin, funcName, pluginName)}
checked={getState(targetPlugin, funcName, pluginName)}
id={`permission-checkbox-${targetPlugin}-${funcName}-${pluginName}`}
aria-describedby={`module ${pluginName} asks permission for ${funcName}`} />
<label
className="ml-4"
htmlFor={`permission-checkbox-${targetPlugin}-${funcName}-${targetPlugin}`}
data-id={`permission-label-${targetPlugin}-${funcName}-${targetPlugin}`}
>
Allow <u>{pluginName}</u> to call <u>{funcName}</u>
</label>
</span>
</div><i
onClick={() => {
clearFunctionPermission(targetPlugin, funcName, pluginName)
} }
className="fa fa-trash-alt"
data-id={`pluginManagerSettingsRemovePermission-${targetPlugin}-${funcName}-${targetPlugin}`} /></>
) : null
}
</div>
))
})}</>
}
return (
<Fragment>
<ModalDialog
handleHide={closeModal}
cancelFn={cancel}
hide={modalVisibility}
title="Plugin Manager Permissions"
okLabel="OK"
cancelLabel="Cancel"
>
{permissions && Object.keys(permissions).length > 0
? (<h4 className="text-center">Current Permission Settings</h4>)
: (<h4 className="text-center">No Permission requested yet.</h4>)
}
<form className="remixui_permissionForm" data-id="pluginManagerSettingsPermissionForm">
<div className="p-2">
{
Object.keys(permissions).map(targetPlugin => (
<div key={`container-${targetPlugin}`}>
<RenderPluginHeader key={`header-${targetPlugin}`} headingName={targetPlugin} />
<RenderPermissions key={`permissions-${targetPlugin}`} targetPlugin={targetPlugin}/>
</div>
))
}
</div>
</form>
</ModalDialog>
<footer className="bg-light remixui_permissions remix-bg-opacity">
<button
onClick={openModal}
className="btn btn-primary settings-button"
data-id="pluginManagerPermissionsButton">
Permissions
</button>
</footer>
</Fragment>
)
}
export default PermisssionsSettings

@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { Fragment, ReactNode, useEffect, useState } from 'react'
import { PluginManagerComponent, PluginManagerSettings } from '../../types'
import PermisssionsSettings from './permissionsSettings'
import { Profile } from '@remixproject/plugin-utils'
import LocalPluginForm from './LocalPluginForm'
interface RootViewProps {
pluginComponent: PluginManagerComponent
pluginManagerSettings: PluginManagerSettings
children: ReactNode
}
export interface pluginDeactivated {
flag: boolean
profile: Profile
}
export interface pluginActivated {
flag: boolean
profile: Profile
}
function RootView ({ pluginComponent, pluginManagerSettings, children }: RootViewProps) {
const [visible, setVisible] = useState<boolean>(true)
const [filterPlugins, setFilterPlugin] = useState<string>('')
const openModal = () => {
setVisible(false)
}
const closeModal = () => setVisible(true)
useEffect(() => {
pluginComponent.getAndFilterPlugins(filterPlugins)
}, [filterPlugins])
return (
<Fragment>
<div id="pluginManager" data-id="pluginManagerComponentPluginManager">
<header className="form-group remixui_pluginSearch plugins-header py-3 px-4 border-bottom" data-id="pluginManagerComponentPluginManagerHeader">
<input
type="text"
onChange={(event) => {
setFilterPlugin(event.target.value.toLowerCase())
}}
value={filterPlugins}
className="form-control"
placeholder="Search"
data-id="pluginManagerComponentSearchInput"
/>
<button onClick={openModal} className="remixui_pluginSearchButton btn bg-transparent text-dark border-0 mt-2 text-underline" data-id="pluginManagerComponentPluginSearchButton">
Connect to a Local Plugin
</button>
</header>
{children}
<PermisssionsSettings pluginSettings={pluginManagerSettings}/>
</div>
<LocalPluginForm
closeModal={closeModal}
visible={visible}
pluginManager={pluginComponent}
/>
</Fragment>
)
}
export default RootView

@ -0,0 +1,78 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
type SetValue<T> = Dispatch<SetStateAction<T>>
function useLocalStorage<T> (key: string, initialValue: T): [T, SetValue<T>] {
// Get from local storage then
// parse stored json or return initialValue
const readValue = (): T => {
// Prevent build error "window is undefined" but keep keep working
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (JSON.parse(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(readValue)
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: SetValue<T> = value => {
// Prevent build error "window is undefined" but keeps working
if (typeof window === 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(storedValue) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// Save state
setStoredValue(newValue)
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
}
useEffect(() => {
setStoredValue(readValue())
}, [])
useEffect(() => {
const handleStorageChange = () => {
setStoredValue(readValue())
}
// this only works for other documents, not the current one
window.addEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
window.addEventListener('local-storage', handleStorageChange)
return () => {
window.removeEventListener('storage', handleStorageChange)
window.removeEventListener('local-storage', handleStorageChange)
}
}, [])
return [storedValue, setValue]
}
export default useLocalStorage

@ -0,0 +1,14 @@
export type localPluginReducerActionType = {
type: 'show' | 'close',
payload?: any
}
export function localPluginToastReducer (currentState: string, toastAction: localPluginReducerActionType) {
switch (toastAction.type) {
case 'show':
return `Cannot create Plugin : ${toastAction.payload!}`
default:
return currentState
}
}

@ -0,0 +1,96 @@
.remixui_pluginSearch {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--light);
padding: 10px;
position: sticky;
top: 0;
z-index: 2;
margin-bottom: 0px;
}
.remixui_pluginSearchInput {
height: 38px;
}
.remixui_pluginSearchButton {
font-size: 13px;
}
.remixui_displayName {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.remixui_pluginIcon {
height: 0.7rem;
width: 0.7rem;
filter: invert(0.5);
}
.remixui_description {
font-size: 13px;
line-height: 18px;
}
.remixui_descriptiontext {
display: block;
}
.remixui_descriptiontext:first-letter {
text-transform: uppercase;
}
.remixui_row {
display: flex;
flex-direction: row;
}
.remixui_isStuck {
background-color: var(--primary);
/* color: */
}
.remixui_versionWarning {
padding: 4px;
margin: 0 8px;
font-weight: 700;
font-size: 9px;
line-height: 12px;
text-transform: uppercase;
cursor: default;
border: 1px solid;
border-radius: 2px;
}
.remixui_permissions {
position: sticky;
bottom: 0;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 5px 20px;
}
.remixui_permissions button {
padding: 2px 5px;
cursor: pointer;
}
.remixui_permissionForm h4 {
font-size: 1.3rem;
text-align: center;
}
.remixui_permissionForm h6 {
font-size: 1.1rem;
}
.remixui_permissionForm hr {
width: 80%;
}
.remixui_permissionKey {
display: flex;
justify-content: space-between;
align-items: center;
}
.remixui_permissionKey i {
cursor: pointer;
}
.remixui_checkbox {
display: flex;
align-items: center;
}
.remixui_checkbox label {
margin: 0;
font-size: 1rem;
}

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Profile } from '@remixproject/plugin-utils'
import React, { useState } from 'react'
import { RemixUiPluginManagerProps } from '../types'
import ActivePluginCardContainer from './components/ActivePluginCardContainer'
import InactivePluginCardContainer from './components/InactivePluginCardContainer'
import RootView from './components/rootView'
import './remix-ui-plugin-manager.css'
export const RemixUiPluginManager = ({ pluginComponent, pluginManagerSettings }: RemixUiPluginManagerProps) => {
const [activeProfiles, setActiveProfiles] = useState<Profile[]>(pluginComponent.activePlugins)
const [inactiveProfiles, setinactiveProfiles] = useState<Profile[]>(pluginComponent.inactivePlugins)
return (
<RootView pluginComponent={pluginComponent} pluginManagerSettings={pluginManagerSettings}>
<section data-id="pluginManagerComponentPluginManagerSection">
<ActivePluginCardContainer
pluginComponent={pluginComponent}
setActiveProfiles={setActiveProfiles}
activeProfiles={activeProfiles}
/>
<InactivePluginCardContainer
pluginComponent={pluginComponent}
setInactiveProfiles={setinactiveProfiles}
inactiveProfiles={inactiveProfiles}
/>
</section>
</RootView>
)
}

@ -0,0 +1,208 @@
import { PermissionHandler } from './app/ui/persmission-handler'
import { PluginManager } from '@remixproject/engine/lib/manager'
import { EventEmitter } from 'events'
import { Engine } from '@remixproject/engine/lib/engine'
import { PluginBase, Profile } from '@remixproject/plugin-utils'
import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web'
/* eslint-disable camelcase */
interface SetPluginOptionType {
queueTimeout: number
}
export class RemixEngine extends Engine {
event: EventEmitter;
setPluginOption ({ name, kind }) : SetPluginOptionType
onRegistration (plugin) : void
}
export function isNative(name: any): any;
/**
* Checks if plugin caller 'from' is allowed to activate plugin 'to'
* The caller can have 'canActivate' as a optional property in the plugin profile.
* This is an array containing the 'name' property of the plugin it wants to call.
* canActivate = ['plugin1-to-call','plugin2-to-call',....]
* or the plugin is allowed by default because it is native
*
* @param {any, any}
* @returns {boolean}
*/
export function canActivate(from: any, to: any): boolean;
export class RemixAppManager extends PluginManager {
constructor();
event: EventEmitter;
pluginsDirectory: string;
pluginLoader: PluginLoader;
permissionHandler: PermissionHandler;
getAll(): import('@remixproject/plugin-utils').Profile<any>[];
getIds(): string[];
isDependent(name: any): any;
isRequired(name: any): any;
registeredPlugins(): Promise<any>;
turnPluginOn(name: string | string[]);
turnPluginOff(name: string);
}
export class PluginManagerSettings {
openDialog(): void;
permissions: any;
currentSetting: any;
onValidation(): void;
/** Clear one permission from a plugin */
clearPersmission(from: string, to: string, method: string): void;
/** Clear all persmissions from a plugin */
clearAllPersmission(to: string): void;
settings(): any;
render(): any;
}
export type PluginPermissions = {
fileManager : {
writeFile: {
pluginName: {
allow: boolean
}
}
}
}
export class PluginManagerComponent extends ViewPlugin extends Plugin implements PluginBase {
constructor(appManager: RemixAppManager, engine: Engine)
appManager: RemixAppManager
pluginSettings: PluginManagerSettings
app: PluginApi<any>
engine: Engine
htmlElement: HTMLDivElement
views: { root: null, items: {} }
localPlugin: LocalPlugin
pluginNames: string[]
inactivePlugins: Profile[]
activePlugins: Profile[]
filter: string
isActive(name: string): boolean
activateP(name: string): void
deactivateP(name: string): void
onActivation(): void
renderComponent(): void
openLocalPlugin(): Promise<void>
render(): HTMLDivElement
getAndFilterPlugins: (filter?: string, profiles?: Profile[]) => void
triggerEngineEventListener: () => void
activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | WebsocketPlugin) => Promise<void>
activeProfiles: string[]
_paq: any
}
// eslint-disable-next-line no-use-before-define
export = LocalPlugin;
declare class LocalPlugin {
/**
* Open a modal to create a local plugin
* @param {Profile[]} plugins The list of the plugins in the store
* @returns {Promise<{api: any, profile: any}>} A promise with the new plugin profile
*/
open(plugins: any[]): Promise<{
api: any;
profile: any;
}>;
profile: any;
/**
* Create the object to add to the plugin-list
*/
create(): any;
updateName({ target }: {
target: any;
}): void;
updateUrl({ target }: {
target: any;
}): void;
updateDisplayName({ target }: {
target: any;
}): void;
updateProfile(key: any, e: any): void;
updateMethods({ target }: {
target: any;
}): void;
/** The form to create a local plugin */
form(): any;
}
export interface PluginManagerContextProviderProps {
children: React.ReactNode
pluginComponent: PluginManagerComponent
}
export interface RemixUiPluginManagerProps {
pluginComponent: PluginManagerComponent
pluginManagerSettings: PluginManagerSettings
}
/** @class Reference loaders.
* A loader is a get,set based object which load a workspace from a defined sources.
* (localStorage, queryParams)
**/
declare class PluginLoader {
get currentLoader(): any;
donotAutoReload: string[];
loaders: {};
current: string;
set(plugin: any, actives: any): void;
get(): any;
}
export type PluginManagerSettings = {
openDialog: () => void
onValidation: () => void
clearPermission: (from: any, to: any, method: any) => void
settings: () => HTMLElement
render: () => HTMLElement
}
export interface DefaultLocalPlugin extends Profile {
name: string
displayName: string
url: string
type: string
hash: string
methods: any
location: string
}
export interface FormStateProps {
name: string
displayName: string
url: string
type: 'iframe' | 'ws'
hash: string
methods: any
location: string
}
export type PluginManagerProfile = Profile & {
name: string,
displayName: string,
methods: Array<any>,
events?: Array<any>,
icon: 'assets/img/pluginManager.webp',
description: string,
kind?: string,
location: 'sidePanel' | 'mainPanel' | 'none',
documentation: 'https://remix-ide.readthedocs.io/en/latest/plugin_manager.html',
version: any
type: 'iframe' | 'ws'
hash: string
}
export type LocalPlugin = {
create: () => Profile
updateName: (target: string) => void
updateDisplayName: (displayName: string) => void
updateProfile: (key: string, e: Event) => void
updateMethods: (target: any) => void
form: () => HTMLElement
}
export { }

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -106,6 +106,9 @@
"remix-ui-checkbox": {
"tags": []
},
"remix-ui-plugin-manager": {
"tags": []
},
"remix-core-plugin": {
"tags": []
},
@ -118,5 +121,5 @@
"remix-ui-renderer": {
"tags": []
}
}
}
}

221
package-lock.json generated

@ -7533,6 +7533,12 @@
"defer-to-connect": "^1.0.1"
}
},
"@testim/chrome-version": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.0.7.tgz",
"integrity": "sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw==",
"dev": true
},
"@testing-library/dom": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.1.tgz",
@ -7861,6 +7867,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.172",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz",
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -8079,6 +8091,16 @@
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz",
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
},
"@types/yauzl": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
"integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==",
"dev": true,
"optional": true,
"requires": {
"@types/node": "*"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.1.tgz",
@ -12255,6 +12277,159 @@
}
}
},
"chromedriver": {
"version": "92.0.1",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-92.0.1.tgz",
"integrity": "sha512-LptlDVCs1GgyFNVbRoHzzy948JDVzTgGiVPXjNj385qXKQP3hjAVBIgyvb/Hco0xSEW8fjwJfsm1eQRmu6t4pQ==",
"dev": true,
"requires": {
"@testim/chrome-version": "^1.0.7",
"axios": "^0.21.1",
"del": "^6.0.0",
"extract-zip": "^2.0.1",
"https-proxy-agent": "^5.0.0",
"proxy-from-env": "^1.1.0",
"tcp-port-used": "^1.0.1"
},
"dependencies": {
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"requires": {
"debug": "4"
}
},
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"del": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
"dev": true,
"requires": {
"globby": "^11.0.1",
"graceful-fs": "^4.2.4",
"is-glob": "^4.0.1",
"is-path-cwd": "^2.2.0",
"is-path-inside": "^3.0.2",
"p-map": "^4.0.0",
"rimraf": "^3.0.2",
"slash": "^3.0.0"
}
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"requires": {
"path-type": "^4.0.0"
}
},
"extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"requires": {
"@types/yauzl": "^2.9.1",
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
}
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"globby": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
}
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "6",
"debug": "4"
}
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
"is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true
},
"p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
"requires": {
"aggregate-error": "^3.0.0"
}
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
}
}
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@ -20946,6 +21121,12 @@
"unc-path-regex": "^0.1.2"
}
},
"is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"dev": true
},
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@ -20975,6 +21156,25 @@
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true
},
"is2": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz",
"integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==",
"dev": true,
"requires": {
"deep-is": "^0.1.3",
"ip-regex": "^4.1.0",
"is-url": "^1.2.4"
},
"dependencies": {
"ip-regex": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
"dev": true
}
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@ -36896,6 +37096,27 @@
"readable-stream": "^3.1.1"
}
},
"tcp-port-used": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
"integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==",
"dev": true,
"requires": {
"debug": "4.3.1",
"is2": "^2.0.6"
},
"dependencies": {
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
}
}
},
"temp-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",

@ -41,7 +41,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@ -208,6 +208,7 @@
"@types/chai": "^4.2.11",
"@types/fs-extra": "^9.0.1",
"@types/jest": "25.1.4",
"@types/lodash": "^4.14.172",
"@types/mocha": "^7.0.2",
"@types/nightwatch": "^1.1.6",
"@types/node": "~8.9.4",

@ -41,6 +41,7 @@
"@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"],
"@remix-ui/solidity-compiler": ["libs/remix-ui/solidity-compiler/src/index.ts"],
"@remix-ui/publish-to-storage": ["libs/remix-ui/publish-to-storage/src/index.ts"],
"@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"],
"@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"]
}
},

@ -777,6 +777,15 @@
}
}
},
"remix-ui-plugin-manager": {
"root": "libs/remix-ui/plugin-manager",
"sourceRoot": "libs/remix-ui/plugin-manager/src",
"tsConfig": ["libs/remix-ui/plugin-manager/tsconfig.lib.json"],
"exclude": [
"**/node_modules/**",
"!libs/remix-ui/plugin-manager/**/*"
]
},
"remix-core-plugin": {
"root": "libs/remix-core-plugin",
"sourceRoot": "libs/remix-core-plugin/src",
@ -858,6 +867,7 @@
"tsConfig": ["libs/remix-ui/renderer/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/renderer/**/*"]
}
}
}
}
@ -928,4 +938,4 @@
}
},
"defaultProject": "remix-ide"
}
}

Loading…
Cancel
Save