Deploy.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <template>
  2. <div>
  3. <el-select v-model="app_id" @change="fetchEnabledHosts">
  4. <el-option v-for="item in apps" :key="item.id" :value="item.id" :label="item.name"></el-option>
  5. </el-select>
  6. <el-select v-model="env_id" @change="fetchEnabledHosts" style="margin-left: 15px">
  7. <el-option v-for="item in environments" :key="item.id" :value="item.id" :label="item.name"></el-option>
  8. </el-select>
  9. <el-button v-if="has_permission('publish_app_publish_deploy') && (TopDeployMenus.length === 0 || hideDeployBtn)"
  10. @click="dialogPreDeployVisible = true"
  11. type="primary" style="float: right; margin-left: 15px"
  12. :disabled="hideDeployBtn">发布
  13. </el-button>
  14. <el-dropdown v-else-if="has_permission('publish_app_publish_deploy')" split-button type="primary"
  15. @click="dialogPreDeployVisible = true"
  16. :disabled="hideDeployBtn"
  17. @command="do_action"
  18. style="float: right; margin-left: 15px; ">发布
  19. <el-dropdown-menu slot="dropdown" v-if="has_permission('publish_app_publish_menu_exec')">
  20. <el-dropdown-item v-for="item in TopDeployMenus" :key="item.id" :command="`${item.id}`"
  21. style="color: #475669"
  22. :disabled="hideDeployBtn">{{item.name}}
  23. </el-dropdown-item>
  24. </el-dropdown-menu>
  25. </el-dropdown>
  26. <el-button v-if="has_permission('publish_app_publish_host_select')" style="float: right; margin-left: 15px"
  27. @click="fetchAllHosts">选择主机
  28. </el-button>
  29. <el-button style="float: right" @click="fetchEnabledHosts">刷新</el-button>
  30. <div v-loading="tableLoading || fields === undefined">
  31. <el-table v-if="fields !== undefined"
  32. ref="multipleTable"
  33. :data="tableData"
  34. @selection-change="handleSelectHost"
  35. @row-click="handleClickRow"
  36. style="width: 100%; margin-top: 20px">
  37. <el-table-column type="selection" width="50"></el-table-column>
  38. <el-table-column prop="name" label="主机" show-overflow-tooltip></el-table-column>
  39. <el-table-column label="镜像版本" show-overflow-tooltip>
  40. <template slot-scope="scope">
  41. <span v-if="scope.row.image">{{ scope.row.image }}</span>
  42. <i v-else class="el-icon-loading"></i>
  43. </template>
  44. </el-table-column>
  45. <el-table-column v-for="field in fields" :key="field.id" :label="field.name" show-overflow-tooltip>
  46. <template slot-scope="scope">
  47. <span v-if="field.output[scope.row.id]">{{ field.output[scope.row.id] }}</span>
  48. <i v-else class="el-icon-loading"></i>
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="状态" ref="table_status">
  52. <template slot-scope="scope">
  53. <el-tooltip :content="scope.row.status" placement="top" :enterable="false">
  54. <el-tag v-if="scope.row.status === 'v_start exit'" type="warning">运行中</el-tag>
  55. <el-tag v-else-if="scope.row.running" type="success">运行中</el-tag>
  56. <el-tag v-else-if="scope.row.status === 'N/A'" type="info">未发布</el-tag>
  57. <el-tag v-else-if="scope.row.status === 'ERROR'" type="danger">异常</el-tag>
  58. <el-tag v-else-if="scope.row.running === false" type="danger">已停止</el-tag>
  59. <i v-else class="el-icon-loading"></i>
  60. </el-tooltip>
  61. </template>
  62. </el-table-column>
  63. <el-table-column label="操作" width="170px"
  64. v-if="has_permission('publish_app_publish_ctr_control|publish_app_publish_ctr_del') || (RowDeployMenus.length && has_permission('publish_app_publish_menu_exec'))">
  65. <template slot-scope="scope">
  66. <el-button v-if="has_permission('publish_app_publish_ctr_control') && scope.row.running"
  67. type="danger"
  68. :disabled="scope.row.status === 'N/A'" @click="doAction(scope.row, 'v_stop')"
  69. size="small"
  70. style="margin-right: 15px"
  71. :loading="btnCtrLoading[scope.row.id]">停止
  72. </el-button>
  73. <el-button v-else-if="has_permission('publish_app_publish_ctr_control')" size="small"
  74. type="success"
  75. :disabled="scope.row.status === 'N/A'"
  76. @click="doAction(scope.row, 'v_start')"
  77. style="margin-right: 15px"
  78. :loading="btnCtrLoading[scope.row.id]">启动
  79. </el-button>
  80. <el-dropdown @command="do_action"
  81. v-if="has_permission('publish_app_publish_ctr_del') || (RowDeployMenus.length && has_permission('publish_app_publish_menu_exec'))">
  82. <el-button type="text">更多<i class="el-icon-caret-bottom el-icon--right"></i></el-button>
  83. <el-dropdown-menu slot="dropdown">
  84. <el-dropdown-item v-for="item in RowDeployMenus"
  85. v-if="has_permission('publish_app_publish_menu_exec')" :key="item.id"
  86. :command="`${item.id} ${scope.row.id}`"
  87. style="color: #20A0FF">{{item.name}}
  88. </el-dropdown-item>
  89. <el-dropdown-item :disabled="scope.row.status === 'N/A'"
  90. :command="`console_log ${scope.row.id}`">日志
  91. </el-dropdown-item>
  92. <el-dropdown-item v-if="has_permission('publish_app_publish_ctr_del')"
  93. :disabled="scope.row.status === 'N/A'" divided
  94. :command="`del ${scope.row.id}`">删除
  95. </el-dropdown-item>
  96. </el-dropdown-menu>
  97. </el-dropdown>
  98. </template>
  99. </el-table-column>
  100. </el-table>
  101. </div>
  102. <el-dialog title="选择启用的主机" :visible.sync="dialogSelectVisible" :close-on-click-modal="false">
  103. <el-tree :data="treeData" ref="tree" nodeKey="id" showCheckbox></el-tree>
  104. <div slot="footer">
  105. <el-button @click="dialogSelectVisible = false">取 消</el-button>
  106. <el-button type="primary" @click="saveHost" :loading="dialogLoading">确 定</el-button>
  107. </div>
  108. </el-dialog>
  109. <el-dialog title="应用发布" :visible.sync="dialogDeployVisible" @close="fetchHostStatus(true)"
  110. :close-on-click-modal="false">
  111. <el-collapse :value="showItem" accordion>
  112. <el-collapse-item v-for="item in updateHosts" :key="item.id" :name="item.id">
  113. <template slot="title">
  114. <el-tag :type="item.type" style="margin-right: 15px">{{item.name}}</el-tag>
  115. {{item.latest}}
  116. </template>
  117. <pre v-for="line in item.detail">{{line}}</pre>
  118. </el-collapse-item>
  119. </el-collapse>
  120. </el-dialog>
  121. <el-dialog title="发布提示" :visible.sync="dialogPreDeployVisible" :close-on-click-modal="false">
  122. <el-form label-width="80px">
  123. <el-form-item v-if="!SysDeployMenus['_init'].id" label="初始化">
  124. <color-input v-model="SysDeployMenus['_init'].command"></color-input>
  125. </el-form-item>
  126. <el-form-item v-if="!SysDeployMenus['_update'].id" label="发布执行">
  127. <color-input v-model="SysDeployMenus['_update'].command"></color-input>
  128. </el-form-item>
  129. <el-form-item v-if="!SysDeployMenus['_start'].id" label="启动执行">
  130. <color-input v-model="SysDeployMenus['_start'].command"></color-input>
  131. </el-form-item>
  132. <el-form-item label="输入消息">
  133. <el-autocomplete popper-class="my-autocomplete" v-model="deploy_message"
  134. :fetch-suggestions="querySearch"
  135. valueKey="deploy_message"
  136. style="width: 280px"
  137. custom-item="my-item-zh" placeholder="此内容会作为位置参数传递给更新命令"></el-autocomplete>
  138. </el-form-item>
  139. <el-form-item label="重启容器">
  140. <el-switch v-model="deploy_restart"></el-switch>
  141. </el-form-item>
  142. </el-form>
  143. <div slot="footer">
  144. <el-button @click="dialogPreDeployVisible = false">取 消</el-button>
  145. <el-button type="primary" @click="handleDeploy" :loading="dialogLoading">确 定</el-button>
  146. </div>
  147. </el-dialog>
  148. <el-dialog title="控制台日志" :visible.sync="console_logs.show" :close-on-click-modal="false">
  149. <pre>{{console_logs.detail}}</pre>
  150. </el-dialog>
  151. <menu-exec v-if="dialogMsgVisible" :menu="menus[execForm['menu_id']]" :data="execForm"
  152. @close="dialogMsgVisible = false"></menu-exec>
  153. </div>
  154. </template>
  155. <style>
  156. pre {
  157. display: block;
  158. padding: 5px 9px;
  159. margin: 0 0 10px;
  160. font-size: 13px;
  161. line-height: 1.42857143;
  162. color: #333;
  163. word-break: break-all;
  164. word-wrap: break-word;
  165. background-color: #f5f5f5;
  166. border: 1px solid #ccc;
  167. border-radius: 4px;
  168. white-space: pre-line;
  169. }
  170. .el-collapse-item__header {
  171. overflow: hidden;
  172. }
  173. .my-autocomplete li {
  174. line-height: normal;
  175. padding: 7px;
  176. }
  177. .my-autocomplete li .name {
  178. text-overflow: ellipsis;
  179. overflow: hidden;
  180. }
  181. .my-autocomplete li .date {
  182. font-size: 12px;
  183. color: #b4b4b4;
  184. }
  185. .my-autocomplete li .highlighted .date {
  186. color: #ddd;
  187. }
  188. </style>
  189. <script>
  190. import ColorInput from './ColorInput.vue'
  191. import MenuExec from './MenuExec.vue'
  192. import Vue from 'vue'
  193. Vue.component('my-item-zh', {
  194. functional: true,
  195. render: function (h, ctx) {
  196. let item = ctx.props.item;
  197. return h('li', ctx.data, [
  198. h('div', {attrs: {'class': 'name'}}, [item.deploy_message]),
  199. h('span', {attrs: {'class': 'date'}}, [item.created])
  200. ]);
  201. },
  202. props: {
  203. item: {type: Object, required: true}
  204. }
  205. });
  206. export default {
  207. components: {
  208. MenuExec,
  209. 'color-input': ColorInput,
  210. 'menu-exec': MenuExec
  211. },
  212. data() {
  213. return {
  214. apps: [],
  215. env_id: '',
  216. environments: [],
  217. btnCtrLoading: {},
  218. dialogSelectVisible: false,
  219. dialogDeployVisible: false,
  220. dialogPreDeployVisible: false,
  221. dialogMsgVisible: false,
  222. dialogLoading: false,
  223. tableLoading: false,
  224. deploy_message: '',
  225. deploy_restart: false,
  226. deploy_histories: undefined,
  227. app_id: Number(this.$route.params['app_id']),
  228. tableData: [],
  229. treeData: [],
  230. updateHosts: [],
  231. fields: undefined,
  232. menus: {},
  233. execForm: {},
  234. TopDeployMenus: [],
  235. RowDeployMenus: [],
  236. SysDeployMenus: {'_init': {}, '_update': {}, '_start': {}},
  237. console_logs: {}
  238. }
  239. },
  240. computed: {
  241. hideDeployBtn() {
  242. return this.updateHosts.length === 0
  243. },
  244. showItem() {
  245. return (this.updateHosts.length === 1) ? this.updateHosts[0].id : ''
  246. },
  247. deployForm() {
  248. return {
  249. app_id: this.app_id,
  250. env_id: this.env_id,
  251. deploy_message: this.deploy_message,
  252. deploy_restart: this.deploy_restart,
  253. host_ids: this.updateHosts.map(x => x.id)
  254. }
  255. }
  256. },
  257. methods: {
  258. // 选择发布主机页面 保存操作处理
  259. saveHost() {
  260. this.dialogLoading = true;
  261. let host_ids = this.$refs['tree'].getCheckedKeys();
  262. this.$http.post(`/api/deploy/apps/${this.app_id}/bind/hosts`, {
  263. ids: host_ids,
  264. env_id: this.env_id
  265. }).then(() => {
  266. this.fetchEnabledHosts();
  267. this.dialogSelectVisible = false
  268. }, res => {
  269. this.$layer_message(res.result);
  270. this.updateSelectedHosts()
  271. }).finally(() => this.dialogLoading = false)
  272. },
  273. // 更新选择发布主机弹出页面的勾选状态
  274. updateSelectedHosts() {
  275. if (this.treeData.length) {
  276. this.$refs['tree'].setCheckedKeys(this.tableData.map(x => x.id))
  277. }
  278. },
  279. // 获取自定义菜单
  280. fetchDeployMenus() {
  281. this.$http.get(`/api/deploy/apps/${this.app_id}/menus?type=all`).then(res => {
  282. for (let item of res.result) {
  283. this.menus[item.id] = item;
  284. if (item.position === 1) {
  285. this.TopDeployMenus.push(item)
  286. } else if (item.position === 2) {
  287. this.RowDeployMenus.push(item)
  288. } else if (item.name === '容器创建') {
  289. this.SysDeployMenus['_init'] = item
  290. } else if (item.name === '应用发布') {
  291. this.SysDeployMenus['_update'] = item
  292. } else if (item.name === '容器启动') {
  293. this.SysDeployMenus['_start'] = item
  294. }
  295. }
  296. }, res => this.$layer_message(res.result))
  297. },
  298. // 更新自定义字段
  299. updateDeployFields() {
  300. this.fields = this.fields.map(x => {
  301. x['output'] = [];
  302. return x
  303. });
  304. for (let item of this.fields) {
  305. for (let host of this.tableData) {
  306. let form = {host_id: host.id, env_id: this.env_id, app_id: this.app_id};
  307. this.$http.post(`/api/deploy/fields/${item.id}/exec`, form).then(res => {
  308. this.$set(item['output'], host.id, res.result)
  309. }, res => {
  310. this.$set(item['output'], host.id, 'N/A');
  311. this.$layer_message(res.result)
  312. })
  313. }
  314. }
  315. },
  316. // 获取自定义字段
  317. fetchDeployFields() {
  318. if (this.fields === undefined) {
  319. this.$http.get(`/api/deploy/apps/${this.app_id}/fields`).then(res => {
  320. this.fields = res.result.map(x => {
  321. x['output'] = '';
  322. return x
  323. });
  324. this.updateDeployFields()
  325. }, res => this.$layer_message(res.result))
  326. } else {
  327. this.updateDeployFields()
  328. }
  329. },
  330. // 处理发布弹出页中输入信息的可选项,可选项为发布的历史记录
  331. querySearch(query, cb) {
  332. if (this.deploy_histories === undefined) {
  333. this.$http.get(`/api/deploy/publish/history/${this.app_id}`).then(res => {
  334. this.deploy_histories = res.result;
  335. cb(this.filter_history(query))
  336. })
  337. } else {
  338. cb(this.filter_history(query))
  339. }
  340. },
  341. // 配合querySearch方法,过滤用户输入的匹配项
  342. filter_history(query) {
  343. return query ? this.deploy_histories.filter(item => {
  344. return item['deploy_message'].indexOf(query) === 0
  345. }) : this.deploy_histories
  346. },
  347. // 选择发布主机
  348. fetchAllHosts() {
  349. this.dialogSelectVisible = true;
  350. if (this.treeData.length) return;
  351. this.$http.get('/api/assets/hosts/?page=-1').then(res => {
  352. let rst = {};
  353. for (let host of res.result.data) {
  354. if (rst.hasOwnProperty(host.zone)) {
  355. rst[host.zone].push({label: `${host.name} (${host.desc})`, id: host.id})
  356. } else {
  357. rst[host.zone] = [{label: `${host.name} (${host.desc})`, id: host.id}]
  358. }
  359. }
  360. let tmp = [];
  361. for (let k in rst) {
  362. tmp.push({label: k, children: rst[k]})
  363. }
  364. this.treeData = tmp;
  365. this.updateSelectedHosts()
  366. }, res => this.$layer_message(res.result))
  367. },
  368. fetchEnabledHosts() {
  369. if (!this.env_id) return;
  370. this.tableLoading = true;
  371. this.$http.get(`/api/deploy/hosts/${this.app_id}/${this.env_id}`).then(res => {
  372. this.tableData = res.result;
  373. this.fetchHostStatus();
  374. this.updateSelectedHosts()
  375. }, res => this.$layer_message(res.result)).finally(() => this.tableLoading = false)
  376. },
  377. fetchHostStatus(alone) {
  378. if (alone) this.tableLoading = true;
  379. // 应用发布后设置history为undefined,以让下次点击发布时更新输入信息的历史记录
  380. this.deploy_histories = undefined;
  381. // 应用发布后更新自定义字段的值
  382. this.fetchDeployFields();
  383. // 更新容器状态
  384. for (let item of this.tableData) {
  385. // 先设置字段值为空,使页面出现loading
  386. this.$set(item, 'running', '');
  387. this.$set(item, 'status', '');
  388. this.$set(item, 'image', '');
  389. this.$http.post('/api/deploy/hosts/state', {
  390. app_id: this.app_id,
  391. env_id: this.env_id,
  392. cli_id: item.id
  393. }).then(res => {
  394. this.$set(item, 'running', res.result['running']);
  395. this.$set(item, 'status', res.result['status']);
  396. if (res.result['image'] !== 'N/A') {
  397. res.result['image'] = res.result['image'].split('/')[1]
  398. }
  399. this.$set(item, 'image', res.result['image'])
  400. }, res => {
  401. this.$set(item, 'running', false);
  402. this.$set(item, 'status', 'ERROR');
  403. this.$set(item, 'image', 'N/A');
  404. this.$layer_message(res.result)
  405. }).finally(() => this.tableLoading = false)
  406. }
  407. },
  408. handleSelectHost(val) {
  409. let local_val = this.$deepCopy(val);
  410. for (let item of local_val) {
  411. item.type = 'info';
  412. item.latest = '等待调度 . . . '
  413. }
  414. this.updateHosts = local_val
  415. },
  416. _handleDeploy() {
  417. this.$http.post('/api/deploy/publish/update', this.deployForm).then(res => {
  418. this.dialogPreDeployVisible = false;
  419. this.dialogDeployVisible = true;
  420. // 初始化状态
  421. for (let [index, item] of this.updateHosts.entries()) {
  422. item.type = 'info';
  423. delete item.detail;
  424. delete item.latest;
  425. this.$set(this.updateHosts, index, item)
  426. }
  427. this.fetchDeployResult(res.result);
  428. }, res => this.$layer_message(res.result)).finally(() => this.dialogLoading = false)
  429. },
  430. handleDeploy() {
  431. this.dialogLoading = true;
  432. if (!this.SysDeployMenus['_update'].id) {
  433. this.$http.post(`/api/deploy/apps/${this.app_id}/bind/menus`, [
  434. {'name': '容器创建', 'command': this.SysDeployMenus['_init'].command},
  435. {'name': '应用发布', 'command': this.SysDeployMenus['_update'].command},
  436. {'name': '容器启动', 'command': this.SysDeployMenus['_start'].command}
  437. ]).then(() => {
  438. this._handleDeploy();
  439. this.fetchDeployMenus()
  440. }, res => {
  441. this.$layer_message(res.result);
  442. this.dialogLoading = false
  443. })
  444. } else {
  445. this._handleDeploy()
  446. }
  447. },
  448. fetchDeployResult(token) {
  449. this.$http.get(`/api/common/queue/state/${token}`).then(res => {
  450. if (res.result['complete'] === true) return;
  451. this.fetchDeployResult(token);
  452. for (let [index, item] of Object.entries(this.updateHosts)) {
  453. if (item.hasOwnProperty('detail') === false) item.detail = [];
  454. if (item.id === res.result.hid) {
  455. if (res.result.update === true) item.detail.pop();
  456. item.detail.push(res.result.msg);
  457. if (res.result.level !== 'console') item.latest = res.result.msg;
  458. if (res.result.level === 'error') item.type = 'danger';
  459. if (res.result.level === 'success') item.type = 'success';
  460. this.$set(this.updateHosts, index, item);
  461. break
  462. }
  463. }
  464. }, res => this.$layer_message(res.result))
  465. },
  466. doAction(row, action) {
  467. this.btnCtrLoading = {[row.id]: true};
  468. this.$http.post(`/api/deploy/hosts/`, {
  469. app_id: this.app_id,
  470. env_id: this.env_id,
  471. cli_id: row.id,
  472. action: action
  473. }).then(() => {
  474. this.fetchHostStatus(true)
  475. }, res => this.$layer_message(res.result)).finally(() => {
  476. this.btnCtrLoading = {}
  477. })
  478. },
  479. do_action(command) {
  480. let [action, cli_id] = command.split(' ');
  481. let form = {app_id: this.app_id, env_id: this.env_id, cli_id: cli_id};
  482. if (action === 'del') {
  483. form['action'] = 'v_remove';
  484. this.$confirm('此操作将删除该容器,是否继续?', '提示', {type: 'warning'}).then(() => {
  485. this.tableLoading = true;
  486. this.$http.post(`/api/deploy/hosts/`, form)
  487. .then(() => this.fetchHostStatus(true), res => {
  488. this.tableLoading = false;
  489. this.$layer_message(res.result)
  490. })
  491. }).catch(() => {
  492. })
  493. } else if (action === 'console_log') {
  494. this.$http.post(`/api/deploy/hosts/logs`, form).then(res => {
  495. this.console_logs = {show: true, detail: res.result}
  496. }, res => this.$layer_message(res.result))
  497. } else {
  498. this.execForm = {app_id: this.app_id, env_id: this.env_id, menu_id: action};
  499. if (cli_id) {
  500. this.execForm['host_ids'] = [cli_id];
  501. } else {
  502. this.execForm['host_ids'] = this.updateHosts.map(x => x.id)
  503. }
  504. this.dialogMsgVisible = true;
  505. }
  506. },
  507. handleClickRow(row) {
  508. this.$refs.multipleTable.toggleRowSelection(row)
  509. }
  510. },
  511. mounted() {
  512. this.fetchDeployMenus();
  513. this.$http.get('/api/configuration/environments/with_publish_permission').then(res => {
  514. this.environments = res.result;
  515. if (this.environments.length) {
  516. this.env_id = this.environments[0].id;
  517. this.fetchEnabledHosts()
  518. } else {
  519. this.fields = [];
  520. this.$layer_message('请在配置管理的环境管理中先创建发布环境')
  521. }
  522. }, res => this.$layer_message(res.result));
  523. this.$http.get('/api/deploy/apps/').then(res => {
  524. this.apps = res.result;
  525. }, res => this.$layer_message(res.result))
  526. // select 组件在初始化时会自动调用,固这里不需要在调用 fetchEnabledHosts()
  527. // this.fetchEnabledHosts()
  528. }
  529. }
  530. </script>