index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. <template>
  2. <a-spin :spinning="loading">
  3. <page-header :title="title" />
  4. <page-body>
  5. <steps class="my-3" v-model="step" />
  6. <div class="step1" v-show="isSetpOne">
  7. <a-form :form="endpointForm" ref="endpointFormRef" v-bind="formItemLayout">
  8. <a-form-item :label="$t('dictionary.domain')" key="domain" v-show="showDomainSelect">
  9. <base-select
  10. v-model="filters.project_domain"
  11. v-if="showDomainSelect"
  12. :isDefaultSelect="true"
  13. resource="domains"
  14. :params="renderOrders.project_domain.params"
  15. filterable
  16. version="v1"
  17. @change="handleDomainChange"
  18. :select-props="{ placeholder: `${$t('common.text00106')}${$t('dictionary.domain')}` }" />
  19. </a-form-item>
  20. <a-form-item :label="$t('common.name')">
  21. <a-input v-decorator="decorators.generate_name" :placeholder="$t('common.placeholder')" @change="handleNameChange" />
  22. <name-repeated v-slot:extra res="proxy_endpoints" :name="generate_name" />
  23. </a-form-item>
  24. <a-form-item :label="$t('common.description')">
  25. <a-textarea :auto-size="{ minRows: 1, maxRows: 3 }" v-decorator="decorators.description" :placeholder="$t('common_367')" />
  26. </a-form-item>
  27. <area-selects
  28. class="mb-0"
  29. ref="areaSelects"
  30. @change="handleAreaChange"
  31. :allowClear="false"
  32. :wrapperCol="formItemLayout.wrapperCol"
  33. :labelCol="formItemLayout.labelCol"
  34. :names="areaselectsName"
  35. :cloudregionParams="renderOrders.region.params"
  36. :providerParams="renderOrders.brand.params"
  37. :cloudregionParamsMapper="cloudregionParamsMapper"
  38. filterBrandResource="compute_engine" />
  39. <a-form-item :label="$t('cloudenv.text_7')">
  40. <a-row :gutter="9">
  41. <a-col :span="12">
  42. <base-select
  43. resource="vpcs"
  44. filterable
  45. need-params
  46. v-model="filters.vpc"
  47. ref="vpcSelects"
  48. @change="handleVpcChange"
  49. class="vpc-selector"
  50. :isDefaultSelect="true"
  51. :params="renderOrders.vpc.params"
  52. :labelFormat="vpcLabelFormat"
  53. :select-props="{ placeholder: $t('network.text_274') }" />
  54. </a-col>
  55. <a-col :span="12">
  56. <base-select
  57. resource="networks"
  58. need-params
  59. filterable
  60. v-model="filters.network"
  61. ref="networkSelects"
  62. :style="{'--network-title': networkTitle}"
  63. class="network-selector"
  64. @change="handleNetworkChange"
  65. :isDefaultSelect="false"
  66. :params="renderOrders.network.params"
  67. :labelFormat="networkLabelFormat"
  68. :select-props="{ placeholder: $t('common_227') }" />
  69. </a-col>
  70. </a-row>
  71. </a-form-item>
  72. <a-form-item :label="$t('network.text_226')">
  73. <div>
  74. <search-box :options="options" v-model="searchValue" @input="search" />
  75. <detect-ssh-table :params="params" :key="table1Key" :showRadioSelect="true" @radio-change="handleRadioChange" :maxColumns="3" :remote="false" style="padding-top: 15px;" />
  76. <a-input v-decorator="decorators.server_id" v-show="false" />
  77. <div v-if="showDocsLink()">{{$t('compute.text_196')}}<help-link href="/vminstance">{{$t('compute.perform_create')}}</help-link>,{{$t('network.ssh-proxy.endpoints.create.vminstance.tips')}}<help-link :href="vmConfigurationLink">{{$t('network.ssh-proxy.endpoints.create.vminstance.tips.link')}}</help-link></div>
  78. </div>
  79. </a-form-item>
  80. </a-form>
  81. </div>
  82. <div class="step2" v-if="!isSetpOne">
  83. <detect-ssh-table :params="t2Params" :key="table2Key" :ansibleTasks="ansibleTasks" :remote="true" :maxColumns="3" @onDetecting="handleDetectStatusChange" />
  84. <a-alert :message="alertMessage" :type="alertType" v-show="alertType" />
  85. <setup-ssh-form ref="setupForm" :servers="servers" @tasks="handleSetupSSHTasks" v-show="showSetupForm" />
  86. </div>
  87. </page-body>
  88. <page-footer>
  89. <template v-slot:right>
  90. <a-button size="large" :loading="loading || detecting" @click="lastStep" v-show="!isSetpOne">{{ $t('scope.text_107') }}</a-button>
  91. <a-button class="ml-2" type="primary" size="large" :loading="loading || detecting" @click="handleSubmit">{{ isSetpOne ? $t('scope.text_108') : $t('common.ok') }}</a-button>
  92. <a-button class="ml-2" size="large" @click="handleCancel">{{ $t('dialog.cancel') }}</a-button>
  93. </template>
  94. </page-footer>
  95. </a-spin>
  96. </template>
  97. <script>
  98. import _ from 'lodash'
  99. import { DetectSshTable } from '@Compute/views/vminstance/dialogs/DetectSSH'
  100. import SetupSshForm from '@Compute/views/vminstance/create/form/SetupSSHForm'
  101. import AreaSelects from '@/sections/AreaSelects'
  102. import NameRepeated from '@/sections/NameRepeated'
  103. import {
  104. getStatusTableColumn,
  105. getIpsTableColumn,
  106. getNameDescriptionTableColumn,
  107. } from '@/utils/common/tableColumn'
  108. import { getNameFilter } from '@/utils/common/tableFilter'
  109. import ListMixin from '@/mixins/list'
  110. import { DOCS_MAP, showDocsLink } from '@/constants/docs'
  111. export default {
  112. name: 'SshProxyCreateForm',
  113. components: {
  114. DetectSshTable,
  115. SetupSshForm,
  116. AreaSelects,
  117. NameRepeated,
  118. },
  119. mixins: [ListMixin],
  120. data () {
  121. let domain = ''
  122. let showDomainSelect = true
  123. const serverlist = this.$list.createList(this, { id: 'ssh-endpoint-create-form', resource: 'servers' })
  124. if (this.$store.getters.scope === 'system') {
  125. domain = 'default'
  126. } else {
  127. showDomainSelect = false
  128. domain = this.$store.getters.userInfo.projectDomain || ''
  129. }
  130. const columns = [
  131. getNameDescriptionTableColumn({
  132. onManager: new this.$Manager('servers'),
  133. hideField: true,
  134. addBackup: true,
  135. editDesc: false,
  136. edit: false,
  137. slotCallback: row => {
  138. return (
  139. <side-page-trigger>{ row.name }</side-page-trigger>
  140. )
  141. },
  142. }),
  143. getStatusTableColumn({
  144. minWidth: 130,
  145. statusModule: 'server',
  146. slotCallback: row => {
  147. return [
  148. <div class='d-flex align-items-center text-truncate'>
  149. <status status={ row.status } statusModule='server' />
  150. </div>,
  151. ]
  152. },
  153. }),
  154. getIpsTableColumn({ field: 'ip', title: 'IP', vm: this }),
  155. ]
  156. this.handleNameChange = _.debounce(this.handleNameChange, 500)
  157. return {
  158. showDocsLink,
  159. loading: false,
  160. detecting: false,
  161. title: this.$t('network.ssh-proxy.endpoints.create'),
  162. networkTitle: JSON.stringify(`${this.$t('network.text_565')}: `),
  163. generate_name: '',
  164. step: {
  165. steps: [
  166. { title: this.$t('network.ssh-proxy.endpoint.create.step1'), key: 'select-endpoint' },
  167. { title: this.$t('network.ssh-proxy.endpoint.create.step2'), key: 'create-ssh-endpoint' },
  168. ],
  169. currentStep: 0,
  170. },
  171. endpointForm: this.$form.createForm(this.$refs.endpointFormRef),
  172. decorators: {
  173. generate_name: [
  174. 'generate_name',
  175. {
  176. rules: [
  177. { required: true, message: this.$t('network.text_116') },
  178. { validator: this.$validate('resourceName') },
  179. ],
  180. },
  181. ],
  182. description: ['description'],
  183. server_id: [
  184. 'server_id',
  185. {
  186. rules: [
  187. { required: true, message: this.$t('network.text_60') },
  188. ],
  189. },
  190. ],
  191. },
  192. servers: [],
  193. searchValue: {},
  194. options: {
  195. name: getNameFilter(),
  196. ip_addr: {
  197. label: 'IP',
  198. },
  199. },
  200. ansibleTasks: {},
  201. areaselectsName: ['provider', 'cloudregion'],
  202. showSetupForm: false,
  203. sshableStatus: '',
  204. alertMessage: '',
  205. alertType: '',
  206. table1Key: 't1',
  207. table2Key: 't2',
  208. t2Params: {
  209. onManager: serverlist.onManager,
  210. data: [],
  211. columns: columns,
  212. },
  213. params: {
  214. onManager: serverlist.onManager,
  215. data: [],
  216. columns: columns,
  217. },
  218. showDomainSelect: showDomainSelect,
  219. filters: {
  220. project_domain: domain,
  221. vpc: '',
  222. network: '',
  223. region: '',
  224. brand: '',
  225. },
  226. currentIndex: 1,
  227. renderOrders: {
  228. project_domain: { order: 1, next: 'brand', params: { scope: this.$store.getters.scope } },
  229. brand: { order: 2, next: 'region', params: { scope: this.$store.getters.scope, project_domain: domain } },
  230. region: { order: 3, next: 'vpc', params: {} },
  231. vpc: { order: 4, next: 'network', params: {} },
  232. network: { order: 5, next: 'server', params: {}, optional: true },
  233. server: { order: 6, params: {} },
  234. },
  235. formItemLayout: {
  236. wrapperCol: {
  237. span: 22,
  238. },
  239. labelCol: {
  240. span: 2,
  241. },
  242. },
  243. }
  244. },
  245. computed: {
  246. isSetpOne () {
  247. return this.step.currentStep === 0
  248. },
  249. vmConfigurationLink () {
  250. const lang = this.$store.getters.setting.language
  251. return DOCS_MAP.sshProxyVmConfiguration(lang)
  252. },
  253. },
  254. created () {
  255. if (!this.showDomainSelect) {
  256. this.handleDomainChange(this.filters.project_domain)
  257. }
  258. },
  259. methods: {
  260. _params (base, key, value) {
  261. const ret = Object.assign({}, base)
  262. if (value) {
  263. ret[key] = value
  264. } else {
  265. if (key) delete ret[key]
  266. }
  267. return ret
  268. },
  269. async fetchServers () {
  270. if (this.waitRender('server')) return
  271. const extraParams = {
  272. limit: 50,
  273. }
  274. if (this.searchValue.name) extraParams.filter = `name.contains('${this.searchValue.name.join(',')}')`
  275. if (this.searchValue.ip_addr) extraParams.ip_addr = this.searchValue.ip_addr.join(',')
  276. const params = Object.assign({}, this.renderOrders.server.params, extraParams)
  277. try {
  278. const ret = await new this.$Manager('servers').list({ params })
  279. this.params.data = ret.data.data
  280. this.$nextTick(() => {
  281. this.table1Key += 1
  282. })
  283. } catch (e) {
  284. throw e
  285. }
  286. },
  287. setNextRender (key, params) {
  288. this.currentIndex = this.renderOrders[key].order
  289. this.renderOrders[key].params = params
  290. // for (const item in this.renderOrders) {
  291. // if (this.renderOrders[item].order > this.currentIndex) {
  292. // this.renderOrders[item].params = {}
  293. // }
  294. // }
  295. },
  296. waitRender (key) {
  297. return this.currentIndex < this.renderOrders[key].order
  298. },
  299. handleDomainChange (e) {
  300. if (this.filters.domain !== e) {
  301. this.filters.domain = e
  302. if (e) {
  303. this.setNextRender('brand', this._params(this.renderOrders.project_domain.params, 'project_domain', e))
  304. this.$refs.areaSelects.fetchs(this.areaselectsName)
  305. }
  306. }
  307. },
  308. handleAreaChange (e) {
  309. if (e.hasOwnProperty('cloudregion')) {
  310. this.filters.region = e.cloudregion ? e.cloudregion.id : ''
  311. this.setNextRender('vpc', this._params(this.renderOrders.region.params, 'region', this.filters.region))
  312. }
  313. if (e.hasOwnProperty('provider')) {
  314. this.filters.brand = e.provider ? e.provider.id : ''
  315. this.setNextRender('region', this._params(this.renderOrders.brand.params, 'brand', this.filters.brand))
  316. }
  317. },
  318. handleVpcChange (e) {
  319. this.filters.vpc = e
  320. if (e || Object.keys(this.$refs.vpcSelects.resOpts).length === 0) {
  321. this.setNextRender('network', this._params(this.renderOrders.vpc.params, 'vpc', this.filters.vpc))
  322. }
  323. },
  324. handleNetworkChange (e) {
  325. this.filters.network = e
  326. if (e || Object.keys(this.$refs.networkSelects.resOpts).length === 0 || this.renderOrders.network.optional) {
  327. this.setNextRender('server', this._params(this.renderOrders.network.params, 'network', this.filters.network))
  328. this.fetchServers()
  329. }
  330. },
  331. search (e) {
  332. this.fetchServers()
  333. },
  334. vpcLabelFormat (item) {
  335. if (item.manager) {
  336. if (item.cidr_block) {
  337. return <div> { item.name }<span>({ item.cidr_block })</span><span class="ml-2 text-color-secondary">{this.$t('common_711')}: { item.manager }</span></div>
  338. }
  339. return <div> { item.name }<span class="ml-2 text-color-secondary">{this.$t('common_711')}: { item.manager }</span></div>
  340. }
  341. return <div>{ item.name }</div>
  342. },
  343. networkLabelFormat (item) {
  344. /* <span className="text-color-secondary option-prefix"></span> */
  345. return <div> { item.name } ({ item.guest_ip_start } - { item.guest_ip_end })</div>
  346. },
  347. handleRadioChange (row) {
  348. if (row) {
  349. this.t2Params.data = [row]
  350. this.servers = [row.id]
  351. this.endpointForm.setFieldsValue({ server_id: row.id })
  352. } else {
  353. this.t2Params.data = []
  354. this.servers = []
  355. this.endpointForm.setFieldsValue({ server_id: undefined })
  356. }
  357. this.table2Key += 1
  358. },
  359. handleSetupSSHTasks (tasks) {
  360. this.ansibleTasks = tasks
  361. this.table2Key += 1
  362. },
  363. handleNameChange (e) {
  364. this.generate_name = e.target.value
  365. },
  366. lastStep () {
  367. this.sshableStatus = ''
  368. this.showSetupForm = false
  369. this.step.currentStep = 0
  370. },
  371. async handleDetectStatusChange (e) {
  372. this.detecting = e
  373. if (this.sshableStatus || !this.detecting) {
  374. const newStatus = this.t2Params.data[0] ? this.t2Params.data[0].sshable_status : ''
  375. // 如果上次已经检测过状态,并且本次新状态是available,则可以直接创建endpoints
  376. if (this.sshableStatus && newStatus === 'available') {
  377. try {
  378. await this.submitEndpoint()
  379. } catch (e) {
  380. throw e
  381. }
  382. } else {
  383. if (newStatus !== 'available') {
  384. this.showSetupForm = true
  385. this.alertType = 'warning'
  386. this.alertMessage = this.$t('network.ssh-proxy.endpoint.create.step2.tips_failed')
  387. } else {
  388. this.alertType = 'success'
  389. this.showSetupForm = false
  390. this.alertMessage = this.$t('network.ssh-proxy.endpoint.create.step2.tips_success')
  391. }
  392. this.sshableStatus = newStatus
  393. }
  394. }
  395. },
  396. cloudregionParamsMapper (params) {
  397. const ret = { ...params }
  398. if (ret.provider === 'OneCloud') {
  399. delete ret.capability
  400. }
  401. return ret
  402. },
  403. handleCancel () {
  404. this.$router.push('/ssh-proxy')
  405. },
  406. validateForm (form) {
  407. return new Promise((resolve, reject) => {
  408. form.validateFieldsAndScroll({ scroll: { alignWithTop: true, offsetTop: 100 } }, (err, values) => {
  409. if (!err) {
  410. resolve(values)
  411. } else {
  412. reject(err)
  413. }
  414. })
  415. })
  416. },
  417. async submitEndpoint () {
  418. const values = await this.validateForm(this.endpointForm)
  419. await new this.$Manager('proxy_endpoints').performClassAction({ action: 'create-from-server', data: values })
  420. this.handleCancel()
  421. },
  422. async submitNextStep () {
  423. await this.validateForm(this.endpointForm)
  424. this.step.currentStep = 1
  425. },
  426. async submitMakeSshable () {
  427. await this.$refs.setupForm.submit()
  428. },
  429. async handleSubmit (e) {
  430. this.loading = true
  431. try {
  432. if (this.isSetpOne) {
  433. await this.submitNextStep()
  434. } else {
  435. if (this.showSetupForm) {
  436. await this.submitMakeSshable()
  437. }
  438. if (this.sshableStatus === 'available') {
  439. await this.submitEndpoint()
  440. }
  441. }
  442. } catch (err) {
  443. throw err
  444. } finally {
  445. this.loading = false
  446. }
  447. },
  448. },
  449. }
  450. </script>
  451. <style scoped>
  452. .vpc-selector .ant-select-selection-selected-value div:before {
  453. content: 'VPC:';
  454. color: rgba(0, 0, 0, 0.45);
  455. }
  456. .network-selector .ant-select-selection-selected-value div:before {
  457. content: var(--network-title);
  458. color: rgba(0, 0, 0, 0.45);
  459. }
  460. </style>