@electerm/electerm-react 3.12.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  export default (path = '') => {
2
- return path.startsWith('/')
2
+ return path.startsWith('/') || /^[a-zA-Z]:/.test(path) || path.startsWith('\\\\')
3
3
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Ensure remote path always starts with /
3
+ * Windows drive letters like c: become /c:
4
+ * Also fixes mixed separators like /c:\windows → /c:/windows
5
+ * This is needed because SFTP protocol expects paths with leading /
6
+ * @param {String} path
7
+ * @return {String}
8
+ */
9
+ export default function normalizeRemotePath (path) {
10
+ if (!path) return path
11
+ // Fix mixed separators: /c:\windows → /c:/windows
12
+ if (/^\/[a-zA-Z]:\\/.test(path)) {
13
+ return path.replace(/\\/g, '/')
14
+ }
15
+ // Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
16
+ if (/^[a-zA-Z]:/.test(path)) {
17
+ return '/' + path.replace(/\\/g, '/')
18
+ }
19
+ return path
20
+ }
@@ -5,6 +5,13 @@
5
5
  * @return {String}
6
6
  */
7
7
 
8
+ export const isWslPath = (path) => /^\\\\(?:wsl\$|wsl\.localhost)\\/.test(path)
9
+
10
+ export const isWslDistroRoot = (path) => {
11
+ const trimmed = path.replace(/\\$/, '')
12
+ return /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(trimmed)
13
+ }
14
+
8
15
  export default function resolve (basePath, nameOrDot) {
9
16
  const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
10
17
  const isWin = basePath.includes('\\') || nameOrDot.includes('\\') || hasWinDrive(basePath) || hasWinDrive(nameOrDot)
@@ -15,7 +22,13 @@ export default function resolve (basePath, nameOrDot) {
15
22
  if (nameOrDot.startsWith('/')) {
16
23
  return nameOrDot.replace(/\\/g, sep)
17
24
  }
25
+ if (nameOrDot.startsWith('\\\\')) {
26
+ return nameOrDot
27
+ }
18
28
  if (nameOrDot === '..') {
29
+ if (isWslDistroRoot(basePath)) {
30
+ return '/'
31
+ }
19
32
  const baseEndsWithSep = basePath.endsWith(sep)
20
33
  const parts = basePath.split(sep)
21
34
  if (parts.length > 1) {
@@ -27,6 +40,9 @@ export default function resolve (basePath, nameOrDot) {
27
40
  }
28
41
  return '/'
29
42
  }
43
+ if (isWslDistroRoot(basePath) && !basePath.endsWith(sep)) {
44
+ return basePath + sep + nameOrDot
45
+ }
30
46
  const result = basePath.endsWith(sep) ? basePath + nameOrDot : basePath + sep + nameOrDot
31
47
  return isWin && result.length === 3 && result.endsWith(':\\') ? '/' : result
32
48
  }
@@ -8,7 +8,6 @@ import {
8
8
  Tooltip
9
9
  } from 'antd'
10
10
  import {
11
- UserOutlined,
12
11
  CopyOutlined,
13
12
  CloseOutlined,
14
13
  CaretDownOutlined,
@@ -172,7 +171,7 @@ export default function AIChatHistoryItem ({ item }) {
172
171
  <span className='pointer mg1r' onClick={toggleOutput}>
173
172
  {showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
174
173
  </span>
175
- <UserOutlined />: {prompt}
174
+ <span>{prompt}</span>
176
175
  {renderStopButton()}
177
176
  </div>
178
177
  ),
@@ -282,7 +282,7 @@ export default auto(function Index (props) {
282
282
  <InfoModal {...infoModalProps} />
283
283
  <RightSidePanel {...rightPanelProps}>
284
284
  <AIChat {...aiChatProps} />
285
- <TerminalInfo {...terminalInfoProps} />
285
+ <TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
286
286
  </RightSidePanel>
287
287
  <SshConfigLoadNotify {...sshConfigProps} />
288
288
  <LoadSshConfigs
@@ -6,7 +6,8 @@ import {
6
6
  ReloadOutlined,
7
7
  ArrowRightOutlined,
8
8
  LoadingOutlined,
9
- HomeOutlined
9
+ HomeOutlined,
10
+ PlusOutlined
10
11
  } from '@ant-design/icons'
11
12
  import {
12
13
  Input,
@@ -74,7 +75,7 @@ function renderAddonBefore (props, realPath) {
74
75
  )
75
76
  }
76
77
 
77
- function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
78
+ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type, handleUploadFromBrowser) {
78
79
  const handleClick = (e) => {
79
80
  e.stopPropagation()
80
81
  if (!isLoadingRemote) {
@@ -82,9 +83,24 @@ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
82
83
  }
83
84
  }
84
85
  return (
85
- <GoIcon
86
- onClick={handleClick}
87
- />
86
+ <>
87
+ {
88
+ type === typeMap.local && window.et.isWebApp
89
+ ? (
90
+ <PlusOutlined
91
+ className='mg1r'
92
+ onClick={(e) => {
93
+ e.stopPropagation()
94
+ handleUploadFromBrowser()
95
+ }}
96
+ />
97
+ )
98
+ : null
99
+ }
100
+ <GoIcon
101
+ onClick={handleClick}
102
+ />
103
+ </>
88
104
  )
89
105
  }
90
106
 
@@ -160,7 +176,7 @@ export default function AddressBar (props) {
160
176
  onBlur={() => props.onInputBlur(type)}
161
177
  disabled={loadingSftp}
162
178
  suffix={
163
- renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type)
179
+ renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type, props.handleUploadFromBrowser)
164
180
  }
165
181
  />
166
182
  {renderHistory(props, type)}
@@ -14,6 +14,7 @@ import copy from 'json-deep-copy'
14
14
  import { pick, some } from 'lodash-es'
15
15
  import Input from '../common/input-auto-focus'
16
16
  import resolve from '../../common/resolve'
17
+ import normalizeRemotePath from '../../common/normalize-remote-path'
17
18
  import { addClass, removeClass } from '../../common/class'
18
19
  import {
19
20
  mode2permission,
@@ -604,7 +605,10 @@ export default class FileSection extends React.Component {
604
605
  const { type, name, isParent } = file
605
606
  const n = `${type}Path`
606
607
  const path = isParent ? file.path : this.props[n]
607
- const np = resolve(path, name)
608
+ let np = resolve(path, name)
609
+ if (type === typeMap.remote) {
610
+ np = normalizeRemotePath(np)
611
+ }
608
612
  const op = this.props[type + 'Path']
609
613
  this.props.modifier({
610
614
  [n]: np,
@@ -882,6 +886,25 @@ export default class FileSection extends React.Component {
882
886
  window.pre.showItemInFolder(p)
883
887
  }
884
888
 
889
+ downloadFromBrowser = async () => {
890
+ const { path, name, isDirectory } = this.state.file
891
+ const p = resolve(path, name)
892
+ const url = '/api/download?path=' + encodeURIComponent(p)
893
+ const res = await window.fetch(url, {
894
+ headers: {
895
+ token: window.store?.config.tokenElecterm
896
+ }
897
+ })
898
+ const blob = await res.blob()
899
+ const a = document.createElement('a')
900
+ a.href = URL.createObjectURL(blob)
901
+ a.download = isDirectory ? name + '.tar.gz' : name
902
+ document.body.appendChild(a)
903
+ a.click()
904
+ document.body.removeChild(a)
905
+ URL.revokeObjectURL(a.href)
906
+ }
907
+
885
908
  newItem = (isDirectory) => {
886
909
  const { type } = this.state.file
887
910
  const list = copy(this.props[type])
@@ -1060,6 +1083,13 @@ export default class FileSection extends React.Component {
1060
1083
  text: e('showInDefaultFileMananger')
1061
1084
  })
1062
1085
  }
1086
+ if (isLocal && isRealFile && window.et.isWebApp) {
1087
+ res.push({
1088
+ func: 'downloadFromBrowser',
1089
+ icon: 'DownloadOutlined',
1090
+ text: 'Download from browser'
1091
+ })
1092
+ }
1063
1093
  if (showEdit) {
1064
1094
  res.push({
1065
1095
  func: 'editFile',
@@ -7,6 +7,12 @@ import fs from '../../common/fs'
7
7
  import { isWin } from '../../common/constants'
8
8
 
9
9
  export const getFileExt = fileName => {
10
+ if (/^\\\\(?:wsl\$|wsl\.localhost)\\/.test(fileName)) {
11
+ return {
12
+ base: fileName,
13
+ ext: ''
14
+ }
15
+ }
10
16
  const sep = '.'
11
17
  const arr = fileName.split(sep)
12
18
  const len = arr.length
@@ -82,12 +88,15 @@ export const getFolderFromFilePath = (filePath, isRemote) => {
82
88
  const arr = filePath.split(sep)
83
89
  const len = arr.length
84
90
  const isWinDisk = isWin && filePath.endsWith(sep)
85
- const path = isWinDisk
91
+ const isWslRoot = isWin && /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(filePath.replace(/\\$/, ''))
92
+ const path = (isWinDisk || isWslRoot)
86
93
  ? '/'
87
94
  : arr.slice(0, len - 1).join(sep)
88
95
  const name = isWinDisk
89
96
  ? filePath.replace(sep, '')
90
- : arr[len - 1]
97
+ : isWslRoot
98
+ ? filePath
99
+ : arr[len - 1]
91
100
 
92
101
  return {
93
102
  path,
@@ -29,6 +29,7 @@ import fs from '../../common/fs'
29
29
  import ListTable from './list-table-ui'
30
30
  import deepCopy from 'json-deep-copy'
31
31
  import isValidPath from '../../common/is-valid-path'
32
+ import normalizeRemotePath from '../../common/normalize-remote-path'
32
33
  import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
33
34
  import * as owner from './owner-list'
34
35
  import AddressBar from './address-bar'
@@ -292,6 +293,7 @@ export default class Sftp extends Component {
292
293
  if (!path && this.sftp) {
293
294
  path = await this.getPwd(this.props.tab.username)
294
295
  }
296
+ path = normalizeRemotePath(path)
295
297
  } else {
296
298
  path = this.getLocalHome()
297
299
  }
@@ -762,7 +764,7 @@ export default class Sftp extends Component {
762
764
 
763
765
  if (!remotePath) {
764
766
  if (startDirectory) {
765
- remotePath = startDirectory
767
+ remotePath = normalizeRemotePath(startDirectory)
766
768
  } else {
767
769
  remotePath = await this.getPwd(username)
768
770
  }
@@ -970,6 +972,31 @@ export default class Sftp extends Component {
970
972
  })
971
973
  }
972
974
 
975
+ handleUploadFromBrowser = () => {
976
+ const input = document.createElement('input')
977
+ input.type = 'file'
978
+ input.multiple = true
979
+ input.onchange = async () => {
980
+ const files = input.files
981
+ if (!files || !files.length) return
982
+ const { localPath } = this.state
983
+ for (const file of files) {
984
+ const formData = new FormData()
985
+ formData.append('file', file)
986
+ formData.append('path', localPath)
987
+ await window.fetch('/api/upload', {
988
+ method: 'POST',
989
+ body: formData,
990
+ headers: {
991
+ token: window.store?.config.tokenElecterm
992
+ }
993
+ })
994
+ }
995
+ this.localList()
996
+ }
997
+ input.click()
998
+ }
999
+
973
1000
  parsePath = async (type, pth) => {
974
1001
  const reg = /^%([^%]+)%/
975
1002
  if (!reg.test(pth)) {
@@ -995,7 +1022,10 @@ export default class Sftp extends Component {
995
1022
  const n = `${type}Path`
996
1023
  const nt = n + 'Temp'
997
1024
  const oldPath = this.state[type + 'Path']
998
- const np = await this.parsePath(type, this.state[nt])
1025
+ let np = await this.parsePath(type, this.state[nt])
1026
+ if (type === typeMap.remote) {
1027
+ np = normalizeRemotePath(np)
1028
+ }
999
1029
  if (!isValidPath(np)) {
1000
1030
  return notification.warning({
1001
1031
  message: 'path not valid'
@@ -1003,6 +1033,7 @@ export default class Sftp extends Component {
1003
1033
  }
1004
1034
  this.setState({
1005
1035
  [n]: np,
1036
+ [nt]: np,
1006
1037
  [`${type}Keyword`]: ''
1007
1038
  }, () => this[`${type}List`](undefined, undefined, oldPath))
1008
1039
  }
@@ -1010,7 +1041,10 @@ export default class Sftp extends Component {
1010
1041
  goParent = (type) => {
1011
1042
  const n = `${type}Path`
1012
1043
  const p = this.state[n]
1013
- const np = resolve(p, '..')
1044
+ let np = resolve(p, '..')
1045
+ if (type === typeMap.remote) {
1046
+ np = normalizeRemotePath(np)
1047
+ }
1014
1048
  const op = this.state[n]
1015
1049
  if (np !== p) {
1016
1050
  this.setState({
@@ -1218,6 +1252,7 @@ export default class Sftp extends Component {
1218
1252
  const addrProps = {
1219
1253
  host,
1220
1254
  type,
1255
+ handleUploadFromBrowser: this.handleUploadFromBrowser,
1221
1256
  ...pick(
1222
1257
  this,
1223
1258
  [
@@ -24,7 +24,11 @@ import {
24
24
  LockOutlined,
25
25
  ReloadOutlined,
26
26
  FileZipOutlined,
27
- AppstoreOutlined
27
+ AppstoreOutlined,
28
+ SaveOutlined,
29
+ PlayCircleFilled,
30
+ StopOutlined,
31
+ DownloadOutlined
28
32
  } from '@ant-design/icons'
29
33
  import IconHolder from './icon-holder'
30
34
 
@@ -52,5 +56,9 @@ export default {
52
56
  LockOutlined,
53
57
  ReloadOutlined,
54
58
  FileZipOutlined,
55
- AppstoreOutlined
59
+ AppstoreOutlined,
60
+ SaveOutlined,
61
+ PlayCircleFilled,
62
+ StopOutlined,
63
+ DownloadOutlined
56
64
  }
@@ -49,3 +49,12 @@ export function setTerminalLogPath (pid, logPath) {
49
49
  action: 'set-terminal-log-path'
50
50
  })
51
51
  }
52
+
53
+ export function startTerminalLogFile (pid, logFilePath, addTimeStampToTermLog) {
54
+ return fetch({
55
+ pid,
56
+ logFilePath,
57
+ addTimeStampToTermLog,
58
+ action: 'start-terminal-log-file'
59
+ })
60
+ }
@@ -7,6 +7,8 @@ import {
7
7
  Dropdown
8
8
  } from 'antd'
9
9
  import message from '../common/message'
10
+ import { notification } from '../common/notification'
11
+ import ShowItem from '../common/show-item.jsx'
10
12
  import Modal from '../common/modal'
11
13
  import classnames from 'classnames'
12
14
  import './terminal.styl'
@@ -30,10 +32,11 @@ import { XmodemClient } from './xmodem-client.js'
30
32
  import DropFileModal from './drop-file-modal.jsx'
31
33
  import keyControlPressed from '../../common/key-control-pressed.js'
32
34
  import NormalBuffer from './normal-buffer.jsx'
33
- import { createTerm, resizeTerm } from './terminal-apis.js'
35
+ import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
34
36
  import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
35
37
  import { KeywordHighlighterAddon } from './highlight-addon.js'
36
38
  import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
39
+ import { getFolderFromFilePath } from '../sftp/file-read.js'
37
40
  import { CommandTrackerAddon } from './command-tracker-addon.js'
38
41
  import AIIcon from '../icons/ai-icon.jsx'
39
42
  import {
@@ -72,6 +75,9 @@ class Term extends Component {
72
75
  saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
73
76
  addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
74
77
  logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
78
+ logFileName: '',
79
+ recording: false,
80
+ recordingFilePath: '',
75
81
  passType: 'password',
76
82
  lines: [],
77
83
  searchResults: [],
@@ -676,8 +682,101 @@ class Term extends Component {
676
682
  )
677
683
  }
678
684
 
685
+ getTerminalBufferText = () => {
686
+ const { addTimeStampToTermLog } = this.state
687
+ const buffer = this.term.buffer.active
688
+ const len = buffer.length
689
+ const rawLines = []
690
+ for (let i = 0; i < len; i++) {
691
+ const line = buffer.getLine(i)
692
+ rawLines.push(line ? line.translateToString(false) : '')
693
+ }
694
+ // trim trailing blank lines before applying timestamps
695
+ while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
696
+ rawLines.pop()
697
+ }
698
+ if (!addTimeStampToTermLog) {
699
+ return rawLines.join('\n')
700
+ }
701
+ return rawLines.map(text => {
702
+ const now = new Date()
703
+ const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
704
+ return ts + text
705
+ }).join('\n')
706
+ }
707
+
708
+ syncTermInfo = (stateUpdate) => {
709
+ this.setState(stateUpdate)
710
+ const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
711
+ if (Object.keys(infoUpdate).length) {
712
+ refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
713
+ }
714
+ }
715
+
716
+ openLogSaveDialog = async (titleKey) => {
717
+ const { logName } = this.props
718
+ const result = await window.api.saveDialog({
719
+ title: e(titleKey),
720
+ defaultPath: logName + '.log',
721
+ filters: [
722
+ { name: 'Log files', extensions: ['log'] }
723
+ ],
724
+ properties: ['createDirectory', 'showOverwriteConfirmation']
725
+ })
726
+ if (result.canceled || !result.filePath) {
727
+ return null
728
+ }
729
+ return result.filePath
730
+ }
731
+
732
+ onSaveTerminalLog = async () => {
733
+ const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
734
+ if (!filePath) {
735
+ return
736
+ }
737
+ const content = this.getTerminalBufferText()
738
+ await window.fs.writeFile(filePath, content).catch(window.store.onError)
739
+ const { addTimeStampToTermLog } = this.state
740
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
741
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
742
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
743
+ notification.success({
744
+ message: e('saveTerminalLogToFile'),
745
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
746
+ duration: 5
747
+ })
748
+ }
749
+
750
+ onRecord = async () => {
751
+ const filePath = await this.openLogSaveDialog('record')
752
+ if (!filePath) {
753
+ return
754
+ }
755
+ const { addTimeStampToTermLog } = this.state
756
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
757
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
758
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
759
+ this.setState({ recording: true, recordingFilePath: filePath })
760
+ notification.success({
761
+ message: e('record'),
762
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
763
+ duration: 5
764
+ })
765
+ }
766
+
767
+ onStopRecord = () => {
768
+ const { recordingFilePath } = this.state
769
+ toggleTerminalLog(this.pid).catch(window.store.onError)
770
+ this.syncTermInfo({ saveTerminalLogToFile: false })
771
+ this.setState({ recording: false, recordingFilePath: '' })
772
+ notification.success({
773
+ message: e('stopRecord'),
774
+ description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
775
+ })
776
+ }
777
+
679
778
  renderContextMenu = () => {
680
- const { hasSelection } = this.state
779
+ const { hasSelection, recording } = this.state
681
780
  const copyed = true
682
781
  const copyShortcut = this.getShortcut('terminal_copy')
683
782
  const pasteShortcut = this.getShortcut('terminal_paste')
@@ -730,6 +829,16 @@ class Term extends Component {
730
829
  icon: <iconsMap.SearchOutlined />,
731
830
  label: e('search'),
732
831
  extra: searchShortcut
832
+ },
833
+ {
834
+ key: 'onSaveTerminalLog',
835
+ icon: <iconsMap.SaveOutlined />,
836
+ label: e('saveTerminalLogToFile')
837
+ },
838
+ {
839
+ key: recording ? 'onStopRecord' : 'onRecord',
840
+ icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
841
+ label: e(recording ? 'stopRecord' : 'record')
733
842
  }
734
843
  ]
735
844
  if (isSerial) {
@@ -8,7 +8,7 @@ import {
8
8
  Button
9
9
  } from 'antd'
10
10
  import defaults from '../../common/default-setting'
11
- import { toggleTerminalLog, toggleTerminalLogTimestamp, setTerminalLogPath } from '../terminal/terminal-apis'
11
+ import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
12
12
  import {
13
13
  ClockCircleOutlined,
14
14
  BorderlessTableOutlined,
@@ -18,7 +18,9 @@ import {
18
18
  PartitionOutlined
19
19
  } from '@ant-design/icons'
20
20
  import { refs } from '../common/ref'
21
- import LogPathEdit from './log-path-edit'
21
+ import ShowItem from '../common/show-item'
22
+ import { osResolve } from '../../common/resolve'
23
+ import createDefaultLogPath from '../../common/default-log-path'
22
24
 
23
25
  const e = window.translate
24
26
 
@@ -35,15 +37,20 @@ export default class TerminalInfoBase extends Component {
35
37
  state = {
36
38
  saveTerminalLogToFile: false,
37
39
  addTimeStampToTermLog: false,
38
- logPath: ''
40
+ logPath: '',
41
+ logFileName: ''
39
42
  }
40
43
 
41
44
  componentDidMount () {
45
+ const { pid } = this.props
46
+ refs.add('term-info-' + pid, this)
42
47
  this.getState()
43
48
  }
44
49
 
45
50
  componentWillUnmount () {
46
51
  clearTimeout(this.timer)
52
+ const { pid } = this.props
53
+ refs.remove('term-info-' + pid)
47
54
  }
48
55
 
49
56
  handleToggleTimestamp = () => {
@@ -74,17 +81,6 @@ export default class TerminalInfoBase extends Component {
74
81
  })
75
82
  }
76
83
 
77
- onLogPathChange = (v) => {
78
- const { pid } = this.props
79
- setTerminalLogPath(pid, v)
80
- refs.get('term-' + pid)?.setState({
81
- logPath: v
82
- })
83
- this.setState({
84
- logPath: v
85
- })
86
- }
87
-
88
84
  handleToggle = () => {
89
85
  const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
90
86
  const {
@@ -112,7 +108,8 @@ export default class TerminalInfoBase extends Component {
112
108
  this.setState({
113
109
  saveTerminalLogToFile: term.state.saveTerminalLogToFile,
114
110
  addTimeStampToTermLog: term.state.addTimeStampToTermLog,
115
- logPath: term.state.logPath
111
+ logPath: term.state.logPath,
112
+ logFileName: term.state.logFileName || ''
116
113
  })
117
114
  } else {
118
115
  this.timer = setTimeout(this.getState, 100)
@@ -166,39 +163,45 @@ export default class TerminalInfoBase extends Component {
166
163
  render () {
167
164
  const {
168
165
  id,
169
- logName,
170
- pid
166
+ logName
171
167
  } = this.props
172
- const { saveTerminalLogToFile, logPath } = this.state
168
+ const { saveTerminalLogToFile, logPath, logFileName } = this.state
173
169
  const name = e('saveTerminalLogToFile')
170
+ const base = logPath || createDefaultLogPath()
171
+ const fileName = logFileName || (logName + '.log')
172
+ const fullPath = osResolve(base, fileName)
174
173
  return (
175
174
  <div className='terminal-info-section terminal-info-base'>
176
- <div className='fix'>
177
- <span className='fleft'><b>ID:</b> {id}</span>
178
- <span className='fright'>
179
- <Switch
180
- checkedChildren={name}
181
- unCheckedChildren={name}
182
- checked={saveTerminalLogToFile}
183
- onChange={this.handleToggle}
184
- className='mg1r mg1b'
185
- />
186
- {
187
- this.renderTimestamp()
188
- }
189
- </span>
175
+ <div className='pd1b'>
176
+ <b>ID:</b> {id}
177
+ </div>
178
+ <div className='pd1b'>
179
+ <Switch
180
+ checkedChildren={name}
181
+ unCheckedChildren={name}
182
+ checked={saveTerminalLogToFile}
183
+ onChange={this.handleToggle}
184
+ className='mg1r mg1b'
185
+ />
186
+ {
187
+ this.renderTimestamp()
188
+ }
190
189
  </div>
190
+ {
191
+ saveTerminalLogToFile
192
+ ? (
193
+ <div className='pd1b font-xs color-grey'>
194
+ {e('terminalLogPath')}: {fullPath} <ShowItem to={fullPath} />
195
+ </div>
196
+ )
197
+ : null
198
+ }
191
199
  <div className='pd2y'>
192
200
  {
193
201
  this.renderInfoSelection()
194
202
  }
195
203
  </div>
196
- <LogPathEdit
197
- pid={pid}
198
- logPath={logPath}
199
- logName={logName}
200
- setLogPath={this.onLogPathChange}
201
- />
204
+
202
205
  </div>
203
206
  )
204
207
  }
@@ -12,10 +12,11 @@ import { osResolve } from '../../common/resolve'
12
12
 
13
13
  const e = window.translate
14
14
 
15
- export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
15
+ export default function LogPathEdit ({ pid, logPath, logName, logFileName, setLogPath }) {
16
16
  const defaultPath = createDefaultLogPath()
17
17
  const base = logPath || defaultPath
18
- const fullPath = osResolve(base, logName + '.log')
18
+ const fileName = logFileName || (logName + '.log')
19
+ const fullPath = osResolve(base, fileName)
19
20
 
20
21
  const testAndSet = async (v) => {
21
22
  if (v) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.12.0",
3
+ "version": "3.15.0",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",