@electerm/electerm-react 3.15.175 → 3.15.186

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.
Files changed (37) hide show
  1. package/client/common/bookmark-schemas.js +2 -0
  2. package/client/common/drop-position.js +59 -0
  3. package/client/components/bookmark-form/bookmark-schema.js +2 -0
  4. package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
  5. package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
  6. package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
  7. package/client/components/bookmark-form/config/serial.js +14 -0
  8. package/client/components/main/term-fullscreen.styl +5 -0
  9. package/client/components/quick-commands/on-drop.js +6 -1
  10. package/client/components/quick-commands/quick-commands-box.jsx +18 -2
  11. package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
  12. package/client/components/quick-commands/quick-commands-list.jsx +18 -2
  13. package/client/components/session/session-control.jsx +331 -0
  14. package/client/components/session/session-control.styl +38 -0
  15. package/client/components/session/session.jsx +41 -246
  16. package/client/components/session/session.styl +1 -0
  17. package/client/components/sftp/address-bookmark-item.jsx +15 -2
  18. package/client/components/sftp/code-compare-utils.js +260 -0
  19. package/client/components/sftp/code-compare.jsx +183 -0
  20. package/client/components/sftp/code-compare.styl +13 -0
  21. package/client/components/sftp/file-compare-modal.jsx +87 -42
  22. package/client/components/shortcuts/shortcut-handler.js +20 -0
  23. package/client/components/sidebar/transport-ui.jsx +21 -14
  24. package/client/components/tabs/index.jsx +18 -3
  25. package/client/components/tabs/tab.jsx +27 -8
  26. package/client/components/tabs/tabs.styl +8 -0
  27. package/client/components/terminal/attach-addon-custom.js +147 -36
  28. package/client/components/terminal/highlight-addon.js +26 -8
  29. package/client/components/terminal/term-search.jsx +1 -1
  30. package/client/components/terminal/terminal.jsx +193 -2
  31. package/client/components/terminal/terminal.styl +0 -2
  32. package/client/components/terminal-info/run-cmd.jsx +1 -1
  33. package/client/components/tree-list/tree-list.jsx +57 -39
  34. package/client/css/basic.styl +14 -0
  35. package/client/css/mobile.styl +31 -9
  36. package/client/store/item.js +10 -6
  37. package/package.json +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  import {
6
6
  Tag
7
7
  } from 'antd'
8
+ import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
8
9
 
