Skip to content

Commit

Permalink
Feature - CLI Tutor Mode (#1572)
Browse files Browse the repository at this point in the history
* [WIP] Feature - CLI Tutor Mode

Signed-off-by: Jasdeep Singh <[email protected]>

* Update src/components/cli-tutor-mode/CliTutorMode.js

Co-authored-by: Jessica Schilling <[email protected]>

* Update src/components/cli-tutor-mode/CliTutorMode.js

Co-authored-by: Jessica Schilling <[email protected]>

* Align fix: cli tutor modal buttons

Signed-off-by: Jasdeep Singh <[email protected]>

* Fix: added compatibility with ApiAddressForm component

Signed-off-by: Jasdeep Singh <[email protected]>

* Fix: added bundle-reactx pattern and en translation keys

Signed-off-by: Jasdeep Singh <[email protected]>

* i18n text change

* Standardize i18n and visual style on Settings page

* Text tweak; replace CliTutorMode.css with tachyons equivalents

* Left-justify shell content in modals for consistency

* Tidy visual presentation of modals

* Size tweak for icon on Files page

* Use StrokeCode instead of CopyIcon, adjust text accordingly

* Lighten icon on settings page explainer text

* Margin consistency

* Update src/bundles/files/consts.js

Co-authored-by: Marcin Rataj <[email protected]>

* Move lh-copy so doesn't fubar checkbox styling

* Fix: Close modal on copy to clipboard

Signed-off-by: Jasdeep Singh <[email protected]>

* Fix: rename file command

Signed-off-by: Jasdeep Singh <[email protected]>

* Fix: delete file by filepath and added type information for cliCommandList

Signed-off-by: Jasdeep Singh <[email protected]>

* fix: whitespace in ipfs files rm

Without this below will fail:
ipfs files rm -r /test/white space/flowers.jpeg

* fix: remove UPDATE_API_SERVER_ADDRESS

This removed CLI tutor from "API Adddress" selector because it does not
make sense:

"API ADDRESS" section does not change the config of IPFS node, but
defines the API which WebUI will use when connecting to the node.

* fix: eslint

* fix: remove angle brackets from i18n strings

text between < and > won't be translated because Transifex blackboxes
HTML tags, it is better to remove it

* Move cliModal i18n keys into app.json

See https://react.i18next.com/guides/multiple-translation-files

* Move copyCommand & relevant close i18n keys into app.json

* fix: include files cp step

The way import via Webui works is:
1. `ipfs add` produces CID
2. we create a reference to that CID in MFS

We do this to produce the same CID as regular ipfs add from CLI would
do, mainly to avoid issue described in:
#676

* refactor: make it easier to support path

Right now CLI tutor defaults to MFS root at /,
this makes it easier to include current path in the future.

Co-authored-by: Jessica Schilling <[email protected]>
Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
3 people authored Aug 26, 2020
1 parent ea1a836 commit b08b14e
Show file tree
Hide file tree
Showing 19 changed files with 417 additions and 35 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ yarn-debug.log*
yarn-error.log*

.vscode
.idea
8 changes: 8 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"actions": {
"close": "Close",
"copyCommand": "Copy"
},
"cliModal": {
"description": "Paste the following into your terminal to do this task in IPFS via the command line. Remember that you'll need to replace placeholders with your specific parameters.",
"extraNotes": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file."
},
"tour": {
"back": "Back",
"close": "Close",
Expand Down
6 changes: 5 additions & 1 deletion public/locales/en/peers.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"insertPeerAddress": "Insert the peer address you want to connect to.",
"add": "Add",
"example": "Example:",
"plusPeers": "+ {number} more peers",
"plusPeers": "+ {number} more peers",
"tour": {
"back": "Back",
"close": "Close",
Expand All @@ -34,5 +34,9 @@
"title": "Peers table",
"paragraph1": "Check the IDs of the connected peers, their address and approximate location."
}
},
"actions": {
"edit": "Change",
"close": "Close"
}
}
5 changes: 5 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"language": "Language",
"analytics": "Analytics",
"api": "API Address",
"cliTutorMode": "CLI Tutor Mode",
"config": "IPFS Config",
"languageModal": {
"title": "Change language",
Expand All @@ -18,6 +19,7 @@
"close": "Close"
},
"apiDescription": "<0>If your node is configured with a <1>custom API address</1>, including a port other than the default 5001, enter it here to update your config file.</0>",
"cliDescription": "<0>Enable this option to display a \"view code\" <1></1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.</0>",
"fetchingSettings": "Fetching settings...",
"configApiNotAvailable": "The IPFS config API is not available. Please disable the \"IPFS Companion\" Web Extension and try again.",
"ipfsDaemonOffline": "The IPFS daemon is offline. Please turn it on and try again.",
Expand Down Expand Up @@ -62,6 +64,9 @@
"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."
}
},
"cliToggle": {
"label": "Enable command-line interface (CLI) tutor mode"
},
"tour": {
"back": "Back",
"close": "Close",
Expand Down
74 changes: 74 additions & 0 deletions src/bundles/cli-tutor-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'

const bundle = createAsyncResourceBundle({
name: 'cliTutorMode',
actionBaseType: 'CLI_TUTOR_MODE_TOGGLE',
persist: true,
checkIfOnline: false,
getPromise: () => {}
})

bundle.reactIsCliTutorModeEnabled = createSelector(
'selectIsCliTutorModeEnabled',
(isCliTutorModeEnabled) => {
const isEnabled = Boolean(JSON.parse(localStorage.getItem('isCliTutorModeEnabled')))

if (isCliTutorModeEnabled !== undefined && isCliTutorModeEnabled !== isEnabled) {
localStorage.setItem('isCliTutorModeEnabled', isCliTutorModeEnabled)
}
}
)

bundle.selectIsCliTutorModeEnabled = state => state.cliTutorMode.isCliTutorModeEnabled

bundle.selectIsCliTutorModalOpen = state => !!state.cliTutorMode.showCliTutorModal

bundle.selectCliOptions = state => state.cliTutorMode.cliOptions

bundle.reducer = (state = {}, action) => {
if (action.type === 'CLI_TUTOR_MODE_TOGGLE') {
return { ...state, isCliTutorModeEnabled: action.payload }
}
if (action.type === 'CLI_TUTOR_MODAL_ENABLE') {
return { ...state, showCliTutorModal: action.payload }
}
if (action.type === 'CLI_OPTIONS') {
return { ...state, cliOptions: action.payload }
}

return state
}

bundle.doToggleCliTutorMode = key => ({ dispatch }) => {
dispatch({
type: 'CLI_TUTOR_MODE_TOGGLE',
payload: key
})
}

bundle.doSetCliOptions = cliOptions => ({ dispatch }) => {
dispatch({
type: 'CLI_OPTIONS',
payload: cliOptions
})
}

bundle.doOpenCliTutorModal = openModal => ({ dispatch }) => {
dispatch({
type: 'CLI_TUTOR_MODAL_ENABLE',
payload: openModal
})
}

bundle.doOpenCliTutorModal = openModal => ({ dispatch }) => {
dispatch({
type: 'CLI_TUTOR_MODAL_ENABLE',
payload: openModal
})
}

bundle.init = store => {
const isEnabled = Boolean(JSON.parse(localStorage.getItem('isCliTutorModeEnabled')))
return store.doToggleCliTutorMode(isEnabled)
}
export default bundle
2 changes: 1 addition & 1 deletion src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const fileFromStats = ({ cumulativeSize, type, size, cid, name, path, pinned, is
* @returns {string}
*/
// TODO: use sth else
const realMfsPath = (path) => {
export const realMfsPath = (path) => {
if (path.startsWith('/files')) {
return path.substr('/files'.length) || '/'
}
Expand Down
55 changes: 55 additions & 0 deletions src/bundles/files/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,58 @@ export const DEFAULT_STATE = {
finished: [],
failed: []
}

export const cliCmdKeys = {
DOWNLOAD_OBJECT_COMMAND: 'downloadObjectCommand',
DELETE_FILE_FROM_IPFS: 'deleteFileFromIpfs',
UPDATE_IPFS_CONFIG: 'updateIpfsConfig',
PIN_OBJECT: 'pinObject',
RENAME_IPFS_OBJECT: 'renameObject',
ADD_FILE: 'addNewFile',
ADD_DIRECTORY: 'addNewDirectory',
CREATE_NEW_DIRECTORY: 'createNewDirectory',
FROM_IPFS: 'fromIpfs',
ADD_NEW_PEER: 'addNewPeer'
}

export const cliCommandList = {
[cliCmdKeys.UPDATE_IPFS_CONFIG]: () => 'ipfs config replace <path-to-settings.json>',
/**
* @param {string} filePath
*/
[cliCmdKeys.DELETE_FILE_FROM_IPFS]: (filePath) => `ipfs files rm -r "${filePath}"`,
/**
* @param {string} cid
*/
[cliCmdKeys.DOWNLOAD_OBJECT_COMMAND]: (cid) => `ipfs get ${cid}`,
/**
* @param {string} cid
* @param {string} op
*/
[cliCmdKeys.PIN_OBJECT]: (cid, op) => `ipfs pin ${op} ${cid}`,
/**
* @param {string} filePath
* @param {string} fileName
*/
[cliCmdKeys.RENAME_IPFS_OBJECT]: (filePath, fileName) => {
const prefix = filePath.replace(fileName, '').trim()
return `ipfs files mv "${filePath}" "${prefix}<new-name>"`
},
/**
* @param {string} path
*/
[cliCmdKeys.ADD_FILE]: (path) => `ipfs files cp /ipfs/$(ipfs add -Q <local-file>) "${path}/<dest-name>"`,
/**
* @param {string} path
*/
[cliCmdKeys.ADD_DIRECTORY]: (path) => `ipfs files cp /ipfs/$(ipfs add -r -Q <local-folder>) "${path}/<dest-name>"`,
/**
* @param {string} path
*/
[cliCmdKeys.CREATE_NEW_DIRECTORY]: (path) => `ipfs files mkdir "${path}/<folder-name>"`,
/**
* @param {string} path
*/
[cliCmdKeys.FROM_IPFS]: (path) => `ipfs cp /ipfs/<cid> "${path}/<dest-name>"`,
[cliCmdKeys.ADD_NEW_PEER]: () => 'ipfs swarm connect <peer-multiaddr>'
}
2 changes: 2 additions & 0 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ipfsDesktop from './ipfs-desktop'
import repoStats from './repo-stats'
import createAnalyticsBundle from './analytics'
import experimentsBundle from './experiments'
import cliTutorModeBundle from './cli-tutor-mode'

export default composeBundles(
createCacheBundle({
Expand All @@ -46,5 +47,6 @@ export default composeBundles(
experimentsBundle,
ipfsDesktop,
repoStats,
cliTutorModeBundle,
createAnalyticsBundle({})
)
1 change: 1 addition & 0 deletions src/components/api-address-form/ApiAddressForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress = '' }) => {
onSubmit(event)
}
}

return (
<form onSubmit={onSubmit}>
<label htmlFor='api-address' className='db f7 mb2 ttu tracked charcoal pl1'>{t('apiAddressForm.apiLabel')}</label>
Expand Down
2 changes: 1 addition & 1 deletion src/components/button/Button.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import './Button.css'

const Button = ({ bg = 'bg-teal', color = 'white', fill = 'white', className = '', disabled, danger, minWidth = 140, children, style, ...props }) => {
const Button = ({ bg = 'bg-teal', color = 'white', fill = 'white', className = '', disabled, danger, minWidth = 86, children, style, ...props }) => {
const bgClass = danger ? 'bg-red' : disabled ? 'bg-gray-muted' : bg
const fillClass = danger ? 'fill-white' : disabled ? 'fill-snow' : fill
const colorClass = danger ? 'white' : disabled ? 'light-gray' : color
Expand Down
107 changes: 107 additions & 0 deletions src/components/cli-tutor-mode/CliTutorMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'redux-bundler-react'

// Components
import { Modal, ModalBody, ModalActions } from '../modal/Modal'
import StrokeCode from '../../icons/StrokeCode'
import Button from '../button/Button'
import Overlay from '../overlay/Overlay'
import Shell from '../shell/Shell'
import StrokeDownload from '../../icons/StrokeDownload'
import { cliCmdKeys, cliCommandList } from '../../bundles/files/consts'

export const CliTutorialModal = ({ command, t, onLeave, className, downloadConfig, ...props }) => {
const onClickCopyToClipboard = (cmd) => {
navigator.clipboard.writeText(cmd).then(() => {
onLeave()
})
}

return (
<Modal {...props} className={className} onCancel={onLeave} style={{ maxWidth: '40em' }}>
<ModalBody icon={StrokeCode}>
<p className='charcoal w-80 center' style={{ lineHeight: '1.3' }}>
{t('app:cliModal.description')}
</p>
<p className='charcoal-muted w-90 center'>
{ command && command === cliCommandList[cliCmdKeys.UPDATE_IPFS_CONFIG]() ? t('app:cliModal.extraNotes') : ''}
</p>
<div>
<Shell className='tl' title="Shell">
<code className='db'><b className='no-select'>$ </b>{command}</code>
</Shell>
</div>
</ModalBody>

<ModalActions>
<div>
<Button className='ma2 tc' bg='bg-gray' onClick={onLeave}>{t('app:actions.close')}</Button>
</div>
<div className='flex items-center'>
{
command && command === cliCommandList[cliCmdKeys.UPDATE_IPFS_CONFIG]()
? <StrokeDownload onClick={downloadConfig} className='dib fill-link pointer' style={{ height: 38 }}
/> : <div />
}
<Button className='ma2 tc' onClick={() => onClickCopyToClipboard(command)}>
{t('app:actions.copyCommand')}
</Button>
</div>
</ModalActions>
</Modal>
)
}

const CliTutorMode = ({
t, filesPage, isCliTutorModeEnabled, onLeave, isCliTutorModalOpen, command, config, showIcon, doOpenCliTutorModal
}) => {
const downloadConfig = (config) => {
const url = window.URL.createObjectURL(new Blob([config]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.download = 'settings.json'
document.body.appendChild(link)
link.click()
window.URL.revokeObjectURL(url)
}

if (isCliTutorModeEnabled) {
if (filesPage) {
return <CliTutorialModal className='outline-0' onLeave={onLeave} t={t} command={command}/>
}
return (
<Fragment>
{
showIcon
? <StrokeCode onClick={() => doOpenCliTutorModal(true)} className='dib fill-link pointer mh2' style={{ height: 44 }}/>
: <div/>
}
<Overlay show={isCliTutorModalOpen} onLeave={() => doOpenCliTutorModal(false)}>
<CliTutorialModal className='outline-0' onLeave={() => doOpenCliTutorModal(false)} t={t} command={command}
downloadConfig={() => downloadConfig(config)}/>
</Overlay>
</Fragment>
)
}

return null
}

CliTutorialModal.propTypes = {
onLeave: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
command: PropTypes.string.isRequired
}

CliTutorialModal.defaultProps = {
className: ''
}

export default connect(
'doOpenCliTutorModal',
'selectIsCliTutorModalOpen',
'selectIsCliTutorModeEnabled',
CliTutorMode
)
Loading

0 comments on commit b08b14e

Please sign in to comment.