Skip to content

Commit

Permalink
feat: IPFS experiments UI (#1048)
Browse files Browse the repository at this point in the history
License: MIT
Signed-off-by: Chris Waring <chris@wwaves.co>
Signed-off-by: Henrique Dias <hacdias@gmail.com>
  • Loading branch information
cwaring authored and hacdias committed Jun 12, 2019
1 parent babbb6d commit 7b15c76
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 6 deletions.
5 changes: 4 additions & 1 deletion public/locales/en/notify.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"filesEventFailed": "Failed to add files to IPFS. Please try again.",
"folderExists": "A folder with that name already exists. Please choose another.",
"couldntConnectToPeer": "Could not connect to the provided peer.",
"connectedToPeer": "Successfully connected to the provided peer."
"connectedToPeer": "Successfully connected to the provided peer.",
"experimentsErrors": {
"npmOnIpfs": "We couldn't make changes to your system. Please install or uninstall 'ipfs-npm' package manually."
}
}
11 changes: 11 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@
"details": "Records JavaScript error messages and stack traces that occur while using the app, where possible. It is very helpful to know when the app is not working for you, but <1>error messages may include identifiable information</1> like CIDs or file paths, so only enable this if you are comfortable sharing that information with us."
}
},
"Experiments": {
"description": "Here you can get an early preview into new IPFS features by enabling options below. We are testing these ideas, and they may not be perfect yet so we'd love to hear your feedback.",
"issueUrl": "Open an issue",
"feedbackUrl": "💌Leave feedback",
"readMoreUrl": "Read More",
"npmOnIpfs": {
"title": "NPM on IPFS",
"label": "Enable ipfs-npm",
"description": "Install npm modules through IPFS with the 'ipfs-npm' command line tool!"
}
},
"ipfsCmdTools": "IPFS command line tools",
"ipfsCmdToolsDescription": "Add <0>ipfs</0> binary to your system <0>PATH</0> so you can use it in the command line."
}
11 changes: 9 additions & 2 deletions src/bundles/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const ASYNC_ACTIONS_TO_RECORD = [
'FILES_ADDBYPATH',
'FILES_MOVE',
'FILES_DELETE',
'FILES_DOWNLOADLINK'
'FILES_DOWNLOADLINK',
'EXPERIMENTS_TOGGLE'
]

