commit
5a12909346
@ -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>` |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,117 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<!-- |
||||||
|
The MIT License (MIT) |
||||||
|
Copyright (c) 2014, 2015, the individual contributors |
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
The above copyright notice and this permission notice shall be included in |
||||||
|
all copies or substantial portions of the Software. |
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
||||||
|
--> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="chrome=1"> |
||||||
|
<title>Remix - Ethereum IDE</title> |
||||||
|
<link rel="stylesheet" href="assets/css/pygment_trac.css"> |
||||||
|
<link rel="icon" type="x-icon" href="assets/img/icon.png"> |
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/4.1.0/introjs.min.css"> |
||||||
|
<script src="assets/js/browserfs.min.js"></script> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> |
||||||
|
<!-- Matomo --> |
||||||
|
<script type="text/javascript"> |
||||||
|
const domains = { |
||||||
|
'remix-alpha.ethereum.org': 27, |
||||||
|
'remix-beta.ethereum.org': 25, |
||||||
|
'remix.ethereum.org': 23 |
||||||
|
} |
||||||
|
if (domains[window.location.hostname]) { |
||||||
|
var _paq = window._paq = window._paq || [] |
||||||
|
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ |
||||||
|
_paq.push(['disableCookies']); |
||||||
|
_paq.push(['enableJSErrorTracking']); |
||||||
|
_paq.push(['trackPageView']); |
||||||
|
_paq.push(['enableLinkTracking']); |
||||||
|
(function() { |
||||||
|
var u="https://matomo.ethereum.org/"; |
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']) |
||||||
|
_paq.push(['setSiteId', domains[window.location.hostname]]) |
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0] |
||||||
|
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s) |
||||||
|
})() |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- End Matomo Code --> |
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/introjs.min.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<script> |
||||||
|
function urlParams () { |
||||||
|
var qs = window.location.hash.substr(1) |
||||||
|
|
||||||
|
if (window.location.search.length > 0) { |
||||||
|
// use legacy query params instead of hash |
||||||
|
window.location.hash = window.location.search.substr(1) |
||||||
|
window.location.search = '' |
||||||
|
} |
||||||
|
|
||||||
|
var params = {} |
||||||
|
var parts = qs.split('&') |
||||||
|
for (var x in parts) { |
||||||
|
var keyValue = parts[x].split('=') |
||||||
|
if (keyValue[0] !== '') { |
||||||
|
params[keyValue[0]] = keyValue[1] |
||||||
|
} |
||||||
|
} |
||||||
|
return params |
||||||
|
} |
||||||
|
const defaultVersion = '0.8.0' |
||||||
|
let versionToLoad = urlParams().appVersion ? urlParams().appVersion : defaultVersion |
||||||
|
|
||||||
|
let assets = { |
||||||
|
'0.8.0': ['https://use.fontawesome.com/releases/v5.8.1/css/all.css', 'assets/css/pygment_trac.css'], |
||||||
|
'0.7.7': ['assets/css/font-awesome.min.css', 'assets/css/pygment_trac.css'] |
||||||
|
} |
||||||
|
let versions = { |
||||||
|
'0.7.7': 'assets/js/0.7.7/app.js', // commit 7b5c7ae3de935e0ccc32eadfd83bf7349478491e |
||||||
|
'0.8.0': 'main.js' |
||||||
|
} |
||||||
|
for (let k in assets[versionToLoad]) { |
||||||
|
let app = document.createElement('link') |
||||||
|
app.setAttribute('rel', 'stylesheet') |
||||||
|
app.setAttribute('href', assets[versionToLoad][k]) |
||||||
|
if (assets[versionToLoad][k] === 'https://use.fontawesome.com/releases/v5.8.1/css/all.css') { |
||||||
|
app.setAttribute('integrity', 'sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf') |
||||||
|
app.setAttribute('crossorigin', 'anonymous') |
||||||
|
} |
||||||
|
document.head.appendChild(app) |
||||||
|
} |
||||||
|
window.onload = () => { |
||||||
|
BrowserFS.install(window) |
||||||
|
BrowserFS.configure({ |
||||||
|
fs: "LocalStorage" |
||||||
|
}, function(e) { |
||||||
|
if (e) console.log(e) |
||||||
|
let app = document.createElement('script') |
||||||
|
app.setAttribute('src', versions[versionToLoad]) |
||||||
|
document.body.appendChild(app) |
||||||
|
window.remixFileSystem = require('fs') |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
<script src="polyfills.js" type="module"></script> |
||||||
|
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script> |
||||||
|
<script type="text/javascript" src="assets/js/intro.min.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
@ -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"] |
||||||
|
} |
Loading…
Reference in new issue