Merge branch 'master' of https://github.com/ethereum/remix-project into fix-verticalIcons-badge
commit
62222eb5a5
@ -1,147 +0,0 @@ |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
const modalDialog = require('../ui/modaldialog') |
||||
|
||||
const css = csjs` |
||||
.remixui_permissions { |
||||
position: sticky; |
||||
bottom: 0; |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
align-items: center; |
||||
padding: 5px 20px; |
||||
} |
||||
.permissions button { |
||||
padding: 2px 5px; |
||||
cursor: pointer; |
||||
} |
||||
.permissionForm h4 { |
||||
font-size: 1.3rem; |
||||
text-align: center; |
||||
} |
||||
.permissionForm h6 { |
||||
font-size: 1.1rem; |
||||
} |
||||
.permissionForm hr { |
||||
width: 80%; |
||||
} |
||||
.permissionKey { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
.permissionKey i { |
||||
cursor: pointer; |
||||
} |
||||
.checkbox { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.checkbox label { |
||||
margin: 0; |
||||
font-size: 1rem; |
||||
} |
||||
` |
||||
|
||||
export class PluginManagerSettings { |
||||
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() } |
||||
) |
||||
} |
||||
|
||||
onValidation () { |
||||
const permissions = JSON.stringify(this.permissions) |
||||
window.localStorage.setItem('plugins/permissions', permissions) |
||||
} |
||||
|
||||
/** 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) { |
||||
delete this.permissions[to][method] |
||||
} |
||||
if (Object.keys(this.permissions[to]).length === 0) { |
||||
delete this.permissions[to] |
||||
} |
||||
yo.update(this.currentSetting, this.settings()) |
||||
} |
||||
} |
||||
|
||||
/** 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()) |
||||
} |
||||
|
||||
settings () { |
||||
const permissionByToPlugin = (toPlugin, funcObj) => { |
||||
const permissionByMethod = (methodName, fromPlugins) => { |
||||
const togglePermission = (fromPlugin) => { |
||||
this.permissions[toPlugin][methodName][fromPlugin].allow = !this.permissions[toPlugin][methodName][fromPlugin].allow |
||||
} |
||||
return Object.keys(fromPlugins).map(fromName => { |
||||
const fromPluginPermission = fromPlugins[fromName] |
||||
const checkbox = fromPluginPermission.allow |
||||
? yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" checked id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />` |
||||
: yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />` |
||||
return yo` |
||||
<div class="form-group ${css.permissionKey}"> |
||||
<div class="${css.checkbox}"> |
||||
${checkbox} |
||||
<label for="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" data-id="permission-label-${toPlugin}-${methodName}-${toPlugin}">Allow <u>${fromName}</u> to call <u>${methodName}</u></label> |
||||
</div> |
||||
<i onclick="${() => this.clearPersmission(fromName, toPlugin, methodName)}" class="fa fa-trash-alt" data-id="pluginManagerSettingsRemovePermission-${toPlugin}-${methodName}-${toPlugin}"></i> |
||||
</div> |
||||
` |
||||
}) |
||||
} |
||||
|
||||
const permissionsByFunctions = Object |
||||
.keys(funcObj) |
||||
.map(methodName => permissionByMethod(methodName, funcObj[methodName])) |
||||
|
||||
return yo` |
||||
<div border p-2> |
||||
<div class="pb-2 ${css.permissionKey}"> |
||||
<h3>${toPlugin} permissions:</h3> |
||||
<i onclick="${() => this.clearAllPersmission(toPlugin)}" class="far fa-trash-alt" data-id="pluginManagerSettingsClearAllPermission-${toPlugin}"></i> |
||||
</div> |
||||
${permissionsByFunctions} |
||||
</div>` |
||||
} |
||||
|
||||
const byToPlugin = Object |
||||
.keys(this.permissions) |
||||
.map(toPlugin => permissionByToPlugin(toPlugin, this.permissions[toPlugin])) |
||||
|
||||
const title = byToPlugin.length === 0 |
||||
? yo`<h4>No Permission requested yet.</h4>` |
||||
: yo`<h4>Current Permission settings</h4>` |
||||
|
||||
return yo`<form class="${css.permissionForm}" data-id="pluginManagerSettingsPermissionForm">
|
||||
${title} |
||||
<hr/> |
||||
${byToPlugin} |
||||
</form>` |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<footer class="bg-light ${css.permissions} remix-bg-opacity"> |
||||
<button onclick="${() => this.openDialog()}" class="btn btn-primary settings-button" data-id="pluginManagerPermissionsButton">Permissions</button> |
||||
</footer>` |
||||
} |
||||
} |
@ -0,0 +1,126 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { Plugin } from '@remixproject/engine' |
||||
import { AppModal } from 'libs/remix-ui/app/src' |
||||
import { PermissionHandlerDialog, PermissionHandlerValue } from 'libs/remix-ui/permission-handler/src' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
|
||||
const profile = { |
||||
name: 'permissionhandler', |
||||
displayName: 'permissionhandler', |
||||
description: 'permissionhandler', |
||||
methods: ['askPermission'] |
||||
} |
||||
|
||||
export class PermissionHandlerPlugin extends Plugin { |
||||
permissions: any |
||||
currentVersion: number |
||||
constructor() { |
||||
super(profile) |
||||
this.permissions = this._getFromLocal() |
||||
this.currentVersion = 1 |
||||
// here we remove the old permissions saved before adding 'permissionVersion'
|
||||
// since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes
|
||||
if (!localStorage.getItem('permissionVersion')) { |
||||
localStorage.setItem('plugins/permissions', '') |
||||
localStorage.setItem('permissionVersion', this.currentVersion.toString()) |
||||
} |
||||
} |
||||
|
||||
_getFromLocal() { |
||||
const permission = localStorage.getItem('plugins/permissions') |
||||
return permission ? JSON.parse(permission) : {} |
||||
} |
||||
|
||||
persistPermissions() { |
||||
const permissions = JSON.stringify(this.permissions) |
||||
localStorage.setItem('plugins/permissions', permissions) |
||||
} |
||||
|
||||
switchMode (from: Profile, to: Profile, method: string, set: boolean) { |
||||
set |
||||
? this.permissions[to.name][method][from.name] = {} |
||||
: delete this.permissions[to.name][method][from.name] |
||||
} |
||||
|
||||
clear() { |
||||
localStorage.removeItem('plugins/permissions') |
||||
} |
||||
|
||||
notAllowWarning(from: Profile, to: Profile, method: string) { |
||||
return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.` |
||||
} |
||||
|
||||
async getTheme() { |
||||
return (await this.call('theme', 'currentTheme')).quality |
||||
} |
||||
|
||||
/** |
||||
* Check if a plugin has the permission to call another plugin and askPermission if needed |
||||
* @param {PluginProfile} from the profile of the plugin that make the call |
||||
* @param {ModuleProfile} to The profile of the module that receive the call |
||||
* @param {string} method The name of the function to be called |
||||
* @param {string} message from the caller plugin to add more details if needed |
||||
* @returns {Promise<boolean>} |
||||
*/ |
||||
async askPermission(from: Profile, to: Profile, method: string, message: string) { |
||||
try { |
||||
this.permissions = this._getFromLocal() |
||||
if (!this.permissions[to.name]) this.permissions[to.name] = {} |
||||
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {} |
||||
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message) |
||||
|
||||
const { allow, hash } = this.permissions[to.name][method][from.name] |
||||
if (!allow) { |
||||
const warning = this.notAllowWarning(from, to, method) |
||||
this.call('notification', 'toast', warning) |
||||
return false |
||||
} |
||||
return hash === from.hash |
||||
? true // Allow
|
||||
: await this.openPermission(from, to, method, message) |
||||
} catch (err) { |
||||
throw new Error(err) |
||||
} |
||||
} |
||||
|
||||
async openPermission(from: Profile, to: Profile, method: string, message: string) { |
||||
const remember = this.permissions[to.name][method][from.name] |
||||
const value: PermissionHandlerValue = { |
||||
from, |
||||
to, |
||||
method, |
||||
message, |
||||
remember |
||||
} |
||||
const modal: AppModal = { |
||||
id: 'PermissionHandler', |
||||
title: `Permission needed for ${to.displayName || to.name}`, |
||||
message: <PermissionHandlerDialog plugin={this} theme={await this.getTheme()} value={value}></PermissionHandlerDialog>, |
||||
okLabel: 'Accept', |
||||
cancelLabel: 'Decline' |
||||
} |
||||
|
||||
const result = await this.call('notification', 'modal', modal) |
||||
return new Promise((resolve, reject) => { |
||||
if (result) { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: true, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
resolve(true) |
||||
} else { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: false, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
reject(this.notAllowWarning(from, to, method)) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -1,114 +0,0 @@ |
||||
var modal = require('./modaldialog.js') |
||||
var yo = require('yo-yo') |
||||
var css = require('./styles/modal-dialog-custom-styles') |
||||
|
||||
module.exports = { |
||||
alert: function (title, text) { |
||||
if (text) return modal(title, yo`<div>${text}</div>`, null, { label: null }) |
||||
return modal('Alert', yo`<div>${title}</div>`, null, { label: null }) |
||||
}, |
||||
prompt: function (title, text, inputValue, ok, cancel, focus) { |
||||
return prompt(title, text, false, inputValue, ok, cancel, focus) |
||||
}, |
||||
promptPassphrase: function (title, text, inputValue, ok, cancel) { |
||||
return prompt(title, text, true, inputValue, ok, cancel) |
||||
}, |
||||
promptPassphraseCreation: function (ok, cancel) { |
||||
var text = 'Please provide a Passphrase for the account creation' |
||||
var input = yo` |
||||
<div> |
||||
<input id="prompt1" type="password" name='prompt_text' class="${css.prompt_text}" oninput="${(e) => validateInput(e)}"> |
||||
<br> |
||||
<br> |
||||
<input id="prompt2" type="password" name='prompt_text' class="${css.prompt_text}" oninput="${(e) => validateInput(e)}"> |
||||
</div> |
||||
` |
||||
return modal(null, yo`<div>${text}<div>${input}</div></div>`, |
||||
{ |
||||
fn: () => { |
||||
if (typeof ok === 'function') { |
||||
if (input.querySelector('#prompt1').value === input.querySelector('#prompt2').value) { |
||||
ok(null, input.querySelector('#prompt1').value) |
||||
} else { |
||||
ok('Passphase does not match') |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
fn: () => { |
||||
if (typeof cancel === 'function') cancel() |
||||
} |
||||
} |
||||
) |
||||
}, |
||||
promptMulti: function ({ title, text, inputValue }, ok, cancel) { |
||||
if (!inputValue) inputValue = '' |
||||
const input = yo` |
||||
<textarea |
||||
id="prompt_text" |
||||
data-id="modalDialogCustomPromptText" |
||||
class=${css.prompt_text} |
||||
rows="4" |
||||
cols="50" |
||||
oninput="${(e) => validateInput(e)}" |
||||
></textarea> |
||||
` |
||||
return modal(title, yo`<div>${text}<div>${input}</div></div>`, |
||||
{ |
||||
fn: () => { if (typeof ok === 'function') ok(document.getElementById('prompt_text').value) } |
||||
}, |
||||
{ |
||||
fn: () => { if (typeof cancel === 'function') cancel() } |
||||
} |
||||
) |
||||
}, |
||||
confirm: function (title, text, ok, cancel) { |
||||
return modal(title, yo`<div>${text}</div>`, |
||||
{ |
||||
fn: () => { if (typeof ok === 'function') ok() } |
||||
}, |
||||
{ |
||||
fn: () => { if (typeof cancel === 'function') cancel() } |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
const validateInput = (e) => { |
||||
if (!document.getElementById('modal-footer-ok')) return |
||||
|
||||
if (e.target.value === '') { |
||||
document.getElementById('modal-footer-ok').classList.add('disabled') |
||||
document.getElementById('modal-footer-ok').style.pointerEvents = 'none' |
||||
} else { |
||||
document.getElementById('modal-footer-ok').classList.remove('disabled') |
||||
document.getElementById('modal-footer-ok').style.pointerEvents = 'auto' |
||||
} |
||||
} |
||||
|
||||
function prompt (title, text, hidden, inputValue, ok, cancel, focus) { |
||||
if (!inputValue) inputValue = '' |
||||
var type = hidden ? 'password' : 'text' |
||||
var input = yo` |
||||
<input |
||||
type=${type} |
||||
name='prompt_text' |
||||
id='prompt_text' |
||||
class="${css.prompt_text} form-control" |
||||
value='${inputValue}' |
||||
data-id="modalDialogCustomPromptText" |
||||
oninput="${(e) => validateInput(e)}" |
||||
> |
||||
` |
||||
|
||||
modal(title, yo`<div>${text}<div>${input}</div></div>`, |
||||
{ |
||||
fn: () => { if (typeof ok === 'function') ok(document.getElementById('prompt_text').value) } |
||||
}, |
||||
{ |
||||
fn: () => { if (typeof cancel === 'function') cancel() } |
||||
}, |
||||
focus ? '#prompt_text' : undefined |
||||
) |
||||
} |
@ -1,150 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var css = require('./styles/modaldialog-styles') |
||||
|
||||
let incomingModal = false // in case modals are queued, ensure we are not hiding the last one.
|
||||
module.exports = (title, content, ok, cancel, focusSelector, opts) => { |
||||
let agreed = true |
||||
let footerIsActive = false |
||||
opts = opts || {} |
||||
var container = document.getElementById('modal-dialog') |
||||
if (!container) { |
||||
document.querySelector('body').appendChild(html(opts)) |
||||
container = document.getElementById('modal-dialog') |
||||
incomingModal = false |
||||
} else incomingModal = true |
||||
|
||||
var closeDiv = document.getElementById('modal-close') |
||||
if (opts.hideClose) closeDiv.style.display = 'none' |
||||
|
||||
var okDiv = document.getElementById('modal-footer-ok') |
||||
okDiv.innerHTML = (ok && ok.label !== undefined) ? ok.label : 'OK' |
||||
okDiv.style.display = okDiv.innerHTML === '' ? 'none' : 'inline-block' |
||||
|
||||
var cancelDiv = document.getElementById('modal-footer-cancel') |
||||
cancelDiv.innerHTML = (cancel && cancel.label !== undefined) ? cancel.label : 'Cancel' |
||||
cancelDiv.style.display = cancelDiv.innerHTML === '' ? 'none' : 'inline-block' |
||||
|
||||
var modal = document.getElementById('modal-body-id') |
||||
var modalTitle = document.getElementById('modal-title-h6') |
||||
|
||||
modalTitle.innerHTML = '' |
||||
if (title) modalTitle.innerText = title |
||||
|
||||
modal.innerHTML = '' |
||||
if (content) modal.appendChild(content) |
||||
|
||||
setFocusOn('ok') |
||||
|
||||
show() |
||||
|
||||
function setFocusOn (btn) { |
||||
var okDiv = document.getElementById('modal-footer-ok') |
||||
var cancelDiv = document.getElementById('modal-footer-cancel') |
||||
if (btn === 'ok') { |
||||
okDiv.className = okDiv.className.replace(/\bbtn-light\b/g, 'btn-dark') |
||||
cancelDiv.className = cancelDiv.className.replace(/\bbtn-dark\b/g, 'btn-light') |
||||
} else { |
||||
cancelDiv.className = cancelDiv.className.replace(/\bbtn-light\b/g, 'btn-dark') |
||||
okDiv.className = okDiv.className.replace(/\bbtn-dark\b/g, 'btn-light') |
||||
} |
||||
} |
||||
|
||||
function okListener () { |
||||
removeEventListener() |
||||
if (ok && ok.fn && agreed) ok.fn() |
||||
if (!incomingModal) hide() |
||||
incomingModal = false |
||||
} |
||||
|
||||
function cancelListener () { |
||||
removeEventListener() |
||||
if (cancel && cancel.fn) cancel.fn() |
||||
if (!incomingModal) hide() |
||||
incomingModal = false |
||||
} |
||||
|
||||
function modalKeyEvent (e) { |
||||
if (e.keyCode === 27) { // Esc
|
||||
cancelListener() |
||||
} else if (e.keyCode === 13) { // Enter
|
||||
e.preventDefault() |
||||
okListener() |
||||
} else if (e.keyCode === 37 && footerIsActive) { // Arrow Left
|
||||
e.preventDefault() |
||||
agreed = true |
||||
setFocusOn('ok') |
||||
} else if (e.keyCode === 39 && footerIsActive) { // Arrow Right
|
||||
e.preventDefault() |
||||
agreed = false |
||||
setFocusOn('cancel') |
||||
} |
||||
} |
||||
|
||||
function hide () { |
||||
if (!container) return |
||||
container.style.display = 'none' |
||||
if (container.parentElement) container.parentElement.removeChild(container) |
||||
container = null |
||||
incomingModal = false |
||||
} |
||||
|
||||
function show () { |
||||
if (!container) return |
||||
container.style.display = 'block' |
||||
if (focusSelector) { |
||||
const focusTarget = document.querySelector(`.modal ${focusSelector}`) |
||||
if (focusTarget) { |
||||
focusTarget.focus() |
||||
if (typeof focusTarget.setSelectionRange === 'function') { |
||||
focusTarget.setSelectionRange(0, focusTarget.value.length) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
function removeEventListener () { |
||||
okDiv.removeEventListener('click', okListener) |
||||
cancelDiv.removeEventListener('click', cancelListener) |
||||
closeDiv.removeEventListener('click', cancelListener) |
||||
document.removeEventListener('keydown', modalKeyEvent) |
||||
if (document.getElementById('modal-background')) { |
||||
document.getElementById('modal-background').removeEventListener('click', cancelListener) |
||||
} |
||||
} |
||||
okDiv.addEventListener('click', okListener) |
||||
cancelDiv.addEventListener('click', cancelListener) |
||||
closeDiv.addEventListener('click', cancelListener) |
||||
document.addEventListener('keydown', modalKeyEvent) |
||||
|
||||
const modalDialog = document.getElementById('modal-dialog') |
||||
if (modalDialog) { |
||||
modalDialog.addEventListener('click', (e) => { |
||||
footerIsActive = document.activeElement === modalDialog |
||||
if (e.toElement === modalDialog) { |
||||
cancelListener() // click is outside of modal-content
|
||||
} |
||||
}) |
||||
} |
||||
return { container, okListener, cancelListener, hide } |
||||
} |
||||
|
||||
function html (opts) { |
||||
return yo` |
||||
<div id="modal-dialog" data-id="modalDialogContainer" data-backdrop="static" data-keyboard="false" class="modal" tabindex="-1" role="dialog"> |
||||
<div id="modal-background" class="modal-dialog" role="document"> |
||||
<div class="modal-content ${css.modalContent} ${opts.class}"> |
||||
<div class="modal-header"> |
||||
<h6 id="modal-title-h6" class="modal-title" data-id="modalDialogModalTitle"></h6> |
||||
<span class="modal-close"> |
||||
<i id="modal-close" title="Close" class="fas fa-times" aria-hidden="true"></i> |
||||
</span> |
||||
</div> |
||||
<div id="modal-body-id" class="modal-body ${css.modalBody}" data-id="modalDialogModalBody"> - </div> |
||||
<div class="modal-footer" data-id="modalDialogModalFooter" autofocus> |
||||
<span id="modal-footer-ok" class="${css.modalFooterOk} modal-ok btn btn-sm btn-light" tabindex='5'>OK</span> |
||||
<span id="modal-footer-cancel" class="${css.modalFooterCancel} modal-cancel btn btn-sm btn-light" tabindex='10' data-dismiss="modal">Cancel</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div>` |
||||
} |
@ -1,202 +0,0 @@ |
||||
import Registry from '../state/registry' |
||||
|
||||
/* global localStorage */ |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
const addTooltip = require('./tooltip') |
||||
const modalDialog = require('./modaldialog') |
||||
|
||||
const css = csjs` |
||||
.permission h4 { |
||||
text-transform: uppercase; |
||||
text-align: center; |
||||
} |
||||
.permission h6 { |
||||
text-transform: uppercase; |
||||
} |
||||
.remember { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
.images { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
padding: 10px; |
||||
} |
||||
.images img { |
||||
width: 40px; |
||||
height: 40px; |
||||
} |
||||
.images i { |
||||
margin: 0 20px; |
||||
} |
||||
` |
||||
|
||||
function notAllowWarning (from, to, method) { |
||||
return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.` |
||||
} |
||||
|
||||
export class PermissionHandler { |
||||
constructor () { |
||||
this.permissions = this._getFromLocal() |
||||
this.currentVersion = 1 |
||||
// here we remove the old permissions saved before adding 'permissionVersion'
|
||||
// since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes
|
||||
if (!localStorage.getItem('permissionVersion')) { |
||||
localStorage.setItem('plugins/permissions', '') |
||||
localStorage.setItem('permissionVersion', this.currentVersion) |
||||
} |
||||
} |
||||
|
||||
_getFromLocal () { |
||||
const permission = localStorage.getItem('plugins/permissions') |
||||
return permission ? JSON.parse(permission) : {} |
||||
} |
||||
|
||||
persistPermissions () { |
||||
const permissions = JSON.stringify(this.permissions) |
||||
localStorage.setItem('plugins/permissions', permissions) |
||||
} |
||||
|
||||
clear () { |
||||
localStorage.removeItem('plugins/permissions') |
||||
addTooltip('All Permissions have been reset') |
||||
} |
||||
|
||||
/** |
||||
* Show a message to ask the user for a permission |
||||
* @param {PluginProfile} from The name and hash of the plugin that make the call |
||||
* @param {ModuleProfile} to The name of the plugin that receive the call |
||||
* @param {string} method The name of the function to be called |
||||
* @param {string} message from the caller plugin to add more details if needed |
||||
* @returns {Promise<{ allow: boolean; remember: boolean }} Answer from the user to the permission |
||||
*/ |
||||
async openPermission (from, to, method, message) { |
||||
return new Promise((resolve, reject) => { |
||||
modalDialog( |
||||
`Permission needed for ${to.displayName || to.name}`, |
||||
this.form(from, to, method, message), |
||||
{ |
||||
label: 'Accept', |
||||
fn: () => { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: true, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
resolve(true) |
||||
} |
||||
}, |
||||
{ |
||||
label: 'Decline', |
||||
fn: () => { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: false, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
reject(notAllowWarning(from, to, method)) |
||||
} |
||||
} |
||||
) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Check if a plugin has the permission to call another plugin and askPermission if needed |
||||
* @param {PluginProfile} from the profile of the plugin that make the call |
||||
* @param {ModuleProfile} to The profile of the module that receive the call |
||||
* @param {string} method The name of the function to be called |
||||
* @param {string} message from the caller plugin to add more details if needed |
||||
* @returns {Promise<boolean>} |
||||
*/ |
||||
async askPermission (from, to, method, message) { |
||||
try { |
||||
this.permissions = this._getFromLocal() |
||||
if (!this.permissions[to.name]) this.permissions[to.name] = {} |
||||
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {} |
||||
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message) |
||||
|
||||
const { allow, hash } = this.permissions[to.name][method][from.name] |
||||
if (!allow) { |
||||
const warning = notAllowWarning(from, to, method) |
||||
addTooltip(warning) |
||||
return false |
||||
} |
||||
return hash === from.hash |
||||
? true // Allow
|
||||
: this.openPermission(from, to, method, message) // New version of a plugin
|
||||
} catch (err) { |
||||
throw new Error(err) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The permission form |
||||
* @param {PluginProfile} from The name and hash of the plugin that make the call |
||||
* @param {ModuleProfile} to The name of the plugin that receive the call |
||||
* @param {string} method The name of te methode to be called |
||||
* @param {string} message from the caller plugin to add more details if needed |
||||
*/ |
||||
form (from, to, method, message) { |
||||
const fromName = from.displayName || from.name |
||||
const toName = to.displayName || to.name |
||||
const remember = this.permissions[to.name][method][from.name] |
||||
|
||||
const switchMode = (e) => { |
||||
e.target.checked |
||||
? this.permissions[to.name][method][from.name] = {} |
||||
: delete this.permissions[to.name][method][from.name] |
||||
} |
||||
const rememberSwitch = remember |
||||
? yo`<input type="checkbox" onchange="${switchMode}" checkbox class="form-check-input" id="remember" data-id="permissionHandlerRememberChecked">` |
||||
: yo`<input type="checkbox" onchange="${switchMode}" class="form-check-input" id="remember" data-id="permissionHandlerRememberUnchecked">` |
||||
const text = `"${fromName}" ${(remember ? 'has changed and' : '')} would like to access to "${method}" of "${toName}"` |
||||
const imgFrom = yo`<img id="permissionModalImagesFrom" src="${from.icon}" />` |
||||
const imgTo = yo`<img id="permissionModalImagesTo" src="${to.icon}" />` |
||||
const pluginsImages = yo` |
||||
<article class="${css.images}"> |
||||
${imgFrom} |
||||
<i class="fas fa-arrow-right"></i> |
||||
${imgTo} |
||||
</article> |
||||
` |
||||
|
||||
Registry.getInstance().get('themeModule').api.fixInvert(imgFrom) |
||||
Registry.getInstance().get('themeModule').api.fixInvert(imgTo) |
||||
|
||||
const pluginMessage = message ? yo` |
||||
<div> |
||||
<h6>Description</h6> |
||||
<p>${message}</p> |
||||
</div> |
||||
` : ''
|
||||
return yo` |
||||
<section class="${css.permission}"> |
||||
${pluginsImages} |
||||
<article> |
||||
<h4 data-id="permissionHandlerMessage">${text} :</h4> |
||||
<h6>${fromName}</h6> |
||||
<p>${from.description || yo`<i>No description Provided</i>`}</p> |
||||
<h6>${toName} :</p> |
||||
<p>${to.description || yo`<i>No description Provided</i>`}</p> |
||||
${pluginMessage} |
||||
</article> |
||||
|
||||
<article class="${css.remember}"> |
||||
<div class="form-check"> |
||||
${rememberSwitch} |
||||
<label class="form-check-label" for="remember" data-id="permissionHandlerRememberChoice">Remember this choice</label> |
||||
</div> |
||||
<button class="btn btn-sm" onclick="${_ => this.clear()}">Reset all Permissions</button> |
||||
</article> |
||||
</section> |
||||
` |
||||
} |
||||
} |
@ -1,110 +0,0 @@ |
||||
/* global Element */ |
||||
var yo = require('yo-yo') |
||||
var css = require('./styles/tooltip-styles') |
||||
var modal = require('./modal-dialog-custom') |
||||
|
||||
/** |
||||
* Open a tooltip |
||||
* @param {string} tooltipText The text shown by the tooltip |
||||
* @param {function} [action] Returns An HTMLElement to display for action |
||||
*/ |
||||
module.exports = function addTooltip (tooltipText, action, opts) { |
||||
action = action || function () { return yo`<div></div>` } |
||||
const t = new Toaster() |
||||
return t.render(tooltipText, action(t), opts) |
||||
} |
||||
|
||||
class Toaster { |
||||
hide () { |
||||
if (this.id) clearTimeout(this.id) |
||||
setTimeout(() => { |
||||
// remove from body after the animation is finished
|
||||
if (this.tooltip.parentElement) this.tooltip.parentElement.removeChild(this.tooltip) |
||||
}, 2000) |
||||
animation(this.tooltip, css.animateTop.className) |
||||
} |
||||
|
||||
render (tooltipText, actionElement, opts) { |
||||
opts = defaultOptions(opts) |
||||
let canShorten = true |
||||
if (tooltipText instanceof Element) { |
||||
canShorten = false |
||||
} else { |
||||
if (typeof tooltipText === 'object') { |
||||
if (tooltipText.message) { |
||||
tooltipText = tooltipText.message |
||||
} else { |
||||
try { |
||||
tooltipText = JSON.stringify(tooltipText) |
||||
} catch (e) { |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return new Promise((resolve, reject) => { |
||||
const shortTooltipText = (canShorten && tooltipText.length > 201) ? tooltipText.substring(0, 200) + '...' : tooltipText |
||||
this.resolveFn = resolve |
||||
|
||||
function showFullMessage () { |
||||
modal.alert(tooltipText) |
||||
} |
||||
|
||||
function closeTheToaster (self) { |
||||
self.hide() |
||||
over() |
||||
resolve() |
||||
} |
||||
const button = tooltipText.length > 201 ? yo` |
||||
<button class="btn btn-secondary btn-sm mx-3" style="white-space: nowrap;" onclick=${() => showFullMessage()}>Show full message</button> |
||||
` : ''
|
||||
|
||||
this.tooltip = yo` |
||||
<div data-shared="tooltipPopup" class="${css.tooltip} alert alert-info p-2" onmouseenter=${() => { over() }} onmouseleave=${() => { out() }}> |
||||
<span class="px-2"> |
||||
${shortTooltipText} |
||||
${button} |
||||
${actionElement} |
||||
</span> |
||||
<span style="align-self: baseline;"> |
||||
<button data-id="tooltipCloseButton" class="fas fa-times btn-info mx-1 p-0" onclick=${() => closeTheToaster(this)}></button> |
||||
</span> |
||||
</div>` |
||||
const timeOut = () => { |
||||
return setTimeout(() => { |
||||
if (this.id) { |
||||
this.hide() |
||||
resolve() |
||||
} |
||||
}, opts.time) |
||||
} |
||||
const over = () => { |
||||
if (this.id) { |
||||
clearTimeout(this.id) |
||||
this.id = null |
||||
} |
||||
} |
||||
const out = () => { |
||||
if (!this.id) this.id = timeOut() |
||||
} |
||||
this.id = timeOut() |
||||
document.body.appendChild(this.tooltip) |
||||
animation(this.tooltip, css.animateBottom.className) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
const defaultOptions = (opts) => { |
||||
opts = opts || {} |
||||
return { |
||||
time: opts.time || 7000 |
||||
} |
||||
} |
||||
|
||||
const animation = (tooltip, anim) => { |
||||
tooltip.classList.remove(css.animateTop.className) |
||||
tooltip.classList.remove(css.animateBottom.className) |
||||
// eslint-disable-next-line
|
||||
void tooltip.offsetWidth // trick for restarting the animation
|
||||
tooltip.classList.add(anim) |
||||
} |
@ -1 +1,2 @@ |
||||
export * from './lib/remix-ui-helper' |
||||
export * from './lib/helper-components' |
||||
|
@ -0,0 +1,55 @@ |
||||
import React from 'react' |
||||
|
||||
export const fileChangedToastMsg = (from: string, path: string) => ( |
||||
<div><i className="fas fa-exclamation-triangle text-danger mr-1"></i> |
||||
<span> |
||||
{from} |
||||
<span className="font-weight-bold text-warning"> |
||||
is modifying
|
||||
</span>{path} |
||||
</span> |
||||
</div> |
||||
) |
||||
|
||||
export const compilerConfigChangedToastMsg = (from: string, value: string) => ( |
||||
<div> |
||||
<b>{ from }</b> is updating the <b>Solidity compiler configuration</b>. |
||||
<pre className="text-left">{value}</pre> |
||||
</div> |
||||
) |
||||
|
||||
export const compileToastMsg = (from: string, fileName: string) => ( |
||||
<div> |
||||
<b>{from}</b> is requiring to compile <b>{fileName}</b> |
||||
</div> |
||||
) |
||||
|
||||
export const compilingToastMsg = (settings: string) => ( |
||||
<div> |
||||
<b>Recompiling and debugging with params</b> |
||||
<pre className="text-left">{settings}</pre></div> |
||||
) |
||||
|
||||
export const compilationFinishedToastMsg = () => ( |
||||
<div> |
||||
<b>Compilation failed...</b> continuing <i>without</i> source code debugging. |
||||
</div> |
||||
) |
||||
|
||||
export const notFoundToastMsg = (address: string) => ( |
||||
<div> |
||||
<b>Contract {address} not found in source code repository</b> continuing <i>without</i> source code debugging. |
||||
</div> |
||||
) |
||||
|
||||
export const localCompilationToastMsg = () => ( |
||||
<div> |
||||
<b>Using compilation result from Solidity module</b> |
||||
</div> |
||||
) |
||||
|
||||
export const sourceVerificationNotAvailableToastMsg = () => ( |
||||
<div> |
||||
<b>Source verification plugin not activated or not available.</b> continuing <i>without</i> source code debugging. |
||||
</div> |
||||
) |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,18 @@ |
||||
{ |
||||
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc"], |
||||
"ignorePatterns": ["!**/*"], |
||||
"overrides": [ |
||||
{ |
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], |
||||
"rules": {} |
||||
}, |
||||
{ |
||||
"files": ["*.ts", "*.tsx"], |
||||
"rules": {} |
||||
}, |
||||
{ |
||||
"files": ["*.js", "*.jsx"], |
||||
"rules": {} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,2 @@ |
||||
export { default as PermissionHandlerDialog } from './lib/permission-dialog' |
||||
export { PermissionHandlerValue, PermissionHandlerProps } from './interface/index' |
@ -0,0 +1,15 @@ |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
|
||||
export interface PermissionHandlerValue { |
||||
from: Profile, |
||||
to: Profile, |
||||
remember: boolean, |
||||
method: string, |
||||
message: string |
||||
} |
||||
|
||||
export interface PermissionHandlerProps { |
||||
value: PermissionHandlerValue |
||||
theme: string |
||||
plugin: any |
||||
} |
@ -0,0 +1,29 @@ |
||||
.permission h4 { |
||||
text-transform: uppercase; |
||||
text-align: center; |
||||
} |
||||
.permission h6 { |
||||
text-transform: uppercase; |
||||
} |
||||
.remember { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
.images { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
padding: 10px; |
||||
} |
||||
.images img { |
||||
width: 40px; |
||||
height: 40px; |
||||
} |
||||
.images i { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
.invert { |
||||
filter: invert(1); |
||||
} |
@ -0,0 +1,67 @@ |
||||
import React, { ChangeEventHandler, useContext, useEffect, useRef, useState } from 'react' // eslint-disable-line
|
||||
import { PermissionHandlerProps } from '../interface' |
||||
import './permission-dialog.css' |
||||
|
||||
const PermissionHandlerDialog = (props: PermissionHandlerProps) => { |
||||
const { from, to, remember, method, message } = props.value |
||||
const [feedback, setFeedback] = useState<string>('') |
||||
const theme = props.theme |
||||
|
||||
const switchMode = (e: any) => { |
||||
props.plugin.switchMode(from, to, method, e.target.checked) |
||||
} |
||||
|
||||
const rememberSwitch = () => { |
||||
return <input type="checkbox" onChange={switchMode} className='form-check-input' id='remember' data-id={remember ? 'permissionHandlerRememberChecked' : 'permissionHandlerRememberUnchecked'}/> |
||||
} |
||||
const reset = () => { |
||||
props.plugin.clear() |
||||
setFeedback('All permisssions have been reset.') |
||||
} |
||||
|
||||
const imgFrom = () => { return <img className={`${theme === 'dark' ? 'invert' : ''}`} alt='' id='permissionModalImagesFrom' src={from.icon} /> } |
||||
const imgTo = () => { return <img className={`${theme === 'dark' ? 'invert' : ''}`} alt='' id='permissionModalImagesTo' src={to.icon} /> } |
||||
const pluginsImages = () => { |
||||
return ( |
||||
<article className='images'> |
||||
{imgFrom()} |
||||
<i className="fas fa-arrow-right"></i> |
||||
{imgTo()} |
||||
</article> |
||||
) |
||||
} |
||||
|
||||
const text = () => { |
||||
return <>"{from.displayName}" {(remember ? 'has changed and' : '')} would like to access to "{method}" of "{to.displayName}"`</>
|
||||
} |
||||
|
||||
const pluginMessage = () => { |
||||
return message |
||||
? <div> |
||||
<h6>Description</h6> |
||||
<p>{message}</p> |
||||
</div> : null |
||||
} |
||||
|
||||
return (<section className="permission"> |
||||
{pluginsImages()} |
||||
<article> |
||||
<h4 data-id="permissionHandlerMessage">{text()} :</h4> |
||||
<h6>{from.displayName}</h6> |
||||
<p> {from.description || <i>No description Provided</i>}</p> |
||||
<h6>{to.displayName} :</h6> |
||||
<p> {to.description || <i>No description Provided</i>}</p> |
||||
{pluginMessage()} |
||||
</article> |
||||
<article className='remember'> |
||||
<div className='form-check'> |
||||
{rememberSwitch()} |
||||
<label className="form-check-label" data-id="permissionHandlerRememberChoice">Remember this choice</label> |
||||
</div> |
||||
<button className="btn btn-sm" onClick={reset}>Reset all Permissions</button> |
||||
</article> |
||||
<div>{feedback}</div> |
||||
</section>) |
||||
} |
||||
|
||||
export default PermissionHandlerDialog |
@ -0,0 +1,20 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"jsx": "react-jsx", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"forceConsistentCasingInFileNames": true, |
||||
"strict": true, |
||||
"noImplicitReturns": true, |
||||
"noFallthroughCasesInSwitch": 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