List.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <page-list
  3. :fixed="true"
  4. show-tag-filter
  5. show-tag-columns
  6. show-tag-columns2
  7. show-tag-config
  8. :list="list"
  9. :columns="templateListColumns || columns"
  10. :group-actions="groupActions"
  11. :single-actions="singleActions"
  12. :showSearchbox="showSearchbox"
  13. :showGroupActions="showGroupActions"
  14. :export-data-options="exportDataOptions"
  15. :defaultSearchKey="defaultSearchKey"
  16. :refresh-method="handleListRefresh"
  17. :tag-config-params="tagConfigParams"
  18. :tableOverviewIndexs="tableOverviewIndexs"
  19. :show-single-actions="!isTemplate"
  20. :show-page="!isTemplate" />
  21. </template>
  22. <script>
  23. import { mapGetters } from 'vuex'
  24. import ListMixin from '@/mixins/list'
  25. import ResTemplateListMixin from '@/mixins/resTemplateList'
  26. import {
  27. getNameFilter,
  28. getBrandFilter,
  29. getStatusFilter,
  30. getDomainFilter,
  31. getTenantFilter,
  32. getHostFilter,
  33. getVpcFilter,
  34. getOsArchFilter,
  35. getRegionFilter,
  36. getZoneFilter,
  37. getDescriptionFilter,
  38. getCreatedAtFilter,
  39. } from '@/utils/common/tableFilter'
  40. import { getIpsTableColumn } from '@/utils/common/tableColumn'
  41. import { disableDeleteAction } from '@/utils/common/tableActions'
  42. import expectStatus from '@/constants/expectStatus'
  43. import WindowsMixin from '@/mixins/windows'
  44. import { typeClouds } from '@/utils/common/hypervisor'
  45. import GlobalSearchMixin from '@/mixins/globalSearch'
  46. import regexp from '@/utils/regexp'
  47. import { Manager } from '@/utils/manager'
  48. import SingleActionsMixin from '../mixins/singleActions'
  49. import ColumnsMixin from '../mixins/columns'
  50. export default {
  51. name: 'VmContainerInstanceList',
  52. mixins: [WindowsMixin, ListMixin, GlobalSearchMixin, ColumnsMixin, SingleActionsMixin, ResTemplateListMixin],
  53. props: {
  54. id: String,
  55. getParams: {
  56. type: Object,
  57. default: () => ({}),
  58. },
  59. cloudEnv: String,
  60. cloudEnvOptions: {
  61. type: Array,
  62. },
  63. hiddenFilterOptions: {
  64. type: Array,
  65. default: () => ([]),
  66. },
  67. tableOverviewIndexs: {
  68. type: Array,
  69. },
  70. hiddenActions: {
  71. type: Array,
  72. default: () => ([]),
  73. },
  74. },
  75. data () {
  76. const filter = {}
  77. if (this.$route.query.id) {
  78. filter.id = [this.$route.query.id]
  79. }
  80. if (this.$route.query.status) {
  81. filter.status = this.$route.query.status
  82. }
  83. const filterOptions = {
  84. external_id: {
  85. label: this.$t('table.title.external_id'),
  86. },
  87. id: {
  88. label: this.$t('table.title.id'),
  89. },
  90. name: getNameFilter(),
  91. description: getDescriptionFilter(),
  92. brand: getBrandFilter('compute_engine_brands'),
  93. ip_addr: {
  94. label: 'IP',
  95. hiddenField: 'ips',
  96. },
  97. status: getStatusFilter('server'),
  98. power_states: getStatusFilter({ title: this.$t('compute.power_states'), statusModule: 'server', field: 'power_states' }),
  99. projects: getTenantFilter(),
  100. project_domains: getDomainFilter(),
  101. billing_type: {
  102. label: this.$t('table.title.bill_type'),
  103. dropdown: true,
  104. items: [
  105. { label: this.$t('billingType.postpaid'), key: 'postpaid' },
  106. { label: this.$t('billingType.prepaid'), key: 'prepaid' },
  107. ],
  108. },
  109. host: getHostFilter(),
  110. region: getRegionFilter(),
  111. zone_ids: getZoneFilter(),
  112. vpc: getVpcFilter(),
  113. os_arch: getOsArchFilter(),
  114. vcpu_count: {
  115. label: 'CPU',
  116. },
  117. created_at: getCreatedAtFilter(),
  118. }
  119. this.hiddenFilterOptions.forEach(key => {
  120. delete filterOptions[key]
  121. })
  122. return {
  123. list: this.$list.createList(this, {
  124. ctx: this,
  125. id: this.id,
  126. resource: 'servers',
  127. getParams: this.getParam,
  128. isTemplate: this.isTemplate,
  129. templateLimit: this.templateLimit,
  130. steadyStatus: {
  131. status: Object.values(expectStatus.container).flat(),
  132. },
  133. filter,
  134. filterOptions,
  135. responseData: this.responseData,
  136. hiddenColumns: ['is_gpu', 'metadata', 'instance_type', 'os_type', 'vpc', 'host', 'account', 'created_at', 'macs', 'os_arch', 'vcpu_count', 'vmem_size', 'disk', 'power_states'],
  137. autoHiddenFilterKey: 'server_hidden_columns',
  138. }),
  139. groupActions: [
  140. // 新建
  141. {
  142. label: this.$t('compute.perform_create'),
  143. action: () => {
  144. this.$openNewWindowForMenuHook('vminstance_configured_callback_address.create_callback_address', () => {
  145. this.$router.push({
  146. path: '/vminstance-container/create',
  147. query: {
  148. type: this.cloudEnv === 'onpremise' ? 'idc' : this.cloudEnv || 'idc',
  149. },
  150. })
  151. })
  152. },
  153. meta: () => {
  154. const ret = {
  155. buttonType: 'primary',
  156. validate: !this.cloudEnvEmpty,
  157. tooltip: this.cloudEnvEmpty ? this.$t('common.no_platform_available') : '',
  158. }
  159. if (ret.validate && !this.hasContainerHost) {
  160. ret.validate = false
  161. ret.tooltip = this.$t('compute.no_enabled_container_host')
  162. }
  163. return ret
  164. },
  165. },
  166. // 开机
  167. {
  168. label: this.$t('compute.text_272'),
  169. action: () => {
  170. const ids = this.list.selectedItems.map(item => item.id)
  171. this.list.onManager('batchPerformAction', {
  172. steadyStatus: 'running',
  173. id: ids,
  174. managerArgs: {
  175. action: 'start',
  176. },
  177. })
  178. },
  179. meta: () => {
  180. const ret = { validate: true, tooltip: null }
  181. if (this.list.selectedItems.length === 0) {
  182. ret.validate = false
  183. return ret
  184. }
  185. const isStatusOk = this.list.selectedItems.every(item => ['ready'].includes(item.status))
  186. if (!isStatusOk) {
  187. ret.validate = false
  188. return ret
  189. }
  190. return ret
  191. },
  192. },
  193. // 批量关机
  194. {
  195. label: this.$t('compute.text_273'),
  196. action: () => {
  197. this.createDialog('VmShutDownDialog', {
  198. data: this.list.selectedItems,
  199. columns: this.columns,
  200. onManager: this.onManager,
  201. })
  202. },
  203. meta: () => {
  204. const ret = { validate: true, tooltip: null }
  205. if (this.list.selectedItems.length === 0) {
  206. ret.validate = false
  207. return ret
  208. }
  209. const isStatusOk = this.list.selectedItems.every(item => ['running'].includes(item.status))
  210. if (!isStatusOk) {
  211. ret.validate = false
  212. return ret
  213. }
  214. return ret
  215. },
  216. },
  217. // 重启
  218. {
  219. label: this.$t('compute.text_274'),
  220. action: () => {
  221. this.createDialog('VmContainerRestartDialog', {
  222. data: this.list.selectedItems,
  223. columns: this.columns,
  224. onManager: this.onManager,
  225. })
  226. },
  227. meta: () => {
  228. const ret = { validate: true, tooltip: null }
  229. if (this.list.selectedItems.length === 0) {
  230. ret.validate = false
  231. return ret
  232. }
  233. const isStatusOk = this.list.selectedItems.every(item => ['running'].includes(item.status))
  234. if (!isStatusOk) {
  235. ret.validate = false
  236. return ret
  237. }
  238. return ret
  239. },
  240. },
  241. // 同步状态
  242. {
  243. label: this.$t('compute.perform_sync_status'),
  244. action: () => {
  245. this.onManager('batchPerformAction', {
  246. steadyStatus: ['running', 'ready'],
  247. managerArgs: {
  248. action: 'syncstatus',
  249. },
  250. })
  251. },
  252. meta: () => {
  253. return {
  254. validate: this.list.selectedItems.length > 0,
  255. }
  256. },
  257. },
  258. /* 批量操作 */
  259. {
  260. label: this.$t('compute.text_275'),
  261. actions: () => {
  262. return [
  263. // 更改项目
  264. {
  265. label: this.$t('compute.perform_change_owner', [this.$t('dictionary.project')]),
  266. action: (obj) => {
  267. this.createDialog('ChangeOwenrDialog', {
  268. data: this.list.selectedItems,
  269. columns: this.columns,
  270. onManager: this.onManager,
  271. refresh: this.refresh,
  272. resource: 'servers',
  273. })
  274. },
  275. },
  276. // 到期释放
  277. {
  278. label: this.$t('compute.text_1132'),
  279. action: () => {
  280. this.createDialog('SetDurationDialog', {
  281. data: this.list.selectedItems,
  282. columns: this.columns,
  283. onManager: this.onManager,
  284. refresh: this.refresh,
  285. name: this.$t('compute.vminstance-container'),
  286. alert: this.$t('compute.repo.helper.set_duration.alert'),
  287. })
  288. },
  289. meta: () => {
  290. const ret = { validate: true }
  291. // 包年包月机器,不支持此操作
  292. const isSomePrepaid = this.list.selectedItems.some((item) => {
  293. return item.billing_type === 'prepaid'
  294. })
  295. if (isSomePrepaid) {
  296. ret.validate = false
  297. ret.tooltip = this.$t('compute.text_285')
  298. return ret
  299. }
  300. // 暂只支持同时操作已设置到期或未设置到期释放的机器
  301. const isSomeExpired = this.list.selectedItems.some((item) => {
  302. return item.release_at
  303. })
  304. const isSomeNotExpired = this.list.selectedItems.some((item) => {
  305. return !item.release_at
  306. })
  307. if (isSomeExpired && isSomeNotExpired) {
  308. ret.validate = false
  309. ret.tooltip = this.$t('compute.text_1133')
  310. return ret
  311. }
  312. return ret
  313. },
  314. },
  315. // 编辑标签
  316. {
  317. label: this.$t('compute.text_283'),
  318. action: () => {
  319. this.createDialog('SetTagDialog', {
  320. data: this.list.selectedItems,
  321. columns: this.columns,
  322. onManager: this.onManager,
  323. params: {
  324. resources: 'server',
  325. },
  326. mode: 'add',
  327. })
  328. },
  329. meta: () => {
  330. const ret = { validate: true }
  331. return ret
  332. },
  333. },
  334. // 推送配置
  335. {
  336. label: this.$t('compute.sync_config'),
  337. permission: 'server_perform_sync_config',
  338. action: () => {
  339. this.createDialog('VmSyncConfigDialog', {
  340. data: this.list.selectedItems,
  341. columns: this.columns,
  342. onManager: this.onManager,
  343. })
  344. },
  345. meta: () => {
  346. const ret = {
  347. validate: true,
  348. tooltip: null,
  349. }
  350. const isAllRunningReady = this.list.selectedItems.every(item => (item.status === 'running' || item.status === 'ready'))
  351. if (!isAllRunningReady) {
  352. ret.validate = false
  353. ret.tooltip = this.$t('compute.text_1126')
  354. return ret
  355. }
  356. ret.validate = true
  357. return ret
  358. },
  359. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_sync_config'),
  360. },
  361. // 设置删除保护
  362. disableDeleteAction(Object.assign(this, {}), {
  363. name: this.$t('compute.vminstance-container'),
  364. }),
  365. // 删除
  366. {
  367. label: this.$t('compute.perform_delete'),
  368. action: () => {
  369. this.createDialog('DeleteVmContainerDialog', {
  370. vm: this,
  371. data: this.list.selectedItems,
  372. columns: this.columns,
  373. onManager: this.onManager,
  374. title: this.$t('compute.perform_delete'),
  375. })
  376. },
  377. meta: () => {
  378. return this.$getDeleteResult(this.list.selectedItems)
  379. },
  380. },
  381. ]
  382. },
  383. meta: () => {
  384. let ret = {
  385. validate: true,
  386. tooltip: null,
  387. }
  388. ret.validate = this.list.selectedItems.length > 0
  389. if (!ret.validate) return ret
  390. ret = this.$isValidateResourceLock(this.list.selectedItems)
  391. return ret
  392. },
  393. },
  394. ],
  395. execLoading: false,
  396. tagConfigParams: {
  397. title: this.$t('common.text00124'),
  398. resource: 'servers',
  399. queryTreeId: 'project-tag-value-tree',
  400. },
  401. hasContainerHost: false,
  402. }
  403. },
  404. computed: {
  405. ...mapGetters(['isAdminMode']),
  406. isSameHyper () {
  407. if (this.list.selectedItems.length > 0) {
  408. const arr = this.list.selectedItems.map(v => v.hypervisor)
  409. const noRepeatArr = Array.from(new Set(arr))
  410. return noRepeatArr.length === 1
  411. }
  412. return true
  413. },
  414. isSameArch () {
  415. return true
  416. },
  417. exportDataOptions () {
  418. const ret = {
  419. isOpenExportUsernamePassword: false,
  420. downloadType: 'local',
  421. items: [
  422. { label: 'ID', key: 'id' },
  423. { label: this.$t('table.title.external_id'), key: 'external_id' },
  424. ],
  425. fixedItems: [
  426. { key: 'metadata.os_distribution', label: this.$t('table.title.os') },
  427. { key: 'disk', label: this.$t('table.title.disk') + '(M)' },
  428. { key: 'vmem_size', label: this.$t('table.title.vmem_size') + '(M)' },
  429. { key: 'eip', title: this.$t('common.eip') },
  430. { key: 'ips', title: 'IP' },
  431. { key: 'is_gpu', title: `${this.$t('table.title.type')}(${this.$t('compute.text_113')}${this.$t('dictionary.server')})` },
  432. ],
  433. hiddenFields: ['os_dist', 'elastic_ip', 'ip'],
  434. async beforeExport () {
  435. const checkOpenExportUsernamePassword = async () => {
  436. try {
  437. const configM = new Manager('services', 'v1')
  438. const servicesRes = await configM.list({ params: { type: 'compute_v2' } })
  439. const serviceId = servicesRes.data.data?.[0]?.id
  440. const configData = await configM.getSpecific({ id: serviceId, spec: 'config' })
  441. const { enable_export_username_password = false } = configData.data?.config?.default
  442. ret.isOpenExportUsernamePassword = enable_export_username_password
  443. return enable_export_username_password
  444. } catch (error) {
  445. console.log('fetch compute_v2 service config error!!!')
  446. }
  447. }
  448. const isOpenExportUsernamePassword = await checkOpenExportUsernamePassword()
  449. ret.isOpenExportUsernamePassword = isOpenExportUsernamePassword
  450. ret.items.forEach(item => {
  451. if (item.field === 'extra_user') {
  452. item.hidden = !isOpenExportUsernamePassword
  453. }
  454. if (item.field === 'extra_password') {
  455. item.hidden = !isOpenExportUsernamePassword
  456. }
  457. })
  458. },
  459. async callback (data, selectedExportKeys) {
  460. if (!ret.isOpenExportUsernamePassword) {
  461. return data
  462. }
  463. if (!selectedExportKeys.includes('extra_user') && !selectedExportKeys.includes('extra_password')) {
  464. return data
  465. }
  466. const manager = new Manager('servers')
  467. const allPromise = data.map(async item => {
  468. let username = ''
  469. let password = ''
  470. try {
  471. const { data } = await manager.objectRpc({
  472. methodname: 'GetLoginInfo',
  473. objId: item.id,
  474. })
  475. username = data.username
  476. password = data.password
  477. } catch (error) {
  478. console.log(`server: ${item.id}, login info fetch error!!!`)
  479. }
  480. return Promise.resolve({ id: item.id, username, password })
  481. })
  482. const results = Promise.all(allPromise).then(values => {
  483. const realData = data.map(item => {
  484. const curObj = values.find(v => v.id === item.id)
  485. return {
  486. ...item,
  487. extra_user: curObj.username,
  488. extra_password: curObj.password,
  489. }
  490. })
  491. return realData
  492. })
  493. return results
  494. },
  495. }
  496. this.columns.map(col => {
  497. if (col.field === 'ips') {
  498. ret.items.push(getIpsTableColumn({ field: 'elastic_ip', title: this.$t('common.eip'), vm: this, onlyElastic: true }))
  499. ret.items.push(getIpsTableColumn({ field: 'ip', title: 'IP', vm: this, noElastic: true }))
  500. } else if (!(col.hidden && col.hidden()) && col.field !== 'password') {
  501. ret.items.push({
  502. field: col.field,
  503. title: col.title || col.label,
  504. formatter: col.formatter,
  505. })
  506. } else if (col.field === 'password') {
  507. ret.items.push({ field: 'extra_user', title: this.$t('compute.text_566') })
  508. ret.items.push({ field: 'extra_password', title: this.$t('common_328') })
  509. }
  510. if (col.field === 'region') {
  511. ret.items.push({ field: 'zone', title: this.$t('compute.text_270') })
  512. }
  513. })
  514. return ret
  515. },
  516. },
  517. watch: {
  518. cloudEnv (val) {
  519. this.$nextTick(() => {
  520. this.list.fetchData(0)
  521. })
  522. },
  523. },
  524. created () {
  525. this.fetchHost()
  526. this.initSidePageTab('detail')
  527. this.list.fetchData().then(() => {
  528. this.$nextTick(() => {
  529. if (this.$route.query.id && this.list.data[this.$route.query.id]) {
  530. this.handleOpenSidepage(this.list.data[this.$route.query.id].data)
  531. }
  532. })
  533. })
  534. this.$bus.$on('VMInstanceListSingleUpdate', args => {
  535. this.list.singleUpdate(...args)
  536. }, this)
  537. this.$bus.$on('VMInstanceListSingleRefresh', args => {
  538. this.list.singleRefresh(...args)
  539. }, this)
  540. },
  541. methods: {
  542. fetchHost () {
  543. try {
  544. new this.$Manager('hosts').list({
  545. params: {
  546. limit: 1,
  547. enabled: true,
  548. host_type: 'container',
  549. },
  550. }).then(res => {
  551. const list = res.data?.data || []
  552. this.hasContainerHost = list.length > 0
  553. })
  554. } catch (err) { }
  555. },
  556. getParam () {
  557. const ret = {
  558. details: true,
  559. with_meta: true,
  560. filter: 'hypervisor.in(pod)',
  561. ...this.getParams,
  562. }
  563. if (this.cloudEnv) ret.cloud_env = this.cloudEnv
  564. return ret
  565. },
  566. handleOpenSidepage (row, tab) {
  567. this.sidePageTriggerHandle(this, 'VmContainerInstanceSidePage', {
  568. id: row.id,
  569. resource: 'servers',
  570. getParams: this.getParam,
  571. steadyStatus: Object.values(expectStatus.server).flat(),
  572. }, {
  573. list: this.list,
  574. tab,
  575. })
  576. },
  577. defaultSearchKey (search) {
  578. if (regexp.isIPv4(search)) {
  579. return 'ip_addr'
  580. }
  581. },
  582. handleListRefresh () {
  583. this.list.refresh()
  584. // 新建按钮无法点击时,刷新云资源情况
  585. this.cloudEnvEmpty && this.$store.dispatch('auth/getCapabilities')
  586. },
  587. hasSomeCloud (selectItems, clouds = [typeClouds.hypervisorMap.bingocloud.key]) {
  588. const ret = { validate: true, tooltip: '' }
  589. const hasList = selectItems.filter(item => clouds.includes(item.hypervisor))
  590. if (hasList && hasList[0]) {
  591. ret.validate = false
  592. ret.tooltip = this.$t('compute.text_473', [typeClouds.hypervisorMap[hasList[0].hypervisor].label])
  593. }
  594. return ret
  595. },
  596. },
  597. }
  598. </script>