WizardScripts.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /*
  2. Copyright (C) 2019 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled, { css } from 'styled-components'
  17. import InfoIcon from '@src/components/ui/InfoIcon'
  18. import { Close as InputClose } from '@src/components/ui/TextInput'
  19. import { Image as InstanceImage } from '@src/components/modules/WizardModule/WizardInstances'
  20. import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
  21. import { ThemePalette, ThemeProps } from '@src/components/Theme'
  22. import FileUtils from '@src/utils/FileUtils'
  23. import type { Instance, InstanceScript } from '@src/@types/Instance'
  24. import { UserScriptData } from '@src/@types/MainItem'
  25. import DomUtils from '@src/utils/DomUtils'
  26. import scriptItemImage from './images/script-item.svg'
  27. const Wrapper = styled.div<any>`
  28. width: 100%;
  29. display: flex;
  30. overflow: auto;
  31. flex-direction: column;
  32. min-height: 0;
  33. `
  34. const Group = styled.div<any>`
  35. display: flex;
  36. flex-direction: column;
  37. width: 100%;
  38. margin-bottom: 32px;
  39. &:last-child {
  40. margin-bottom: 0;
  41. }
  42. `
  43. const Heading = styled.div<any>`
  44. margin-bottom: 16px;
  45. font-size: ${props => (props.layout === 'modal' ? '16px' : '24px')};
  46. font-weight: ${props => (props.layout === 'modal' ? ThemeProps.fontWeights.medium : ThemeProps.fontWeights.light)};
  47. display: flex;
  48. `
  49. const InfoIconStyled = styled(InfoIcon)<any>`
  50. margin-top: ${props => (props.layout === 'modal' ? '1px' : '5px')};
  51. margin-left: 8px;
  52. `
  53. const Scripts = styled.div<any>`
  54. width: 100%;
  55. display: flex;
  56. flex-direction: column;
  57. `
  58. const Script = styled.div<any>`
  59. width: 100%;
  60. display: flex;
  61. justify-content: space-between;
  62. align-items: center;
  63. flex-shrink: 0;
  64. border-top: 1px solid ${ThemePalette.grayscale[1]};
  65. padding: 8px 0;
  66. &:last-child {
  67. border-bottom: 1px solid ${ThemePalette.grayscale[1]};
  68. }
  69. `
  70. const Name = styled.div<any>`
  71. display: flex;
  72. align-items: center;
  73. `
  74. const OsImage = styled.div<any>`
  75. ${ThemeProps.exactSize('48px')}
  76. background: url('${scriptItemImage}') center no-repeat;
  77. `
  78. const NameLabel = styled.div<any>`
  79. display: flex;
  80. flex-direction: column;
  81. margin-left: 16px;
  82. `
  83. const NameLabelTitle = styled.div<any>`
  84. font-size: 16px;
  85. word-break: break-word;
  86. `
  87. const NameLabelSubtitle = styled.div<any>`
  88. font-size: 12px;
  89. color: ${ThemePalette.grayscale[5]};
  90. margin-top: 1px;
  91. word-break: break-word;
  92. `
  93. const LinkButton = styled.div<any>`
  94. color: ${ThemePalette.primary};
  95. flex-shrink: 0;
  96. margin: 0 8px 0 16px;
  97. cursor: pointer;
  98. :hover {
  99. text-decoration: underline;
  100. }
  101. `
  102. const UploadedScript = styled.div<any>`
  103. display: flex;
  104. position: relative;
  105. `
  106. const UploadedScriptFileName = styled.div<any>`
  107. max-width: 124px;
  108. text-overflow: ellipsis;
  109. overflow: hidden;
  110. margin-right: 32px;
  111. white-space: nowrap;
  112. `
  113. const InputCloseStyled = styled(InputClose)`
  114. top: 0px;
  115. `
  116. const FakeFileInput = styled.input`
  117. position: absolute;
  118. opacity: 0;
  119. top: -99999px;
  120. `
  121. const ScriptDataActions = styled.div`
  122. display: flex;
  123. margin-left: -8px;
  124. margin-top: 8px;
  125. > div {
  126. margin-left: 8px;
  127. }
  128. `
  129. const ScriptDataAction = styled.div<{ red?: boolean, disabled?: boolean }>`
  130. color: ${props => (props.red ? ThemePalette.alert : ThemePalette.primary)};
  131. cursor: pointer;
  132. ${props => (props.disabled ? css`
  133. opacity: 0.6;
  134. cursor: default;
  135. ` : '')}
  136. font-size: 12px;
  137. `
  138. type Props = {
  139. instances: Instance[],
  140. uploadedScripts: InstanceScript[],
  141. removedScripts: InstanceScript[],
  142. layout?: 'modal' | 'page',
  143. loadingInstances?: boolean,
  144. userScriptData: UserScriptData | null | undefined
  145. style?: React.CSSProperties
  146. onScriptUpload: (instanceScript: InstanceScript) => void,
  147. onCancelScript: (global: 'windows' | 'linux' | null, instanceName: string | null) => void,
  148. onScrollableRef?: (ref: HTMLElement) => void,
  149. scrollableRef?: (r: HTMLElement) => void
  150. onScriptDataRemove: (script: InstanceScript) => void
  151. }
  152. type FileInputRefs = {
  153. [prop: string]: {
  154. inputRef: HTMLInputElement,
  155. }
  156. }
  157. @observer
  158. class WizardScripts extends React.Component<Props> {
  159. fileInputRefs: FileInputRefs = {}
  160. async handleFileUpload(
  161. files: FileList | null,
  162. global: 'windows' | 'linux' | null,
  163. instanceId: string | null,
  164. ) {
  165. if (!files || !files.length) {
  166. return
  167. }
  168. const fileName = files[0].name
  169. const scriptContent = await FileUtils.readTextFromFirstFile(files)
  170. this.props.onScriptUpload({
  171. instanceId,
  172. global,
  173. fileName,
  174. scriptContent: scriptContent || '',
  175. })
  176. }
  177. handleScriptDataDownload(scriptData: string, fileName: string) {
  178. DomUtils.download(scriptData, fileName)
  179. }
  180. renderScriptItem(opts: {
  181. global?: 'windows' | 'linux',
  182. instanceId?: string,
  183. title: string,
  184. subtitle?: string,
  185. }) {
  186. const {
  187. global, instanceId, title, subtitle,
  188. } = opts
  189. const uploadedScript = this.props.uploadedScripts.find(
  190. s => (s.instanceId
  191. ? s.instanceId === instanceId : s.global ? s.global === global : false),
  192. )
  193. let scriptData: string | null | undefined = null
  194. if (global) {
  195. scriptData = this.props.userScriptData?.global?.[global]
  196. } else if (instanceId) {
  197. scriptData = this.props.userScriptData?.instances?.[instanceId]
  198. }
  199. const isRemoved: boolean = Boolean(this.props.removedScripts
  200. .find(s => (global ? s.global === global : s.instanceId === instanceId)))
  201. return (
  202. <Script key={title}>
  203. <Name>
  204. {global ? <OsImage /> : <InstanceImage />}
  205. <NameLabel>
  206. <NameLabelTitle>{title}</NameLabelTitle>
  207. {subtitle ? <NameLabelSubtitle>{subtitle}</NameLabelSubtitle> : null}
  208. {scriptData ? (
  209. <ScriptDataActions>
  210. <ScriptDataAction
  211. title="Downloads the currently uploaded script"
  212. onClick={() => {
  213. this.handleScriptDataDownload(scriptData as string, title.toLowerCase().replaceAll(' ', '_'))
  214. }}
  215. >Download
  216. </ScriptDataAction>
  217. <ScriptDataAction
  218. title={isRemoved ? 'The currently uploaded script will be removed' : 'Removes the currently uploaded script'}
  219. red
  220. disabled={isRemoved}
  221. onClick={() => {
  222. if (isRemoved) {
  223. return
  224. }
  225. this.props.onScriptDataRemove({
  226. global, instanceId, scriptContent: null, fileName: null,
  227. })
  228. }}
  229. >{isRemoved ? 'To be removed' : 'Remove'}
  230. </ScriptDataAction>
  231. </ScriptDataActions>
  232. ) : null}
  233. </NameLabel>
  234. </Name>
  235. {uploadedScript ? (
  236. <UploadedScript>
  237. <UploadedScriptFileName
  238. title={uploadedScript.fileName}
  239. >{uploadedScript.fileName}
  240. </UploadedScriptFileName>
  241. <InputCloseStyled
  242. show
  243. onClick={() => {
  244. this.props.onCancelScript(global || null, instanceId || null)
  245. const ref = this.fileInputRefs[title]
  246. if (ref) {
  247. ref.inputRef.value = ''
  248. }
  249. }}
  250. />
  251. </UploadedScript>
  252. )
  253. : (
  254. <LinkButton
  255. onClick={() => {
  256. const ref = this.fileInputRefs[title]
  257. if (ref) {
  258. ref.inputRef.click()
  259. }
  260. }}
  261. >Choose File...
  262. </LinkButton>
  263. )}
  264. <FakeFileInput
  265. type="file"
  266. ref={(r: HTMLInputElement) => { this.fileInputRefs[title] = { inputRef: r } }}
  267. onChange={e => { this.handleFileUpload(e.target.files, global || null, instanceId || null) }}
  268. />
  269. </Script>
  270. )
  271. }
  272. renderScriptGroup(group: 'global' | 'instance') {
  273. if (group === 'global') {
  274. return (
  275. <Group>
  276. <Heading
  277. layout={this.props.layout}
  278. >
  279. Global Scripts
  280. <InfoIconStyled
  281. layout={this.props.layout}
  282. text="Specify user scripts that will run during OS morphing for a particular OS type"
  283. />
  284. </Heading>
  285. <Scripts>
  286. {this.renderScriptItem({ global: 'windows', title: 'Windows Script File' })}
  287. {this.renderScriptItem({ global: 'linux', title: 'Linux Script File' })}
  288. </Scripts>
  289. </Group>
  290. )
  291. }
  292. if (this.props.instances.length === 0 && !this.props.loadingInstances) {
  293. return null
  294. }
  295. return (
  296. <Group layout={this.props.layout}>
  297. <Heading
  298. layout={this.props.layout}
  299. >
  300. Instance Scripts
  301. {!this.props.loadingInstances ? (
  302. <InfoIconStyled
  303. layout={this.props.layout}
  304. text="Specify user scripts that will run during OS morphing for a particular instance. These override the uploaded global scripts."
  305. />
  306. ) : null}
  307. {this.props.loadingInstances ? (
  308. <StatusIcon style={{ marginTop: '1px', marginLeft: '8px' }} status="RUNNING" />
  309. ) : null}
  310. </Heading>
  311. <Scripts>
  312. {this.props.instances.map(instance => {
  313. const id = instance.instance_name || instance.id
  314. const title = instance.name
  315. const osLabel = instance.os_type ? instance.os_type === 'windows' ? 'Windows' : instance.os_type === 'linux' ? 'Linux' : instance.os_type : ''
  316. const osType = osLabel ? `${osLabel} OS | ` : ''
  317. const subtitle = `${osType}${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM`
  318. return this.renderScriptItem({ instanceId: id, title, subtitle })
  319. })}
  320. </Scripts>
  321. </Group>
  322. )
  323. }
  324. render() {
  325. return (
  326. <Wrapper
  327. style={this.props.style}
  328. ref={(r: HTMLElement) => {
  329. if (this.props.onScrollableRef) {
  330. this.props.onScrollableRef(r)
  331. }
  332. }}
  333. >
  334. {this.renderScriptGroup('global')}
  335. {this.renderScriptGroup('instance')}
  336. </Wrapper>
  337. )
  338. }
  339. }
  340. export default WizardScripts