const ASYNC_ACTION_RE = new RegExp(`^${ASYNC_ACTIONS_TO_RECORD.join('_|')}`)
Expand Down Expand Up @@ -106,8 +107,14 @@ const createAnalyticsBundle = ({
EventMap.delete(name)
} else {
const durationInSeconds = (root.performance.now() - start) / 1000
let key = state === 'FAILED' ? action.type : name

if (name === 'EXPERIMENTS_TOGGLE') {
key += `_${action.payload.key}`
}

root.Countly.q.push(['add_event', {
key: state === 'FAILED' ? action.type : name,
key: key,
count: 1,
dur: durationInSeconds
}])
Expand Down
125 changes: 125 additions & 0 deletions src/bundles/experiments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { createSelector } from 'redux-bundler'

export const ACTIONS = {
EXP_TOGGLE_STARTED: 'EXPERIMENTS_TOGGLE_STARTED',
EXP_TOGGLE_FINISHED: 'EXPERIMENTS_TOGGLE_FINISHED',
EXP_TOGGLE_FAILED: 'EXPERIMENTS_TOGGLE_FAILED',
EXP_UPDATE_STATE: 'EXPERIMENTS_UPDATE_STATE'
}

const EXPERIMENTS = [
{
key: 'npmOnIpfs',
actionUrls: [
{
url: 'https://github.com/ipfs-shipyard/npm-on-ipfs',
key: 'readMoreUrl'
},
{
url: 'https://github.com/ipfs-shipyard/npm-on-ipfs/issues',
key: 'issueUrl'
},
{
url: 'https://github.com/ipfs-shipyard/ipfs-desktop/issues/957',
key: 'feedbackUrl'
}
],
desktopOnly: true
}
]

const mergeState = (state, payload) =>
Object.keys(payload).reduce(
(all, key) => ({
...all,
[key]: {
...state[key],
...payload[key]
}
}),
state
)

const toggleEnabled = (state, key) => {
return unblock(
{
...state,
[key]: {
...state[key],
enabled: !(state && state[key] && state[key].enabled)
}
},
key
)
}

const unblock = (state, key) => {
return {
...state,
[key]: {
...state[key],
blocked: false
}
}
}

const block = (state, key) => {
return {
...state,
[key]: {
...state[key],
blocked: true
}
}
}

export default {
name: 'experiments',

persistActions: [
ACTIONS.EXP_TOGGLE_FINISHED,
ACTIONS.EXP_TOGGLE_FAILED,
ACTIONS.EXP_UPDATE_STATE
],

reducer: (state = {}, action) => {
if (action.type === ACTIONS.EXP_TOGGLE_STARTED) {
return block(state, action.payload.key)
}

if (action.type === ACTIONS.EXP_UPDATE_STATE) {
return mergeState(state, action.payload)
}

if (action.type === ACTIONS.EXP_TOGGLE_FINISHED) {
return toggleEnabled(state, action.payload.key)
}

if (action.type === ACTIONS.EXP_TOGGLE_FAILED) {
return unblock(state, action.payload.key)
}

return state
},

doExpToggleAction: key => ({ dispatch }) => {
if (!key) return

dispatch({
type: ACTIONS.EXP_TOGGLE_STARTED,
payload: { key }
})
},

selectExperimentsState: state => state.experiments,

selectExperiments: createSelector(
'selectIsIpfsDesktop',
'selectExperimentsState',
(isDesktop, state) =>
EXPERIMENTS.filter(e => !!e.desktopOnly === isDesktop).map(e => ({
...e,
...state[e.key]
}))
)
}
2 changes: 2 additions & 0 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import bundleCache from '../lib/bundle-cache'
import ipfsDesktop from './ipfs-desktop'
import repoStats from './repo-stats'
import createAnalyticsBundle from './analytics'
import experimentsBundle from './experiments'

export default composeBundles(
createCacheBundle(bundleCache.set),
Expand Down Expand Up @@ -76,6 +77,7 @@ export default composeBundles(
notifyBundle,
connectedBundle,
retryInitBundle,
experimentsBundle,
ipfsDesktop,
repoStats,
createAnalyticsBundle({})
Expand Down
39 changes: 36 additions & 3 deletions src/bundles/ipfs-desktop.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ACTIONS } from './experiments'

let bundle = {
name: 'ipfsDesktop',
reducer: (state = {}) => state,
Expand All @@ -8,6 +10,10 @@ if (window.ipfsDesktop) {
bundle = {
...bundle,
reducer: (state = {}, action) => {
if (action.type === ACTIONS.EXP_TOGGLE_STARTED) {
window.ipfsDesktop.toggleSetting(`experiments.${action.payload.key}`)
}

if (!action.type.startsWith('DESKTOP_')) {
return state
}
Expand All @@ -23,16 +29,43 @@ if (window.ipfsDesktop) {

selectDesktopVersion: () => window.ipfsDesktop.version,

doDesktopStartListening: () => async ({ dispatch }) => {
window.ipfsDesktop.onConfigChanged(config => {
doDesktopStartListening: () => async ({ dispatch, store }) => {
window.ipfsDesktop.onConfigChanged(({ config, changed, success }) => {
const prevConfig = store.selectDesktopSettings()

if (Object.keys(prevConfig).length === 0) {
dispatch({
type: ACTIONS.EXP_UPDATE_STATE,
payload: Object.keys(config.experiments).reduce(
(all, key) => ({
...all,
[key]: {
enabled: config.experiments[key]
}
}),
{}
)
})
}

if (changed && changed.startsWith('experiments.')) {
const key = changed.replace('experiments.', '')

if (success) {
dispatch({ type: ACTIONS.EXP_TOGGLE_FINISHED, payload: { key } })
} else {
dispatch({ type: ACTIONS.EXP_TOGGLE_FAILED, payload: { key } })
}
}

dispatch({
type: 'DESKTOP_SETTINGS_CHANGED',
payload: config
})
})
},

doDesktopSettingsToggle: (setting) => () => {
doDesktopSettingsToggle: setting => () => {
window.ipfsDesktop.toggleSetting(setting)
},

Expand Down
10 changes: 10 additions & 0 deletions src/bundles/notify.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSelector } from 'redux-bundler'
import { ACTIONS as EXP_ACTIONS } from './experiments'

/*
# Notify
Expand Down Expand Up @@ -59,6 +60,15 @@ const notify = {
}
}

if (action.type === EXP_ACTIONS.EXP_TOGGLE_FAILED) {
return {
...state,
show: true,
error: true,
eventId: `experimentsErrors.${action.payload.key}`
}
}

return state
},

Expand Down
63 changes: 63 additions & 0 deletions src/components/experiments/ExperimentsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react'
import { connect } from 'redux-bundler-react'
import Checkbox from '../checkbox/Checkbox'
import Box from '../box/Box'
import Title from '../../settings/Title'

const Experiments = ({ doExpToggleAction, experiments, t }) => {
// if there are no experiments to show don't render
if (experiments && experiments.length > 0) {
const tkey = (selector, key) =>
t(`Experiments.${key ? `${key}.${selector}` : `${selector}`}`)
return (
<Box className='mb3 pa4 lh-copy'>
<Title>{t('experiments')}</Title>
<p className='db mv4 f6 charcoal mw7'>{tkey('description')}</p>
<div className='flex flex-wrap pb3'>
{experiments.map(({ key, actionUrls, enabled, blocked }) => {
return (
<div
key={key}
className='pa3 mr3 mb3 mw6 br3 bg-white dib f6 ba b1 b--light-gray'
>
<h3>{tkey('title', key)}</h3>
<p className='charcoal'>{tkey('description', key)}</p>
<Checkbox
className='dib'
disabled={blocked}
onChange={() => doExpToggleAction(key, enabled)}
checked={enabled}
label={<span className='fw5 f6'>{tkey('label', key)}</span>}
/>
{actionUrls && (
<div className='mv3'>
{actionUrls.map((action, i) => (
<a
target='_blank'
rel='noopener noreferrer'
key={action.key}
className={`link blue pr2 ${i > 0 &&
'bl b1 pl2 b--light-gray'}`}
href={action.url}
>
{tkey(action.key)}
</a>
))}
</div>
)}
</div>
)
})}
</div>
</Box>
)
} else {
return null
}
}

export default connect(
'selectExperiments',
'doExpToggleAction',
Experiments
)
3 changes: 3 additions & 0 deletions src/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LanguageSelector from '../components/language-selector/LanguageSelector'
import AnalyticsToggle from '../components/analytics-toggle/AnalyticsToggle'
import JsonEditor from './editor/JsonEditor'
import DesktopSettings from './DesktopSettings'
import Experiments from '../components/experiments/ExperimentsPanel'
import Title from './Title'

const PAUSE_AFTER_SAVE_MS = 3000
Expand Down Expand Up @@ -40,6 +41,8 @@ export const SettingsPage = ({
</div>
</Box>

<Experiments t={t} />

<Box>
<Title>{t('config')}</Title>
<div className='flex pb3'>
Expand Down

0 comments on commit 7b15c76

Please sign in to comment.