Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - CLI Tutor Mode #1572

Merged
merged 29 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0531b14
[WIP] Feature - CLI Tutor Mode
jay-dee7 Aug 1, 2020
a6c350a
Update src/components/cli-tutor-mode/CliTutorMode.js
jay-dee7 Aug 4, 2020
3553611
Update src/components/cli-tutor-mode/CliTutorMode.js
jay-dee7 Aug 4, 2020
c8b4034
Align fix: cli tutor modal buttons
jay-dee7 Aug 4, 2020
da31e1c
Fix: added compatibility with ApiAddressForm component
jay-dee7 Aug 9, 2020
97a06b0
Fix: added bundle-reactx pattern and en translation keys
jay-dee7 Aug 11, 2020
7d018c3
i18n text change
jessicaschilling Aug 12, 2020
942bfaf
Standardize i18n and visual style on Settings page
jessicaschilling Aug 12, 2020
8d59332
Text tweak; replace CliTutorMode.css with tachyons equivalents
jessicaschilling Aug 12, 2020
7b7f361
Left-justify shell content in modals for consistency
jessicaschilling Aug 12, 2020
c9d76b0
Tidy visual presentation of modals
jessicaschilling Aug 12, 2020
77e7661
Size tweak for icon on Files page
jessicaschilling Aug 12, 2020
61b4c25
Use StrokeCode instead of CopyIcon, adjust text accordingly
jessicaschilling Aug 13, 2020
1b20af7
Lighten icon on settings page explainer text
jessicaschilling Aug 13, 2020
d6d707e
Margin consistency
jessicaschilling Aug 13, 2020
6507448
Update src/bundles/files/consts.js
jay-dee7 Aug 14, 2020
c291643
Move lh-copy so doesn't fubar checkbox styling
jessicaschilling Aug 14, 2020
3ee8432
Fix: Close modal on copy to clipboard
jay-dee7 Aug 15, 2020
15d6578
Fix: rename file command
jay-dee7 Aug 15, 2020
ce41d30
Merge branch 'jay-dee7/cli-tutor-mode' of github.com:jay-dee7/ipfs-we…
jay-dee7 Aug 15, 2020
b761d7d
Fix: delete file by filepath and added type information for cliComman…
jay-dee7 Aug 20, 2020
9821bc8
fix: whitespace in ipfs files rm
lidel Aug 21, 2020
7d71641
fix: remove UPDATE_API_SERVER_ADDRESS
lidel Aug 21, 2020
72a7ab7
fix: eslint
lidel Aug 21, 2020
8cc6d85
fix: remove angle brackets from i18n strings
lidel Aug 21, 2020
881bcbf
Move cliModal i18n keys into app.json
jessicaschilling Aug 21, 2020
84b6cf4
Move copyCommand & relevant close i18n keys into app.json
jessicaschilling Aug 21, 2020
5641184
fix: include files cp step
lidel Aug 26, 2020
3a337de
refactor: make it easier to support path
lidel Aug 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 6 additions & 2 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"save": "Save",
"pin": "Pin",
"unpin": "Unpin",
"unselectAll": "Unselect all"
"unselectAll": "Unselect all",
"copyCommand": "Copy"
},
"shareModal": {
"title": "Share files",
Expand Down Expand Up @@ -120,5 +121,8 @@
"more": "More",
"files": "Files",
"cidNotFileNorDir": "The current link isn't a file, nor a directory. Try to <0>inspect</0> it instead.",
"sortBy": "Sort items by {name}"
"sortBy": "Sort items by {name}",
"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."
}
}
10 changes: 9 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,13 @@
"title": "Peers table",
"paragraph1": "Check the IDs of the connected peers, their address and approximate location."
}
},
"actions": {
"edit": "Change",
"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."
}
}
12 changes: 11 additions & 1 deletion public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
"language": "Language",
"analytics": "Analytics",
"api": "API Address",
"cliTutorMode": "CLI Tutor Mode",
"config": "IPFS Config",
"languageModal": {
"title": "Change language",
"description": "Pick your preferred language.",
"translationProjectIntro": "Add or improve translations and make IPFS better for everyone!",
"translationProjectLink": "Join the IPFS Translation Project"
},
"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."
},
"actions": {
"edit": "Change",
"close": "Close"
"close": "Close",
"copyCommand": "Copy"
},
"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 +69,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
7 changes: 7 additions & 0 deletions public/locales/en/welcome.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,12 @@
"paragraph2": "If you aren't connected to the IPFS API, this page also appears in place of some other pages, with hints for how to get connected.",
"paragraph3": "You can visit this page from anywhere in the app by clicking the IPFS cube logo in the navigation bar."
}
},
"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."
}
}
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
43 changes: 43 additions & 0 deletions src/bundles/files/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,46 @@ 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>"`
},
[cliCmdKeys.ADD_FILE]: () => 'ipfs add <file-name>',
[cliCmdKeys.ADD_DIRECTORY]: () => 'ipfs add -r <folder-name>',
[cliCmdKeys.CREATE_NEW_DIRECTORY]: () => 'ipfs files mkdir <folder-name>',
[cliCmdKeys.FROM_IPFS]: () => 'ipfs cp <content-path-or-cid> <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('cliModal.description')}
</p>
<p className='charcoal-muted w-90 center'>
{ command && command === cliCommandList[cliCmdKeys.UPDATE_IPFS_CONFIG]() ? t('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('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('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