Skip to content

Commit 6d3e319

Browse files
robinfehrrmevans9
authored andcommitted
feat(timeline): Timeline export
* timeline export * change icon place within gui * cleanup * Move file to new location
1 parent 2b981fa commit 6d3e319

File tree

6 files changed

+216
-5
lines changed

6 files changed

+216
-5
lines changed

App/Dialogs/ExportTimelineDialog.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React from "react"
2+
import Modal from "react-modal"
3+
import { inject, observer } from "mobx-react"
4+
import AppStyles from "../Theme/AppStyles"
5+
import Colors from "../Theme/Colors"
6+
import Checkbox from "../Shared/Checkbox"
7+
import Keystroke from "../Lib/Keystroke"
8+
9+
import { remote } from 'electron'
10+
11+
12+
const FIELD_LABEL = "File path"
13+
const INPUT_PLACEHOLDER = "e.g. /Documents/timeline.log"
14+
15+
const ESCAPE_HINT = "Close"
16+
const ESCAPE_KEYSTROKE = "ESC"
17+
const ENTER_KEYSTROKE = `${Keystroke.modifierName} + Enter`
18+
const ENTER_HINT = "Save File"
19+
const BUTTON_TEXT = "Choose Path"
20+
const DIALOG_TITLE = "Export Timeline"
21+
22+
const Styles = {
23+
error: {
24+
color: Colors.tag,
25+
padding: "1em 2em 0em"
26+
},
27+
success: {
28+
color: Colors.string,
29+
padding: "1em 2em 0em"
30+
},
31+
link: {
32+
alignItems: 'center',
33+
justifyContent: 'center',
34+
textAlign: 'center',
35+
padding: 20,
36+
cursor: 'pointer',
37+
backgroundColor: Colors.chrome,
38+
borderRadius: 8,
39+
width: 110,
40+
border: `1px solid ${Colors.chromeLine}`,
41+
color: Colors.foregroundLight
42+
},
43+
path: {
44+
marginLeft: 20,
45+
color: Colors.foregroundLight,
46+
alignSelf: "center",
47+
fontSize: "9pt"
48+
},
49+
body: {
50+
...AppStyles.Modal.body,
51+
flexDirection: "row",
52+
padding: "1em 2em 1em"
53+
},
54+
keyStrokes: {
55+
...AppStyles.Modal.keystrokes,
56+
marginTop: 40
57+
}
58+
}
59+
60+
@inject("session")
61+
@observer
62+
class ExportTimelineDialog extends React.Component {
63+
constructor(props) {
64+
super()
65+
const { session } = props
66+
const { ui } = session
67+
ui.writingFileError = ""
68+
ui.writingFileSuccess = false
69+
this.path = ""
70+
}
71+
72+
render() {
73+
const { session } = this.props
74+
const { ui } = session
75+
76+
return (
77+
<Modal
78+
isOpen={ui.showTimelineExportDialog}
79+
onRequestClose={ui.closeExportTimelineDialog}
80+
onAfterOpen={this.onAfterOpenModal}
81+
style={{
82+
content: AppStyles.Modal.content,
83+
overlay: AppStyles.Modal.overlay,
84+
}}
85+
>
86+
<div style={AppStyles.Modal.container}>
87+
<div style={AppStyles.Modal.header}>
88+
89+
<h1 style={AppStyles.Modal.title}>{DIALOG_TITLE}</h1>
90+
<div>
91+
<p>The filters you applied to the timeline will be applied for the export too.</p>
92+
</div>
93+
</div>
94+
<div style={Styles.body}>
95+
<button style={Styles.link} onClick={() => {
96+
const path = remote.dialog.showOpenDialog({properties: ['openDirectory']})
97+
ui.setExportFilePath(path[0]);
98+
}}>
99+
{BUTTON_TEXT}
100+
</button>
101+
<p style={Styles.path}>{ui.exportFilePath}</p>
102+
</div>
103+
{ ui.writingFileError !== "" ?
104+
<p style={Styles.error}>The file could not be exported. {ui.writingFileError}</p>
105+
: null
106+
}
107+
{ ui.writingFileSuccess ?
108+
<p style={Styles.success}>The file was exported.</p>
109+
: null
110+
}
111+
<div style={Styles.keyStrokes}>
112+
<div style={AppStyles.Modal.hotkey}>
113+
<span style={AppStyles.Modal.keystroke}>{ESCAPE_KEYSTROKE}</span> {ESCAPE_HINT}
114+
</div>
115+
<div style={AppStyles.Modal.hotkey} onClick={() => session.exportCommands(this.path) }>
116+
<span style={AppStyles.Modal.keystroke}>{ENTER_KEYSTROKE}</span> {ENTER_HINT}
117+
</div>
118+
</div>
119+
</div>
120+
</Modal>
121+
)
122+
}
123+
}
124+
125+
export default ExportTimelineDialog

