index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <template>
  2. <overview-card-layout :card_style="`${card_stype} ${showMonitor ? '' : ''}`">
  3. <template #header>
  4. <div v-if="!readOnly">
  5. <a-row type="flex" style="padding: 12px;border-bottom: 1px solid #e8e8e8">
  6. <a-col class="d-flex" style="flex: 1 1 auto">
  7. <!-- 折叠 -->
  8. <a v-if="!isTemplate" class="font-weight-bold h-100 d-block" style="margin-right: 6px;" @click="toggleShowMonitor">
  9. <a-icon type="down" style="font-size: 12px;" v-if="showMonitor" />
  10. <a-icon type="right" style="font-size: 12px;" v-if="!showMonitor" />
  11. </a>
  12. <!-- 表格隐藏 -->
  13. <a-tooltip>
  14. <template slot="title">
  15. {{ $t('monitor.show_hide_legend_table') }}
  16. </template>
  17. <a class="font-weight-bold h-100 d-block" style="margin-right: 6px;" @click="toggleShowTableLegend">
  18. <a-icon type="line-chart" style="font-size: 14px;" v-if="showMonitor && showLegend" />
  19. <a-icon type="credit-card" style="font-size: 14px;" v-if="showMonitor && !showLegend" />
  20. <a-icon type="minus" style="font-size: 14px;" v-if="!showMonitor" />
  21. </a>
  22. </a-tooltip>
  23. <!-- 名称 -->
  24. <span>{{ panel.panel_name || (chart.metric && chart.metric.label) }}</span>
  25. </a-col>
  26. <a-col v-if="!useLocalPanels && !isTemplate" class="flex: 0 0 24px">
  27. <a-dropdown style="float: right" :trigger="['click']" placement="bottomRight">
  28. <a class="ant-dropdown-link font-weight-bold h-100 d-block action-btn" @click="e => e.preventDefault()">
  29. <icon type="more" style="font-size: 18px; margin-left: 9px;" />
  30. </a>
  31. <a-menu slot="overlay" @click="handleActionClick">
  32. <a-menu-item key="handleEdit"><a-icon type="edit" />{{$t('dashboard.text_104')}}</a-menu-item>
  33. <a-menu-item key="handleClone"><a-icon type="copy" />{{$t('dashboard.text_107')}}</a-menu-item>
  34. <a-menu-item key="handleExport"><a-icon type="download" />{{$t('table.action.export')}}</a-menu-item>
  35. <a-menu-item key="handleDelete"><a-icon type="delete" />{{$t('scope.text_18')}}</a-menu-item>
  36. </a-menu>
  37. </a-dropdown>
  38. </a-col>
  39. </a-row>
  40. </div>
  41. <div v-if="selectable">
  42. <a-radio :checked="focusPanelId == panel.panel_id" @click="(e)=>{chose_panel(panel.panel_id,panel.panel_name)}">{{panel.panel_name}}</a-radio>
  43. </div>
  44. </template>
  45. <monitor-line
  46. v-if="showMonitor"
  47. :isTemplate="isTemplate"
  48. ref="monitorLine"
  49. :loading="loading"
  50. :description="description"
  51. :metricInfo="metricInfo"
  52. :series="series"
  53. :reducedResult="reducedResult"
  54. :reducedResultOrder="reducedResultOrder"
  55. :pager="pager"
  56. :showTableLegend="showLegend"
  57. :monitorLineCardStyle="{border:'none'}"
  58. :otherCursorMovePoint="otherCursorMovePoint"
  59. @pageChange="pageChange"
  60. @chartInstance="setChartInstance"
  61. @exportTable="exportTable"
  62. @reducedResultOrderChange="reducedResultOrderChange"
  63. @cursorMove="cursorMove" />
  64. </overview-card-layout>
  65. </template>
  66. <script>
  67. import _ from 'lodash'
  68. import * as R from 'ramda'
  69. import WindowsMixin from '@/mixins/windows'
  70. import DialogMixin from '@/mixins/dialog'
  71. import { metric_zh, tableColumnMaps } from '@Monitor/constants'
  72. import { getSignature } from '@/utils/crypto'
  73. import { getRequestT, transformUnit } from '@/utils/utils'
  74. import { getNameDescriptionTableColumn } from '@/utils/common/tableColumn'
  75. // import { currencyUnitMap } from '@/constants/currency'
  76. import MonitorLine from '@Monitor/sections/MonitorLine'
  77. import { addMissingSeries } from '@Monitor/utils'
  78. import OverviewCardLayout from '../layout'
  79. export default {
  80. name: 'DashboardCard',
  81. components: {
  82. OverviewCardLayout,
  83. MonitorLine,
  84. },
  85. mixins: [DialogMixin, WindowsMixin],
  86. props: {
  87. // 是否作为模板组件使用
  88. isTemplate: {
  89. type: Boolean,
  90. default: false,
  91. },
  92. updated_at: {
  93. type: String,
  94. },
  95. dashboard_id: {
  96. type: String,
  97. },
  98. panel: {
  99. type: Object,
  100. required: true,
  101. default: () => {
  102. return {}
  103. },
  104. },
  105. extraParams: {
  106. type: Object,
  107. default: () => ({}),
  108. },
  109. editChart: {
  110. type: Function,
  111. required: true,
  112. },
  113. card_style: {
  114. type: String,
  115. },
  116. readOnly: {
  117. type: Boolean,
  118. default: () => {
  119. return false
  120. },
  121. },
  122. selectable: {
  123. type: Boolean,
  124. default: () => {
  125. return false
  126. },
  127. },
  128. focusPanelId: {
  129. type: String,
  130. default: () => {
  131. return ''
  132. },
  133. },
  134. chartHeigth: {
  135. type: String,
  136. default: '320px',
  137. },
  138. time: {
  139. type: String,
  140. },
  141. timeGroup: {
  142. type: String,
  143. },
  144. customTime: {
  145. type: Object,
  146. },
  147. groupFunc: {
  148. type: String,
  149. },
  150. tablePageSize: {
  151. type: Number,
  152. default: 10,
  153. },
  154. useLocalPanels: {
  155. type: Boolean,
  156. default: false,
  157. },
  158. cursorMove: {
  159. type: Function,
  160. },
  161. otherCursorMovePoint: {
  162. type: Array,
  163. default: () => {
  164. return [-10, -10]
  165. },
  166. },
  167. },
  168. data () {
  169. return {
  170. chart: {},
  171. tableLoading: false,
  172. resizeStatus: false,
  173. pager: {
  174. page: 1,
  175. limit: this.tablePageSize,
  176. total: 0,
  177. },
  178. series: [],
  179. reducedResult: {},
  180. reducedResultOrder: '',
  181. showLegend: this.isTemplate,
  182. showMonitor: true,
  183. }
  184. },
  185. computed: {
  186. description () {
  187. if (this.useLocalPanels) {
  188. const { selectItem, unit, metric, label } = this.panel.constants
  189. return {
  190. display_name: `${label}${metric ? `(${metric})` : ''}`,
  191. name: selectItem,
  192. unit: unit,
  193. }
  194. }
  195. const { common_alert_metric_details = [] } = this.panel
  196. return common_alert_metric_details.length ? common_alert_metric_details[0].field_description || {} : {}
  197. },
  198. metric () {
  199. const detail = _.get(this.panel, 'common_alert_metric_details[0]')
  200. if (!detail) {
  201. if (this.useLocalPanels) {
  202. const { fromItem, seleteItem, unit, metric, label } = this.panel.constants
  203. return {
  204. label: `${label}${metric ? `(${metric})` : ''}`,
  205. measurement: fromItem,
  206. field: seleteItem,
  207. format: unit,
  208. }
  209. }
  210. return { label: '', measurement: '', field: '', format: '0.00' }
  211. }
  212. const measurement = detail.measurement
  213. const field = detail.field
  214. const display_name = _.get(detail, 'field_description.display_name')
  215. let label = metric_zh[detail.measurement_display_name] || '-'
  216. if (display_name) {
  217. label += `(${metric_zh[display_name] ? metric_zh[display_name] : display_name})`
  218. }
  219. const unit = _.get(detail, 'field_description.unit')
  220. let format = '0.00'
  221. switch (unit) {
  222. case '%':
  223. format = '0.00 %'
  224. break
  225. case 'bps':
  226. format = '0.00 Bps'
  227. break
  228. case 'Bps':
  229. format = '0.00 b'
  230. break
  231. case 'byte':
  232. format = '0.00 b'
  233. break
  234. case 'RMB':
  235. format = '0.00'
  236. break
  237. case 'USD':
  238. format = '0.00'
  239. break
  240. case 'BRL':
  241. format = '0.00'
  242. break
  243. case 'EUR':
  244. format = '0.00'
  245. break
  246. case 'ms':
  247. format = '0.00'
  248. break
  249. case 'count':
  250. format = '0'
  251. break
  252. }
  253. return { label: label, measurement: measurement, field: field, format: format }
  254. },
  255. chartQueryData () {
  256. let params = {}
  257. if (this.useLocalPanels) {
  258. params = this.panel.queryData
  259. delete params.signature
  260. } else {
  261. const conditions = _.get(this.panel, 'setting.conditions')
  262. if (!conditions || conditions.length <= 0 || !conditions[0].query || !conditions[0].query.model) {
  263. console.log('invaild chart query condition', this.panel)
  264. return
  265. }
  266. const query = conditions[0].query
  267. const model = { ...query.model }
  268. if (model.group_by) {
  269. model.group_by = model.group_by.filter(item => {
  270. if ((item.type === 'time' && item.params && item.params[0] === '$interval') || (item.type === 'fill' && item.params && item.params[0] === 'none')) {
  271. return false
  272. }
  273. return true
  274. })
  275. }
  276. delete model.interval
  277. const metric_query = [{ model }]
  278. if (query.result_reducer) {
  279. metric_query[0].result_reducer = query.result_reducer
  280. }
  281. if (this.reducedResultOrder) {
  282. metric_query[0].result_reducer_order = this.reducedResultOrder
  283. }
  284. params = {
  285. from: query.from,
  286. to: query.to,
  287. metric_query,
  288. slimit: this.pager.limit,
  289. soffset: (this.pager.page - 1) * this.pager.limit,
  290. ...this.extraParams,
  291. }
  292. }
  293. if (this.timeGroup) {
  294. params.interval = this.timeGroup
  295. }
  296. if (this.time === 'custom') { // 自定义时间
  297. if (this.customTime && this.customTime.from && this.customTime.to) {
  298. params.from = this.customTime.from
  299. params.to = this.customTime.to
  300. }
  301. } else {
  302. params.from = this.time
  303. if (!this.isTemplate) {
  304. delete params.to
  305. }
  306. }
  307. // groupFunc 为该时间间隔内如何聚合数据,与原图表group_by不一样
  308. // if (this.groupFunc) {
  309. // const { select = [] } = model
  310. // if (select.length) {
  311. // select.forEach(s => {
  312. // const index = s.findIndex(item => ['mean', 'min', 'max', this.groupFunc].includes(item.type))
  313. // if (index !== -1) {
  314. // s[index].type = this.groupFunc
  315. // } else {
  316. // s.push({ type: this.groupFunc, params: [] })
  317. // }
  318. // })
  319. // } else {
  320. // select.push([{ type: this.groupFunc, params: [] }])
  321. // model.select = select
  322. // }
  323. // }
  324. return params
  325. },
  326. metricInfo () {
  327. const { metric_query = [] } = this.chartQueryData
  328. return metric_query.length ? metric_query[0] : {}
  329. },
  330. groupBy () {
  331. return _.get(this.metricInfo, 'model.group_by')
  332. },
  333. resultReducer () {
  334. return _.get(this.metricInfo, 'result_reducer')
  335. },
  336. isSelectFunction () {
  337. const select = _.get(this.metricInfo, 'model.select') || []
  338. let ret = ''
  339. select.map(item => {
  340. if (R.is(Array, item)) {
  341. item.map(l => {
  342. if (l.type === 'mean' || l.type === 'sum') {
  343. ret = l.type
  344. }
  345. })
  346. } else if (R.is(Object, item)) {
  347. if (item.type === 'mean' || item.type === 'sum') {
  348. ret = item.type
  349. }
  350. }
  351. })
  352. return ret
  353. },
  354. showTable () {
  355. if (!this.groupBy && !this.resultReducer && this.isSelectFunction) {
  356. return false
  357. }
  358. return true
  359. },
  360. chartSetting () {
  361. const ret = {}
  362. ret.tooltip = {
  363. trigger: 'axis',
  364. position: (point, params, dom, rect, size) => {
  365. const series = params.map((line, i) => {
  366. const unit = _.get(this.description, 'unit')
  367. const val = transformUnit(line.value[1], unit)
  368. const value = _.get(val, 'text') || line.value[1]
  369. // if (unit === 'currency') {
  370. // let unit = ''
  371. // if (currencys.length === 1) {
  372. // unit = currencyUnitMap[currencys[0]]?.sign || ''
  373. // }
  374. // value = unit ? `${unit} ${_.get(val, 'value' || line.value[0])}` : _.get(val, 'value' || line.value[0])
  375. // }
  376. // const color = i === this.highlight.index ? this.highlight.color : '#616161'
  377. let name = line.seriesName
  378. if (!this.showTable) {
  379. name = this.isSelectFunction.toUpperCase()
  380. } else {
  381. if (name.startsWith('{') && name.endsWith('}')) {
  382. try {
  383. const str = name.substring(1, name.length - 1)
  384. const list = str.split(',')
  385. const obj = {}
  386. list.forEach(item => {
  387. const [key, value] = item.split('=')
  388. obj[key] = value
  389. })
  390. let field = ''
  391. R.forEachObjIndexed((value, key) => {
  392. if (list.length && obj[key] && !field) {
  393. field = key
  394. }
  395. }, tableColumnMaps)
  396. name = field ? obj[field] : name
  397. } catch (err) { }
  398. }
  399. }
  400. return `<div class="d-flex align-items-center"><span>${line.marker}</span> <span class="text-truncate" style="max-width: 500px;">${name || ' '}</span>:&nbsp;<span>${value}</span></div>`
  401. }).join('')
  402. const wrapper = `<div>
  403. <div>${params[0].name}</div>
  404. <div class="lines-wrapper">${series}</div>
  405. </div>`
  406. dom.style.border = 'none'
  407. // dom.style.backgroundColor = 'transparent'
  408. dom.innerHTML = wrapper
  409. },
  410. }
  411. return ret
  412. },
  413. },
  414. watch: {
  415. updated_at: {
  416. deep: true,
  417. handler () {
  418. this.fetchChart()
  419. },
  420. },
  421. time () {
  422. this.fetchChart()
  423. },
  424. timeGroup () {
  425. this.fetchChart()
  426. },
  427. customTime () {
  428. this.fetchChart()
  429. },
  430. groupFunc () {
  431. this.fetchChart()
  432. },
  433. reducedResultOrder () {
  434. this.fetchChart()
  435. },
  436. },
  437. created () {
  438. this.fetchChart()
  439. },
  440. methods: {
  441. pageChange (pager) {
  442. this.pager = { ...this.pager, ...pager }
  443. this.$emit('pageChange', this.pager)
  444. this.fetchChart()
  445. },
  446. reducedResultOrderChange (order) {
  447. this.reducedResultOrder = order
  448. },
  449. resize () {
  450. this.resizeStatus = false
  451. this.$nextTick(() => {
  452. this.resizeStatus = true
  453. })
  454. },
  455. chose_panel (id, name) {
  456. this.$emit('chose_panel', { id: id, name: name })
  457. },
  458. handleActionClick ({ key }) {
  459. if (this[key]) this[key]()
  460. },
  461. handleDelete () {
  462. this.createDialog('DeleteMonitorDashboardChart', {
  463. data: [{ id: this.panel.panel_id, name: this.metric.label }],
  464. refresh: () => { this.$emit('delete') },
  465. columns: [getNameDescriptionTableColumn()],
  466. })
  467. },
  468. handleClone () {
  469. this.createDialog('CloneMonitorDashboardChart', {
  470. data: [{ id: this.panel.panel_id, name: this.metric.label, dashboard_id: this.dashboard_id }],
  471. panelName: this.panel.panel_name || (this.chart.metric && this.chart.metric.label),
  472. refresh: () => { this.$emit('delete') },
  473. columns: [getNameDescriptionTableColumn()],
  474. })
  475. },
  476. handleEdit () {
  477. this.editChart({ id: this.panel.panel_id })
  478. },
  479. handleExport () {
  480. this.$refs.monitorLine.exportTable()
  481. },
  482. toLineChartData (series) {
  483. const data = { columns: ['time'], rows: [] }
  484. const tv = {}
  485. series.map((s) => {
  486. const name = s.raw_name || s.name || ''
  487. const index = name.indexOf(':') + 1
  488. const column = index > 0 && name.slice(index).trim().length > 0 ? name.slice(index).trim() : name
  489. data.columns.push(column)
  490. s.points.map((p) => {
  491. const t = p[1]
  492. if (!tv[t]) {
  493. const padding = (n) => { return n > 9 ? `${n}` : '0' + n }
  494. const _t = new Date(t)
  495. const _h = padding(_t.getHours())
  496. const _m = padding(_t.getMinutes())
  497. tv[t] = { time: _t.toLocaleDateString() + ' ' + _h + ':' + _m }
  498. }
  499. tv[t][column] = p[0]
  500. })
  501. })
  502. // 时间排序
  503. const sortedKeys = Object.keys(tv).sort((a, b) => { return a - b })
  504. // 展开数据
  505. data.rows = sortedKeys.map((k) => {
  506. return tv[k]
  507. })
  508. return data
  509. },
  510. async fetchChart () {
  511. this.loading = true
  512. const moment = this.$moment()
  513. try {
  514. const { series, series_total = 0, reduced_result = {}, chartQueryData } = await this.fetchData()
  515. if (series) {
  516. this.series = addMissingSeries(series, chartQueryData, moment)
  517. this.pager = { ...this.pager, total: series_total }
  518. this.reducedResult = reduced_result
  519. }
  520. this.$emit('fetchDataSuccess')
  521. } catch (error) {
  522. throw error
  523. } finally {
  524. this.loading = false
  525. }
  526. },
  527. async fetchData (metric_query) {
  528. try {
  529. const data = _.cloneDeep(this.chartQueryData)
  530. if (!data) {
  531. this.resizeStatus = false
  532. return
  533. }
  534. this.resizeStatus = true
  535. data.signature = getSignature(this.chartQueryData)
  536. const { data: { series = [], series_total, reduced_result = {} } } = await new this.$Manager('unifiedmonitors', 'v1').performAction({ id: 'query', action: '', data, params: { $t: getRequestT() } })
  537. return { series, series_total, reduced_result, chartQueryData: this.chartQueryData }
  538. } catch (error) {
  539. throw error
  540. }
  541. },
  542. async exportTable (total) {
  543. try {
  544. const data = { ..._.cloneDeep(this.chartQueryData), slimit: total, soffset: 0 }
  545. data.signature = getSignature(this.chartQueryData)
  546. const { data: { series = [], series_total, reduced_result = {} } } = await new this.$Manager('unifiedmonitors', 'v1').performAction({ id: 'query', action: '', data, params: { $t: getRequestT() } })
  547. this.$refs.monitorLine.exportFullData(series, reduced_result, series_total)
  548. } catch (error) {
  549. throw error
  550. }
  551. },
  552. toggleShowTableLegend () {
  553. this.showMonitor = true
  554. this.showLegend = !this.showLegend
  555. },
  556. toggleShowMonitor () {
  557. this.showMonitor = !this.showMonitor
  558. },
  559. },
  560. }
  561. </script>
  562. <style scoped>
  563. </style>