9
10
  export default class AddrBookmarkItem extends Component {
10
11
  handleClick = () => {
@@ -31,6 +32,15 @@ export default class AddrBookmarkItem extends Component {
31
32
 
32
33
  handleDragOver = e => {
33
34
  e.preventDefault()
35
+ // only the hovered item should show an indicator
36
+ document.querySelectorAll(
37
+ '.addr-bookmark-item.dnd-before, .addr-bookmark-item.dnd-after'
38
+ ).forEach(el => clearDropIndicator(el))
39
+ setDropIndicator(e.currentTarget, isDropAfterHalf(e, e.currentTarget))
40
+ }
41
+
42
+ handleDragLeave = e => {
43
+ clearDropIndicator(e.currentTarget)
34
44
  }
35
45
 
36
46
  handleDragStart = e => {
@@ -39,13 +49,15 @@ export default class AddrBookmarkItem extends Component {
39
49
 
40
50
  handleDrop = e => {
41
51
  e.preventDefault()
52
+ clearDropIndicator(e.currentTarget)
42
53
  const { store } = window
43
54
  const [host, idDragged] = e.dataTransfer.getData('idDragged').split('#')
44
- const idDrop = e.target.getAttribute('data-id').split('#')[1]
55
+ const idDrop = e.currentTarget.getAttribute('data-id').split('#')[1]
45
56
  const dataName = host
46
57
  ? 'addressBookmarks'
47
58
  : 'addressBookmarksLocal'
48
- store.adjustOrder(dataName, idDragged, idDrop)
59
+ const insertAfter = isDropAfterHalf(e, e.currentTarget)
60
+ store.adjustOrder(dataName, idDragged, idDrop, insertAfter)
49
61
  }
50
62
 
51
63
  render () {
@@ -64,6 +76,7 @@ export default class AddrBookmarkItem extends Component {
64
76
  data-id={id}
65
77
  draggable
66
78
  onDragOver={this.handleDragOver}
79
+ onDragLeave={this.handleDragLeave}
67
80
  onDragStart={this.handleDragStart}
68
81
  onDrop={this.handleDrop}
69
82
  >
@@ -0,0 +1,260 @@
1
+ /**
2
+ * code compare utilities
3
+ * - binary file detection (by extension + content)
4
+ * - line splitting and size limits
5
+ * - file extension to Prism language mapping for syntax highlighting
6
+ */
7
+
8
+ import { getFileExt } from './file-read'
9
+ import { maxEditFileSize } from '../../common/constants'
10
+
11
+ // extensions that are definitely binary and should never be compared as text
12
+ const binaryExtMap = {
13
+ png: 1,
14
+ jpg: 1,
15
+ jpeg: 1,
16
+ gif: 1,
17
+ bmp: 1,
18
+ ico: 1,
19
+ webp: 1,
20
+ tiff: 1,
21
+ tif: 1,
22
+ heic: 1,
23
+ heif: 1,
24
+ avif: 1,
25
+ raw: 1,
26
+ cr2: 1,
27
+ nef: 1,
28
+ arw: 1,
29
+ psd: 1,
30
+ ai: 1,
31
+ eps: 1,
32
+ zip: 1,
33
+ tar: 1,
34
+ gz: 1,
35
+ tgz: 1,
36
+ bz2: 1,
37
+ xz: 1,
38
+ lz: 1,
39
+ lzma: 1,
40
+ '7z': 1,
41
+ rar: 1,
42
+ jar: 1,
43
+ war: 1,
44
+ ear: 1,
45
+ apk: 1,
46
+ aab: 1,
47
+ xpi: 1,
48
+ crx: 1,
49
+ exe: 1,
50
+ dll: 1,
51
+ so: 1,
52
+ dylib: 1,
53
+ bin: 1,
54
+ class: 1,
55
+ o: 1,
56
+ obj: 1,
57
+ a: 1,
58
+ lib: 1,
59
+ pyc: 1,
60
+ pyo: 1,
61
+ node: 1,
62
+ wasm: 1,
63
+ swf: 1,
64
+ pdf: 1,
65
+ doc: 1,
66
+ docx: 1,
67
+ xls: 1,
68
+ xlsx: 1,
69
+ ppt: 1,
70
+ pptx: 1,
71
+ odt: 1,
72
+ ods: 1,
73
+ odp: 1,
74
+ mp3: 1,
75
+ mp4: 1,
76
+ avi: 1,
77
+ mov: 1,
78
+ wmv: 1,
79
+ flv: 1,
80
+ ogg: 1,
81
+ wav: 1,
82
+ aac: 1,
83
+ m4a: 1,
84
+ m4v: 1,
85
+ webm: 1,
86
+ mkv: 1,
87
+ mpg: 1,
88
+ mpeg: 1,
89
+ '3gp': 1,
90
+ opus: 1,
91
+ db: 1,
92
+ sqlite: 1,
93
+ sqlite3: 1,
94
+ mdb: 1,
95
+ accdb: 1,
96
+ iso: 1,
97
+ img: 1,
98
+ dmg: 1,
99
+ vmdk: 1,
100
+ qcow2: 1,
101
+ vdi: 1,
102
+ wim: 1,
103
+ ttf: 1,
104
+ otf: 1,
105
+ woff: 1,
106
+ woff2: 1,
107
+ eot: 1,
108
+ pak: 1,
109
+ dat: 1,
110
+ bson: 1,
111
+ msg: 1,
112
+ eml: 1,
113
+ pdb: 1,
114
+ ax: 1,
115
+ nib: 1,
116
+ mo: 1
117
+ }
118
+
119
+ // map file extensions to Prism / refractor language names
120
+ // only includes languages that react-diff-viewer-continued can highlight
121
+ const extToLangMap = {
122
+ js: 'javascript',
123
+ jsx: 'jsx',
124
+ mjs: 'javascript',
125
+ cjs: 'javascript',
126
+ ts: 'typescript',
127
+ tsx: 'tsx',
128
+ json: 'json',
129
+ html: 'html',
130
+ htm: 'html',
131
+ xml: 'xml',
132
+ svg: 'xml',
133
+ css: 'css',
134
+ scss: 'scss',
135
+ sass: 'sass',
136
+ less: 'less',
137
+ styl: 'stylus',
138
+ py: 'python',
139
+ rb: 'ruby',
140
+ go: 'go',
141
+ rs: 'rust',
142
+ java: 'java',
143
+ kt: 'kotlin',
144
+ c: 'c',
145
+ h: 'c',
146
+ cpp: 'cpp',
147
+ cc: 'cpp',
148
+ cxx: 'cpp',
149
+ hpp: 'cpp',
150
+ cs: 'csharp',
151
+ php: 'php',
152
+ swift: 'swift',
153
+ sh: 'bash',
154
+ bash: 'bash',
155
+ zsh: 'bash',
156
+ fish: 'bash',
157
+ yml: 'yaml',
158
+ yaml: 'yaml',
159
+ toml: 'toml',
160
+ md: 'markdown',
161
+ markdown: 'markdown',
162
+ sql: 'sql',
163
+ graphql: 'graphql',
164
+ gql: 'graphql',
165
+ dockerfile: 'docker',
166
+ vue: 'markup',
167
+ svelte: 'markup',
168
+ lua: 'lua',
169
+ r: 'r',
170
+ rkt: 'racket',
171
+ clj: 'clojure',
172
+ cljs: 'clojure',
173
+ edn: 'clojure',
174
+ scala: 'scala',
175
+ pl: 'perl',
176
+ pm: 'perl',
177
+ dart: 'dart',
178
+ ex: 'elixir',
179
+ exs: 'elixir',
180
+ erl: 'erlang',
181
+ hs: 'haskell',
182
+ nim: 'nim',
183
+ zig: 'zig',
184
+ v: 'verilog',
185
+ vim: 'vim',
186
+ ini: 'properties',
187
+ conf: 'properties',
188
+ cfg: 'properties',
189
+ properties: 'properties'
190
+ }
191
+
192
+ // upper bound of lines to diff, keeps memory usage reasonable
193
+ // react-diff-viewer-continued uses jsdiff's Myers algorithm, which is far more
194
+ // memory-efficient than the previous hand-written LCS DP table, so we can
195
+ // safely support larger files.
196
+ export const maxDiffLines = 10000
197
+
198
+ export function getExt (name = '') {
199
+ return (getFileExt(name).ext || '').toLowerCase()
200
+ }
201
+
202
+ export function isBinaryExt (name = '') {
203
+ return binaryExtMap[getExt(name)] === 1
204
+ }
205
+
206
+ /**
207
+ * Whether the two files can be compared as text/code at all.
208
+ * Only non-directory, non-binary-ext, not-too-large files qualify.
209
+ */
210
+ export function canCodeCompare (file1 = {}, file2 = {}) {
211
+ if (!file1 || !file2) {
212
+ return false
213
+ }
214
+ if (file1.isDirectory || file2.isDirectory) {
215
+ return false
216
+ }
217
+ if (isBinaryExt(file1.name) || isBinaryExt(file2.name)) {
218
+ return false
219
+ }
220
+ if ((file1.size || 0) > maxEditFileSize || (file2.size || 0) > maxEditFileSize) {
221
+ return false
222
+ }
223
+ return true
224
+ }
225
+
226
+ /**
227
+ * Check whether already-decoded text content looks binary (contains null bytes).
228
+ * utf8-decoded binary data usually contains \u0000.
229
+ */
230
+ export function looksBinary (text = '') {
231
+ return text.includes('\u0000')
232
+ }
233
+
234
+ export function splitLines (text = '') {
235
+ if (text === '') {
236
+ return []
237
+ }
238
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
239
+ }
240
+
241
+ /**
242
+ * Map a filename to a Prism/refractor language name
243
+ * for syntax highlighting in the diff viewer.
244
+ * Returns undefined if no mapping is found.
245
+ */
246
+ export function getHighlightLanguage (name = '') {
247
+ const ext = getExt(name)
248
+ if (!ext) {
249
+ // check for special filenames
250
+ const base = (name || '').toLowerCase()
251
+ if (base === 'dockerfile') {
252
+ return 'docker'
253
+ }
254
+ if (base === 'makefile' || base === 'gnumakefile') {
255
+ return 'makefile'
256
+ }
257
+ return undefined
258
+ }
259
+ return extToLangMap[ext]
260
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * code compare component
3
+ * reads both file contents and renders a side-by-side diff
4
+ * using react-diff-viewer-continued (word-level diff + syntax highlight)
5
+ */
6
+
7
+ import { useState, useEffect, useMemo } from 'react'
8
+ import { Spin } from 'antd'
9
+ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
10
+ import resolve from '../../common/resolve'
11
+ import { refs } from '../common/ref'
12
+ import { typeMap } from '../../common/constants'
13
+ import isColorDark from '../../common/is-color-dark'
14
+ import {
15
+ looksBinary,
16
+ isBinaryExt,
17
+ splitLines,
18
+ maxDiffLines,
19
+ getHighlightLanguage
20
+ } from './code-compare-utils'
21
+ import './code-compare.styl'
22
+
23
+ const e = window.translate
24
+
25
+ export default function CodeCompare (props) {
26
+ const { file1, file2, tab = {} } = props
27
+ const [loading, setLoading] = useState(true)
28
+ const [error, setError] = useState('')
29
+ const [text1, setText1] = useState('')
30
+ const [text2, setText2] = useState('')
31
+
32
+ useEffect(() => {
33
+ let cancelled = false
34
+ async function load () {
35
+ setLoading(true)
36
+ setError('')
37
+ setText1('')
38
+ setText2('')
39
+ // check binary by extension before reading
40
+ if (isBinaryExt(file1 && file1.name) || isBinaryExt(file2 && file2.name)) {
41
+ setError('binary file - not supported for code compare')
42
+ setLoading(false)
43
+ return
44
+ }
45
+ try {
46
+ const [t1, t2] = await Promise.all([
47
+ readFileContent(file1, tab),
48
+ readFileContent(file2, tab)
49
+ ])
50
+ if (cancelled) {
51
+ return
52
+ }
53
+ if (looksBinary(t1) || looksBinary(t2)) {
54
+ setError(e('binary file') + ' - ' + e('not supported for code compare'))
55
+ setLoading(false)
56
+ return
57
+ }
58
+ const a = splitLines(t1)
59
+ const b = splitLines(t2)
60
+ if (a.length > maxDiffLines || b.length > maxDiffLines) {
61
+ setError(e('file too large for code compare') + ' (max ' + maxDiffLines + ' ' + e('lines') + ')')
62
+ setLoading(false)
63
+ return
64
+ }
65
+ setText1(t1)
66
+ setText2(t2)
67
+ } catch (err) {
68
+ if (!cancelled) {
69
+ setError(err && err.message ? err.message : String(err))
70
+ }
71
+ }
72
+ if (!cancelled) {
73
+ setLoading(false)
74
+ }
75
+ }
76
+ load()
77
+ return () => {
78
+ cancelled = true
79
+ }
80
+ }, [file1, file2, tab])
81
+
82
+ const { useDarkTheme, styles } = useMemo(() => {
83
+ const themeConf = window.store?.getUiThemeConfig?.() || {}
84
+ const dark = isColorDark(themeConf.main || '#121214')
85
+ const vars = {
86
+ diffViewerBackground: themeConf['main-light'] || (dark ? '#1e1e1e' : '#fff'),
87
+ diffViewerColor: themeConf.text || (dark ? '#ddd' : '#333'),
88
+ gutterBackground: themeConf['main-dark'] || (dark ? '#2a2a2a' : '#f5f5f5'),
89
+ gutterColor: themeConf['text-dark'] || (dark ? '#888' : '#999'),
90
+ addedBackground: themeConf.success || '#06D6A0',
91
+ addedColor: dark ? '#06D6A0' : '#058f6b',
92
+ removedBackground: themeConf.error || '#EF476F',
93
+ removedColor: dark ? '#EF476F' : '#c93659',
94
+ wordAddedBackground: 'rgba(6, 214, 160, 0.25)',
95
+ wordRemovedBackground: 'rgba(239, 71, 111, 0.25)',
96
+ addedGutterBackground: 'rgba(6, 214, 160, 0.15)',
97
+ removedGutterBackground: 'rgba(239, 71, 111, 0.15)',
98
+ codeFoldBackground: themeConf['main-dark'] || (dark ? '#252525' : '#f0f0f0'),
99
+ codeFoldContentColor: themeConf['text-dark'] || (dark ? '#666' : '#bbb'),
100
+ emptyLineBackground: themeConf['main-dark'] || (dark ? '#1a1a1a' : '#fafafa'),
101
+ diffViewerTitleBackground: themeConf['main-dark'] || (dark ? '#2a2a2a' : '#f0f0f0'),
102
+ diffViewerTitleColor: themeConf.text || (dark ? '#ddd' : '#333'),
103
+ diffViewerTitleBorderColor: themeConf.primary || '#08c'
104
+ }
105
+ return {
106
+ useDarkTheme: dark,
107
+ styles: {
108
+ variables: {
109
+ light: vars,
110
+ dark: vars
111
+ },
112
+ diffContainer: {
113
+ fontFamily: "'Maple Mono', 'Menlo', 'Consolas', monospace",
114
+ fontSize: '13px',
115
+ lineHeight: '1.5'
116
+ }
117
+ }
118
+ }
119
+ }, [])
120
+
121
+ const highlightLanguage = useMemo(() => {
122
+ const lang1 = getHighlightLanguage(file1?.name)
123
+ const lang2 = getHighlightLanguage(file2?.name)
124
+ return lang1 === lang2 ? lang1 : undefined
125
+ }, [file1, file2])
126
+
127
+ const name1 = file1 ? file1.name : ''
128
+ const name2 = file2 ? file2.name : ''
129
+ const identical = text1 === text2 && !loading && !error
130
+
131
+ let body = null
132
+ if (error) {
133
+ body = <div className='code-compare-message'>{error}</div>
134
+ } else if (identical) {
135
+ body = <div className='code-compare-message'>{e('files are identical')}</div>
136
+ } else {
137
+ body = (
138
+ <div className='code-compare-body'>
139
+ <ReactDiffViewer
140
+ oldValue={text1}
141
+ newValue={text2}
142
+ splitView
143
+ compareMethod={DiffMethod.WORDS}
144
+ useDarkTheme={useDarkTheme}
145
+ styles={styles}
146
+ leftTitle={name1}
147
+ rightTitle={name2}
148
+ hideLineNumbers={false}
149
+ showDiffOnly
150
+ extraLinesSurroundingDiff={3}
151
+ hideSummary
152
+ codeFoldMessageRenderer={() => <span />}
153
+ disableWorker
154
+ highlightLanguage={highlightLanguage}
155
+ />
156
+ </div>
157
+ )
158
+ }
159
+
160
+ return (
161
+ <div className='code-compare-wrap'>
162
+ <Spin spinning={loading}>
163
+ {body}
164
+ </Spin>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ async function readFileContent (file, tab) {
170
+ if (!file) {
171
+ return ''
172
+ }
173
+ const p = resolve(file.path, file.name)
174
+ if (file.type === typeMap.remote) {
175
+ const sftpEntry = refs.get('sftp-' + tab.id)
176
+ const sftp = sftpEntry && sftpEntry.sftp
177
+ if (!sftp) {
178
+ throw new Error(e('sftp not ready'))
179
+ }
180
+ return sftp.readFile(p)
181
+ }
182
+ return window.fs.readFile(p)
183
+ }
@@ -0,0 +1,13 @@
1
+ .code-compare-wrap
2
+ width 100%
3
+
4
+ .code-compare-body
5
+ max-height 70vh
6
+ overflow auto
7
+ border 1px solid var(--main-darker)
8
+ border-radius 4px
9
+
10
+ .code-compare-message
11
+ padding 24px
12
+ text-align center
13
+ color var(--text-dark)
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * file compare modal - shows side by side comparison of two files' attributes
3
+ * and an optional code/content diff tab for text files
3
4
  */
4
5
 
5
- import React from 'react'
6
+ import React, { lazy, Suspense } from 'react'
7
+ import { Spin, Tabs } from 'antd'
6
8
  import Modal from '../common/modal'
7
9
  import resolve from '../../common/resolve'
8
10
  import time from '../../common/time'
@@ -15,6 +17,15 @@ import './file-compare-modal.styl'
15
17
  const e = window.translate
16
18
  const formatTime = time
17
19
 
20
+ // lazy load the code compare component so it is only bundled when needed
21
+ const CodeCompare = lazy(() => import('./code-compare'))
22
+
23
+ const Loading = () => (
24
+ <div style={{ padding: 40, textAlign: 'center' }}>
25
+ <Spin />
26
+ </div>
27
+ )
28
+
18
29
  export default class FileCompareModal extends React.PureComponent {
19
30
  state = {
20
31
  visible: false,
@@ -22,7 +33,8 @@ export default class FileCompareModal extends React.PureComponent {
22
33
  file2: {},
23
34
  tab: null,
24
35
  uidTree: {},
25
- gidTree: {}
36
+ gidTree: {},
37
+ activeTab: 'info'
26
38
  }
27
39
 
28
40
  componentDidMount () {
@@ -32,7 +44,8 @@ export default class FileCompareModal extends React.PureComponent {
32
44
  showFileCompareModal (data) {
33
45
  this.setState({
34
46
  ...data,
35
- visible: true
47
+ visible: true,
48
+ activeTab: 'info'
36
49
  })
37
50
  }
38
51
 
@@ -40,36 +53,17 @@ export default class FileCompareModal extends React.PureComponent {
40
53
  this.setState({
41
54
  file1: {},
42
55
  file2: {},
43
- visible: false
56
+ visible: false,
57
+ activeTab: 'info'
44
58
  })
45
59
  }
46
60
 
47
- render () {
48
- const {
49
- visible,
50
- file1,
51
- file2,
52
- tab = {},
53
- uidTree = {},
54
- gidTree = {}
55
- } = this.state
56
- if (!visible) {
57
- return null
58
- }
61
+ renderInfo (file1, file2, tab, uidTree, gidTree) {
59
62
  const {
60
63
  host,
61
64
  port,
62
65
  username
63
66
  } = tab
64
- const title = e('compare')
65
- const ps = {
66
- open: visible,
67
- width: 800,
68
- title,
69
- footer: null,
70
- onCancel: this.onClose
71
- }
72
-
73
67
  const fp1 = resolve(file1.path, file1.name)
74
68
  const fp2 = resolve(file2.path, file2.name)
75
69
  const ffp1 = file1.type === 'local'
@@ -111,8 +105,8 @@ export default class FileCompareModal extends React.PureComponent {
111
105
  ],
112
106
  [
113
107
  e('size'),
114
- file1.isDirectory ? '-' : filesize(file1.size || 0),
115
- file2.isDirectory ? '-' : filesize(file2.size || 0)
108
+ file1.isDirectory ? '-' : `${filesize(file1.size || 0)}(${file1.size || 0}byte)`,
109
+ file2.isDirectory ? '-' : `${filesize(file2.size || 0)}(${file2.size || 0}byte)`
116
110
  ],
117
111
  [e('mode'), permission1, permission2],
118
112
  [e('fullPath'), ffp1, ffp2],
@@ -122,23 +116,74 @@ export default class FileCompareModal extends React.PureComponent {
122
116
  [e('modifyTime'), formatTime(file1.modifyTime), formatTime(file2.modifyTime)]
123
117
  ]
124
118
 
119
+ return (
120
+ <table className='file-compare-table'>
121
+ <tbody>
122
+ {rows.map(([label, v1, v2], i) => {
123
+ const same = String(v1 ?? '') === String(v2 ?? '')
124
+ const cls = same ? '' : 'file-compare-diff'
125
+ return (
126
+ <tr key={i}>
127
+ <td className='bold file-compare-label'>{label}</td>
128
+ <td className={cls}>{v1}</td>
129
+ <td className={cls}>{v2}</td>
130
+ </tr>
131
+ )
132
+ })}
133
+ </tbody>
134
+ </table>
135
+ )
136
+ }
137
+
138
+ render () {
139
+ const {
140
+ visible,
141
+ file1,
142
+ file2,
143
+ tab = {},
144
+ uidTree = {},
145
+ gidTree = {},
146
+ activeTab
147
+ } = this.state
148
+ if (!visible) {
149
+ return null
150
+ }
151
+ const title = e('compare')
152
+ const isCodeTab = activeTab === 'code'
153
+ const width = isCodeTab ? '90%' : 800
154
+ const ps = {
155
+ open: visible,
156
+ width,
157
+ title,
158
+ footer: null,
159
+ onCancel: this.onClose
160
+ }
161
+
162
+ const tabItems = [
163
+ {
164
+ key: 'info',
165
+ label: e('info'),
166
+ children: this.renderInfo(file1, file2, tab, uidTree, gidTree)
167
+ },
168
+ {
169
+ key: 'code',
170
+ label: e('code'),
171
+ children: (
172
+ <Suspense fallback={<Loading />}>
173
+ <CodeCompare file1={file1} file2={file2} tab={tab} />
174
+ </Suspense>
175
+ )
176
+ }
177
+ ]
178
+
125
179
  return (
126
180
  <Modal {...ps}>
127
- <table className='file-compare-table'>
128
- <tbody>
129
- {rows.map(([label, v1, v2], i) => {
130
- const same = String(v1 ?? '') === String(v2 ?? '')
131
- const cls = same ? '' : 'file-compare-diff'
132
- return (
133
- <tr key={i}>
134
- <td className='bold file-compare-label'>{label}</td>
135
- <td className={cls}>{v1}</td>
136
- <td className={cls}>{v2}</td>
137
- </tr>
138
- )
139
- })}
140
- </tbody>
141
- </table>
181
+ <Tabs
182
+ activeKey={activeTab}
183
+ onChange={key => this.setState({ activeTab: key })}
184
+ items={tabItems}
185
+ destroyInactiveTabPane={false}
186
+ />
142
187
  </Modal>
143
188
  )
144
189
  }