List.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <template>
  2. <page-list
  3. show-tag-filter
  4. :show-searchbox="true"
  5. :list="list"
  6. :columns="columns"
  7. :group-actions="groupActions"
  8. :single-actions="singleActions"
  9. :export-data-options="exportDataOptions" />
  10. </template>
  11. <script>
  12. import expectStatus from '@/constants/expectStatus'
  13. import WindowsMixin from '@/mixins/windows'
  14. import ListMixin from '@/mixins/list'
  15. import {
  16. getNameFilter,
  17. getStatusFilter,
  18. getTenantFilter,
  19. getDomainFilter,
  20. getCreatedAtFilter,
  21. getCustomDistinctFieldFilter,
  22. getDistinctFieldFilter,
  23. } from '@/utils/common/tableFilter'
  24. import { LLM_TYPE_OPTIONS } from '../../llm-sku/llmTypeConfig'
  25. import SingleActionsMixin from '../mixins/singleActions'
  26. import ColumnsMixin from '../mixins/columns'
  27. export default {
  28. name: 'PhoneList',
  29. mixins: [WindowsMixin, ListMixin, ColumnsMixin, SingleActionsMixin],
  30. props: {
  31. id: String,
  32. getParams: {
  33. type: Object,
  34. },
  35. },
  36. data () {
  37. return {
  38. list: this.$list.createList(this, {
  39. id: this.id,
  40. resource: 'llms',
  41. getParams: this.getParam,
  42. steadyStatus: {
  43. status: (data) => {
  44. if (Object.values(expectStatus.server).flat().includes(data.status) || Object.values(expectStatus.container).flat().includes(data.llm_status)) {
  45. return false
  46. }
  47. return true
  48. },
  49. },
  50. filterOptions: {
  51. id: {
  52. label: this.$t('table.title.id'),
  53. },
  54. name: getNameFilter(),
  55. llm_type: {
  56. label: this.isApplyType ? this.$t('aice.llm_type.app') : this.$t('aice.llm_type.llm'),
  57. dropdown: true,
  58. items: LLM_TYPE_OPTIONS.map(opt => ({ key: opt.id, label: this.$t(opt.name) })),
  59. },
  60. status: getStatusFilter('server'),
  61. llm_ip: {
  62. label: 'IP',
  63. filter: true,
  64. formatter: val => {
  65. return `llm_ip.contains(${val})`
  66. },
  67. },
  68. host: getCustomDistinctFieldFilter({
  69. label: this.$t('res.host'),
  70. field: 'host',
  71. fetchMethod: this.hostFetcher,
  72. disabledFormatter: true,
  73. multiple: false,
  74. mapper: (list) => {
  75. return list
  76. },
  77. }),
  78. // no_volume: {
  79. // label: this.$t('aice.volume_mounted'),
  80. // dropdown: true,
  81. // items: [
  82. // { label: this.$t('compute.text_394'), key: 'false' },
  83. // { label: this.$t('compute.text_395'), key: 'true' },
  84. // ],
  85. // },
  86. // llm_sku: {
  87. // label: this.$t('aice.llm_sku'),
  88. // },
  89. llm_sku: getDistinctFieldFilter({
  90. field: 'llm_sku',
  91. type: 'extra_field',
  92. label: this.$t('aice.llm_sku'),
  93. multiple: false,
  94. }),
  95. llm_image: {
  96. label: this.$t('aice.image'),
  97. },
  98. projects: getTenantFilter(),
  99. project_domains: getDomainFilter(),
  100. created_at: getCreatedAtFilter(),
  101. },
  102. hiddenColumns: [],
  103. fetchDataCb: (response) => {
  104. this.enrichLlmRowsWithCmpInfo(response)
  105. },
  106. }),
  107. groupActions: [
  108. {
  109. label: this.$t('common.create'),
  110. action: () => {
  111. this.$router.push(this.isApplyType ? '/app-llm/create' : '/llm/create')
  112. },
  113. meta: () => {
  114. return {
  115. buttonType: 'primary',
  116. validate: true,
  117. }
  118. },
  119. },
  120. // 开机
  121. {
  122. label: this.$t('compute.text_272'),
  123. action: () => {
  124. this.createDialog('LlmBatchConfirmDialog', {
  125. data: this.list.selectedItems,
  126. columns: this.columns,
  127. onManager: this.onManager,
  128. action: 'start',
  129. actionText: this.$t('compute.text_272'),
  130. steadyStatus: 'running',
  131. })
  132. },
  133. meta: () => {
  134. let ret = {
  135. validate: true,
  136. tooltip: null,
  137. }
  138. ret.validate = this.list.selectedItems.length > 0
  139. if (!ret.validate) return ret
  140. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  141. ret.validate = this.list.selectedItems.every(item => item.status === 'ready')
  142. return ret
  143. })
  144. return ret
  145. },
  146. // hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_start'),
  147. },
  148. // 重启
  149. {
  150. label: this.$t('compute.text_274'),
  151. permission: 'llms_perform_restart',
  152. action: () => {
  153. this.createDialog('LlmBatchConfirmDialog', {
  154. data: this.list.selectedItems,
  155. columns: this.columns,
  156. onManager: this.onManager,
  157. action: 'restart',
  158. actionText: this.$t('compute.text_274'),
  159. steadyStatus: 'running',
  160. })
  161. },
  162. meta: () => {
  163. let ret = {
  164. validate: true,
  165. tooltip: null,
  166. }
  167. ret.validate = this.list.selectedItems.length > 0
  168. if (!ret.validate) return ret
  169. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  170. ret.validate = this.list.selectedItems.every(item => ['running', 'stop_fail', 'ready'].includes(item.status))
  171. return ret
  172. })
  173. return ret
  174. },
  175. },
  176. // 同步状态
  177. {
  178. label: this.$t('common.text00043'),
  179. action: () => {
  180. this.onManager('batchPerformAction', {
  181. steadyStatus: ['running', 'ready'],
  182. managerArgs: {
  183. action: 'syncstatus',
  184. },
  185. })
  186. },
  187. meta: () => {
  188. return {
  189. validate: this.list.selectedItems.length > 0,
  190. }
  191. },
  192. },
  193. /* 批量操作 */
  194. {
  195. label: this.$t('compute.text_275'),
  196. actions: () => {
  197. return [
  198. // 更改项目
  199. {
  200. label: this.$t('compute.perform_change_owner', [this.$t('dictionary.project')]),
  201. permission: 'llms_perform_public',
  202. action: (obj) => {
  203. this.createDialog('ChangeOwenrDialog', {
  204. data: this.list.selectedItems,
  205. columns: this.columns,
  206. onManager: this.onManager,
  207. refresh: this.refresh,
  208. resource: 'llms',
  209. })
  210. },
  211. meta: () => {
  212. const ret = {
  213. validate: true,
  214. tooltip: null,
  215. }
  216. ret.validate = this.list.selectedItems.length > 0
  217. return ret
  218. },
  219. },
  220. // 关机
  221. {
  222. label: this.$t('compute.text_273'),
  223. permission: 'llms_perform_stop',
  224. action: () => {
  225. this.createDialog('LlmBatchConfirmDialog', {
  226. data: this.list.selectedItems,
  227. columns: this.columns,
  228. onManager: this.onManager,
  229. action: 'stop',
  230. actionText: this.$t('compute.text_273'),
  231. steadyStatus: 'ready',
  232. })
  233. },
  234. meta: () => {
  235. let ret = {
  236. validate: true,
  237. tooltip: null,
  238. }
  239. ret.validate = this.list.selectedItems.length > 0
  240. if (!ret.validate) return ret
  241. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  242. ret.validate = this.list.selectedItems.every(item => item.status === 'running')
  243. return ret
  244. })
  245. return ret
  246. },
  247. // hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_stop'),
  248. },
  249. // // 安装秒装应用
  250. // {
  251. // label: this.$t('aice.instant_app.install'),
  252. // action: () => {
  253. // this.createDialog('PhoneAppInstallConfirmDialog', {
  254. // vm: this,
  255. // data: this.list.selectedItems,
  256. // columns: this.columns,
  257. // title: this.$t('aice.instant_app.install'),
  258. // name: this.$t('aice.instant_app'),
  259. // action: this.$t('aice.instant_app.install'),
  260. // actionText: this.$t('aice.instant_app.install'),
  261. // })
  262. // },
  263. // meta: () => {
  264. // let ret = {
  265. // validate: true,
  266. // tooltip: null,
  267. // }
  268. // ret.validate = this.list.selectedItems.length > 0
  269. // if (!ret.validate) return ret
  270. // ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  271. // ret.validate = this.list.selectedItems.every(item => ['running', 'ready'].includes(item.status))
  272. // return ret
  273. // })
  274. // return ret
  275. // },
  276. // },
  277. // 删除
  278. {
  279. label: this.$t('table.action.delete'),
  280. action: () => {
  281. this.createDialog('DeleteResDialog', {
  282. vm: this,
  283. data: this.list.selectedItems,
  284. columns: this.columns,
  285. title: this.$t('table.action.delete'),
  286. name: this.$t('aice.llm'),
  287. onManager: this.onManager,
  288. })
  289. },
  290. meta: () => {
  291. const ret = { validate: this.list.selected.length }
  292. if (this.list.selectedItems.some(item => !item.can_delete)) {
  293. ret.validate = false
  294. return ret
  295. }
  296. return ret
  297. },
  298. },
  299. ]
  300. },
  301. },
  302. ],
  303. }
  304. },
  305. computed: {
  306. isApplyType () {
  307. return this.$route.path.includes('app-llm')
  308. },
  309. exportDataOptions () {
  310. return {
  311. downloadType: 'local',
  312. title: this.$t('aice.instance'),
  313. items: [
  314. { label: 'ID', key: 'id' },
  315. ...this.columns,
  316. ],
  317. }
  318. },
  319. },
  320. created () {
  321. this.$hM = new this.$Manager('hosts')
  322. this.serverManager = new this.$Manager('servers')
  323. this.initSidePageTab('detail')
  324. this.list.fetchData()
  325. },
  326. methods: {
  327. async enrichLlmRowsWithCmpInfo (response) {
  328. const rows = response?.data?.data
  329. if (!Array.isArray(rows) || !rows.length) return
  330. const params = {
  331. scope: this.$store.getters.scope,
  332. }
  333. const patch = {}
  334. await Promise.all(
  335. rows.map(async (row) => {
  336. if (!row.cmp_id) return
  337. try {
  338. const { data } = await this.serverManager.get({ id: row.cmp_id, params })
  339. if (data) {
  340. patch[row.id] = { cmp_info: data }
  341. }
  342. } catch (e) {
  343. // 单行失败不影响列表
  344. }
  345. }),
  346. )
  347. if (this._isDestroyed) return
  348. if (Object.keys(patch).length) {
  349. this.list.updatesProperty(patch)
  350. }
  351. },
  352. getParam () {
  353. const ret = {
  354. ...this.getParams,
  355. llm_types: this.isApplyType ? ['dify', 'openclaw', 'comfyui'] : ['vllm', 'ollama'],
  356. details: true,
  357. }
  358. return ret
  359. },
  360. async hostFetcher () {
  361. const params = {
  362. details: false,
  363. host_type: 'container',
  364. field: [
  365. 'id',
  366. 'name',
  367. ],
  368. }
  369. const response = await this.$hM.list({ params })
  370. return {
  371. data: {
  372. host: (response.data?.data || []).map(item => item.name),
  373. },
  374. }
  375. },
  376. handleOpenSidepage (row) {
  377. this.sidePageTriggerHandle(this, 'LlmSidePage', {
  378. id: row.id,
  379. resource: 'llms',
  380. getParams: this.getParam,
  381. }, {
  382. list: this.list,
  383. })
  384. },
  385. },
  386. }
  387. </script>
  388. <style>
  389. </style>