App/Foundation/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Provider, observer } from "mobx-react"
22
import React, { Component } from "react"
33
import { ipcRenderer } from "electron"
44
import FilterTimelineDialog from "../Dialogs/FilterTimelineDialog"
5+
import ExportTimelineDialog from "../Dialogs/ExportTimelineDialog"
56
import RenameStateDialog from "../Dialogs/RenameStateDialog"
67
import SendCustomDialog from "../Dialogs/SendCustomDialog"
78
import StateDispatchDialog from "../Dialogs/StateDispatchDialog"
@@ -88,6 +89,7 @@ export default class App extends Component {
8889
<StateWatchDialog />
8990
<RenameStateDialog />
9091
<FilterTimelineDialog />
92+
<ExportTimelineDialog />
9193
<SendCustomDialog />
9294
</div>
9395
</Provider>

App/Lib/commands.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { action, observable } from "mobx"
2-
import { pipe, reject } from "ramda"
1+
import { action, observable, computed } from "mobx"
2+
import { pipe, reject, contains, propEq } from "ramda"
33
import { dotPath } from "ramdasauce"
44

55
export const MAX_COMMANDS = 500
@@ -19,8 +19,9 @@ class Commands {
1919
/**
2020
* Constructor with an optional overrideable max list size.
2121
*/
22-
constructor() {
22+
constructor(session) {
2323
setTimeout(() => this.flush(), FLUSH_TIME)
24+
this.session = session
2425
}
2526

2627
/**
@@ -55,6 +56,29 @@ class Commands {
5556
this.all.clear()
5657
this.all.push(...newCommands)
5758
}
59+
60+
@computed
61+
get commands() {
62+
const isSubscription = propEq("type", "state.values.change")
63+
const isSubscriptionCommandWithEmptyChanges = command => {
64+
return isSubscription(command) && dotPath("payload.changes.length", command) === 0
65+
}
66+
67+
const result = pipe(
68+
() => this.all,
69+
reject(this.rejectCommandsFromOtherConnections),
70+
reject(isSubscriptionCommandWithEmptyChanges),
71+
reject(command => contains(command.type, this.session.commandsHiddenInTimeline)),
72+
reject(this.session.rejectCommandWhenSearching)
73+
)(this);
74+
return result;
75+
}
76+
77+
rejectCommandsFromOtherConnections = command => {
78+
if (!this.session.selectedConnection) return false
79+
80+
return this.session.selectedConnection.clientId !== command.clientId
81+
}
5882
}
5983

6084
export default Commands

App/Stores/SessionStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ class Session {
249249
this.server.on("disconnect", this.handleConnectionsChange)
250250

251251
this.stateBackupStore = new StateBackupStore(this.server)
252-
this.ui = new UiStore(this.server, this.commandsManager, this.stateBackupStore, this.getSelectedConnection)
252+
this.ui = new UiStore(this, this.server, this.commandsManager, this.stateBackupStore, this.getSelectedConnection)
253253

254254
// hide or show the watch panel depending if we have watches
255255
reaction(

App/Stores/UiStore.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { trim } from "ramda"
44
import { isNilOrEmpty } from "ramdasauce"
55
import Keystroke from "../Lib/Keystroke"
66
import Mousetrap from "../Lib/Mousetrap.min.js"
7+
import fs from "fs";
78

89
/**
910
* Handles UI state.
@@ -19,6 +20,7 @@ class UI {
1920
@observable showHelpDialog = false
2021
@observable showStateWatchDialog = false
2122
@observable showFilterTimelineDialog = false
23+
@observable showTimelineExportDialog = false
2224
@observable statusBarExpanded = false
2325
@observable watchToAdd
2426
@observable actionToDispatch
@@ -30,13 +32,17 @@ class UI {
3032
@observable isSidebarVisible = true
3133
@observable isStorybookShown = false
3234
@observable searchPhrase = ""
35+
@observable exportFilePath = ""
36+
@observable writingFileError = ""
37+
@observable writingFileSuccess = false
3338
zoomLevel = 0
3439

3540
// additional properties that some commands may want... a way to communicate
3641
// from the command toolbar to the command
3742
commandProperties = {}
3843

39-
constructor(server, commandsManager, stateBackupStore, getSelectedConnection) {
44+
constructor(session, server, commandsManager, stateBackupStore, getSelectedConnection) {
45+
this.session = session
4046
this.server = server
4147
this.commandsManager = commandsManager
4248
this.stateBackupStore = stateBackupStore
@@ -47,6 +53,7 @@ class UI {
4753
Mousetrap.bind(`${Keystroke.mousetrap}+backspace`, this.reset)
4854
Mousetrap.bind(`${Keystroke.mousetrap}+k`, this.openStateFindDialog)
4955
Mousetrap.bind(`${Keystroke.mousetrap}+shift+f`, this.openFilterTimelineDialog)
56+
Mousetrap.bind(`${Keystroke.mousetrap}+shift+e`, this.openExportTimelineDialog)
5057
Mousetrap.bind(`${Keystroke.mousetrap}+d`, this.openStateDispatchDialog)
5158
Mousetrap.bind(`${Keystroke.mousetrap}+s`, () => this.stateBackupStore.sendBackup())
5259
Mousetrap.bind(`tab`, this.toggleKeysValues)
@@ -188,6 +195,7 @@ class UI {
188195
this.showStateFindDialog ||
189196
this.showStateDispatchDialog ||
190197
this.showFilterTimelineDialog ||
198+
this.showTimelineExportDialog ||
191199
this.stateBackupStore.renameDialogVisible ||
192200
this.showSendCustomDialog
193201
)
@@ -219,6 +227,9 @@ class UI {
219227
if (this.showStateDispatchDialog) {
220228
this.submitStateDispatch()
221229
}
230+
if (this.showTimelineExportDialog) {
231+
this.exportCommands();
232+
}
222233
}
223234

224235
@action
@@ -323,6 +334,24 @@ class UI {
323334
this.showFilterTimelineDialog = false
324335
}
325336

337+
@action
338+
openExportTimelineDialog = () => {
339+
this.showTimelineExportDialog = true
340+
}
341+
342+
@action
343+
closeExportTimelineDialog = () => {
344+
this.showTimelineExportDialog = false
345+
this.exportFilePath = ""
346+
this.writingFileError = ""
347+
this.writingFileSuccess = false
348+
}
349+
350+
@action
351+
setExportFilePath = (path) => {
352+
this.exportFilePath = path;
353+
}
354+
326355
@action
327356
openSendCustomDialog = () => {
328357
this.showSendCustomDialog = true
@@ -469,6 +498,31 @@ class UI {
469498
this.server.send("storybook", this.isStorybookShown, clientId)
470499
}
471500

501+
exportCommands = () => {
502+
503+
if (!this.exportFilePath) {
504+
this.writingFileError = "Please choose a path."
505+
return
506+
}
507+
const formatCommand = (command) => ({
508+
clientId: command.clientId,
509+
payload: {
510+
name: command.payload.name,
511+
action: command.payload.action
512+
},
513+
type: command.type
514+
})
515+
const commands = JSON.stringify(this.session.commands.map(formatCommand))
516+
fs.writeFile(this.exportFilePath + "/reactotron_timeline.txt", commands, (err) => {
517+
if (err) {
518+
console.log(err)
519+
this.writingFileError = err.message
520+
} else {
521+
this.writingFileSuccess = true;
522+
this.writingFileError = ""
523+
}
524+
})
525+
}
472526
}
473527

474528
export default UI

App/Timeline/TimelineHeader.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { inject, observer } from "mobx-react"
33
import React, { Component } from "react"
44
import IconClear from "react-icons/lib/md/delete-sweep"
55
import IconFilter from "react-icons/lib/md/filter-list"
6+
import IconDownload from "react-icons/lib/md/file-download"
67
import IconSearch from "react-icons/lib/md/search"
78
import IconReverseOrder from "react-icons/lib/md/swap-vert"
89
import AppStyles from "../Theme/AppStyles"
@@ -138,6 +139,11 @@ class TimelineHeader extends Component {
138139
style={Styles.reverseOrderIcon}
139140
onClick={ui.toggleTimelineOrder}
140141
/>
142+
<IconDownload
143+
size={Styles.iconSize}
144+
style={Styles.toolbarFilter}
145+
onClick={ui.openExportTimelineDialog}
146+
/>
141147
<IconClear size={Styles.iconSize} style={Styles.toolbarClear} onClick={ui.reset} />
142148
</div>
143149
</div>

0 commit comments

Comments
 (0)