About Dashboard

The dashboard is implemented by the module a-dashboard. This article is mainly for secondary development users to explain how to make widgets. Therefore, if you are interested in the underlying implementation mechanism, you can directly view the source code of the module a-dashboard

About Widget

Widgets are the basic units that make up the dashboard. The widgets in CabloyJS make full use of the responsive mechanism of Vue component and realize the very flexible data binding features, which can let data flow between the widgets of the dashboard

The module test-party has four built-in widgets: Fruit Sales, Fruit Sales(Line Chart), Fruit Sales(Pie Chart), Snapshots

The widget Fruit Sales(Line Chart) can obtain data from the widget Fruit Sales, and can provide data to the widget “Snapshots”, which has a connecting role. Therefore, we take the widget Fruit Sales(Line Chart) as an example to illustrate how to develop a widget

1. Create a Vue component

The widget itself is a Vue component, but it cleverly uses multiple syntax features of Vue. In order to explain it more clearly, all the source codes are pasted here before detailed explanation

src/suite-vendor/test-party/modules/test-party/front/src/kitchen-sink/components/widgets/salesLine.vue

  1. 1<template>
  2. 2 <f7-card>
  3. 3 <f7-card-header>{{$text('Fruit Sales(Line Chart)')}}</f7-card-header>
  4. 4 <f7-card-content>
  5. 5 <canvas ref="chart"></canvas>
  6. 6 <div class="error" v-if="errorMessage">{{errorMessage}}</div>
  7. 7 </f7-card-content>
  8. 8 </f7-card>
  9. 9</template>
  10. 10<script>
  11. 11const propsSchema = {
  12. 12 type: 'object',
  13. 13 properties: {
  14. 14 dataSource: {
  15. 15 type: 'object',
  16. 16 ebType: 'text',
  17. 17 ebTitle: 'Data Source',
  18. 18 ebWidget: {
  19. 19 bindOnly: true,
  20. 20 clue: 'salesDataSource',
  21. 21 },
  22. 22 },
  23. 23 fruit: {
  24. 24 type: 'string',
  25. 25 ebType: 'select',
  26. 26 ebTitle: 'Fruit',
  27. 27 ebOptions: [
  28. 28 { title: 'All', value: 'All' },
  29. 29 { title: 'Apples', value: 'Apples' },
  30. 30 { title: 'Pears', value: 'Pears' },
  31. 31 ],
  32. 32 ebOptionsBlankAuto: true,
  33. 33 ebWidget: {
  34. 34 clue: 'salesFruit',
  35. 35 },
  36. 36 },
  37. 37 },
  38. 38};
  39. 39
  40. 40const attrsSchema = {
  41. 41 type: 'object',
  42. 42 properties: {
  43. 43 snapshot: {
  44. 44 ebTitle: 'Snapshot',
  45. 45 ebWidget: {
  46. 46 clue: 'snapshot',
  47. 47 },
  48. 48 },
  49. 49 },
  50. 50};
  51. 51
  52. 52// export
  53. 53export default {
  54. 54 install,
  55. 55};
  56. 56
  57. 57// install
  58. 58function install(_Vue) {
  59. 59 const Vue = _Vue;
  60. 60 const ebDashboardWidgetBase = Vue.prototype.$meta.module.get('a-dashboard').options.mixins.ebDashboardWidgetBase;
  61. 61 return {
  62. 62 meta: {
  63. 63 widget: {
  64. 64 schema: {
  65. 65 props: propsSchema,
  66. 66 attrs: attrsSchema,
  67. 67 },
  68. 68 },
  69. 69 },
  70. 70 mixins: [ebDashboardWidgetBase],
  71. 71 props: {
  72. 72 dataSource: {
  73. 73 type: Object,
  74. 74 },
  75. 75 fruit: {
  76. 76 type: String,
  77. 77 },
  78. 78 },
  79. 79 data() {
  80. 80 return {
  81. 81 chartjs: null,
  82. 82 chart: null,
  83. 83 snapshot: null,
  84. 84 errorMessage: null,
  85. 85 };
  86. 86 },
  87. 87 watch: {
  88. 88 dataSource() {
  89. 89 this.__updateChart();
  90. 90 },
  91. 91 fruit() {
  92. 92 this.__updateChart();
  93. 93 },
  94. 94 },
  95. 95 mounted() {
  96. 96 this.__init();
  97. 97 },
  98. 98 beforeDestroy() {
  99. 99 if (this.chart) {
  100. 100 this.chart.destroy();
  101. 101 }
  102. 102 },
  103. 103 methods: {
  104. 104 __init() {
  105. 105 this.$meta.module.use('a-chartjs', module => {
  106. 106 this.chartjs = module.options.utils.chartjs;
  107. 107 this.__updateChart();
  108. 108 });
  109. 109 },
  110. 110 __prepareData() {
  111. 111 const fruitIndex = this.dataSource.cols.findIndex(item => item === this.fruit);
  112. 112 if (fruitIndex === -1) throw new Error();
  113. 113 const chartData = {
  114. 114 labels: this.dataSource.rows,
  115. 115 datasets: [{
  116. 116 fill: false,
  117. 117 backgroundColor: this.dataSource.colors[fruitIndex],
  118. 118 data: this.dataSource.dataset.map(item => item[fruitIndex]),
  119. 119 }, ],
  120. 120 };
  121. 121 return chartData;
  122. 122 },
  123. 123 __prepareOptions() {
  124. 124 const chartOptions = {
  125. 125 maintainAspectRatio: false,
  126. 126 responsive: true,
  127. 127 animation: {
  128. 128 onComplete: () => {
  129. 129 this.__createSnapshot();
  130. 130 }
  131. 131 },
  132. 132 title: {
  133. 133 display: true,
  134. 134 position: 'top',
  135. 135 text: this.fruit,
  136. 136 fontColor: 'rgba(128, 128, 128, 0.6)',
  137. 137 },
  138. 138 legend: {
  139. 139 display: false,
  140. 140 },
  141. 141 scales: {
  142. 142 xAxes: [{
  143. 143 gridLines: {
  144. 144 display: false,
  145. 145 },
  146. 146 ticks: {
  147. 147 fontColor: 'rgba(128, 128, 128, 0.6)',
  148. 148 },
  149. 149 }],
  150. 150 yAxes: [{
  151. 151 gridLines: {
  152. 152 display: true,
  153. 153 },
  154. 154 ticks: {
  155. 155 fontColor: 'rgba(128, 128, 128, 0.6)',
  156. 156 stepSize: 200,
  157. 157 },
  158. 158 }],
  159. 159 },
  160. 160 };
  161. 161 return chartOptions;
  162. 162 },
  163. 163 __createSnapshot() {
  164. 164 const image = this.chart.toBase64Image();
  165. 165 this.snapshot = {
  166. 166 title: this.$text('Fruit Sales(Line Chart)'),
  167. 167 image,
  168. 168 };
  169. 169 },
  170. 170 __clearChart() {
  171. 171 if (this.chart) {
  172. 172 this.chart.clear();
  173. 173 }
  174. 174 },
  175. 175 __updateChart() {
  176. 176 try {
  177. 177 if (!this.dataSource || !this.fruit) {
  178. 178 this.__clearChart();
  179. 179 this.errorMessage = this.$text('Please set data source');
  180. 180 return;
  181. 181 }
  182. 182 const chartData = this.__prepareData();
  183. 183 const chartOptions = this.__prepareOptions();
  184. 184 if (!this.chart) {
  185. 185 // canvas
  186. 186 const chartCanvas = this.$refs.chart.getContext('2d');
  187. 187 // fill
  188. 188 this.chart = new this.chartjs(chartCanvas, {
  189. 189 type: 'line',
  190. 190 data: chartData,
  191. 191 options: chartOptions,
  192. 192 });
  193. 193 } else {
  194. 194 this.chart.data = this.__prepareData();
  195. 195 this.chart.options = this.__prepareOptions();
  196. 196 this.chart.update();
  197. 197 }
  198. 198 this.errorMessage = null;
  199. 199 return;
  200. 200 } catch (err) {
  201. 201 this.__clearChart();
  202. 202 this.errorMessage = this.$text('There may be a binding error');
  203. 203 }
  204. 204 },
  205. 205 },
  206. 206 };
  207. 207
  208. 208}
  209. 209
  210. 210</script>
  211. 211<style lang="less" scoped>
  212. 212.error {
  213. 213 position: absolute;
  214. 214 bottom: 6px;
  215. 215 right: 6px;
  216. 216 font-size: smaller;
  217. 217}
  218. 218
  219. 219</style>

1.1 export default

First of all, we do not export Options object directly like ordinary Vue component, but export an object with the install method like Vue plugin

1.2 mixins: [ebDashboardWidgetBase]

ebDashboardWidgetBase is the base component provided by module a-dashboard. All widgets must mixin this base component

Here we can explain why we can’t export the Options object directly. Because module a-dashboard is a module loaded asynchronously, and all widgets must mixin ebDashboardWidgetBase. If export Options object directly, you will not obtain ebDashboardWidgetBase at compile time, so mixin will fail

1.3 meta

Meta is used to store the meta information of Vue component

1.3.1 meta.widget.schema.props

schema.props is the JSON Schema corresponding to the props of the widget component, which is used to render the widget’s Properties form page

About JSON Schema, Please refer to: Form Validation

Name Default Description
type Prop’s type
ebType Prop’s rendering component type
ebTitle Prop’s title
ebWidget.bindOnly false Only dynamic binding is allowed
ebWidget.clue Prop’s clue. When binding data between widgets, only the properties with the same clue are allowed to be bound, so as to simplify the selection of users and reduce the interference of irrelevant widgets and data

1.3.2 meta.widget.schema.attrs

schema.attrs is the JSON Schema corresponding to the data attributes of the widget component, which is used as data source when data binding

In other words, not all data attributes of a widget can be used as a data source. To be a data source, it must be defined in schema.attrs

1.4 watch

In order to respond to the change of data source in real time, we can use another syntax feature of Vue, watch, to watch the change of all properties in schema.props

2. Reference Vue component

In order for the system to find this Vue component, you need to reference it in the component list

src/suite-vendor/test-party/modules/test-party/front/src/components.js

  1. 1import widgetSalesLine from './kitchen-sink/components/widgets/salesLine.vue';
  2. 2
  3. 3export default {
  4. 4 widgetSalesLine,
  5. 5};

3. Definition of Function at backend

In a complex system, the problem of data authorization will be involved. We need to control the authority of widgets. Then, we need to define the function corresponding to the widget at backend so that the widget can be authorized

3.1 Definition of Function

src/suite-vendor/test-party/modules/test-party/backend/src/meta.js

  1. 1...
  2. 2widgetSalesLine: {
  3. 3 title: 'Fruit Sales(Line Chart)',
  4. 4 component: 'widgetSalesLine',
  5. 5 menu: 3,
  6. 6 public: 1,
  7. 7},
  8. 8...
Name Description
title Widget’s title
component Widget’s Vue component
menu const 3:Function’s type which means widget
public Public or not. If it is public, no authorization is required, and all users can access it

3.2 Initialization

src/suite-vendor/test-party/modules/test-party/backend/src/service/version.js

  1. 1 if (options.version === 5) {
  2. 2 // roleFunctions: widgets
  3. 3 const roleFunctions = [
  4. 4 { roleName: null, name: 'widgetSales' },
  5. 5 { roleName: null, name: 'widgetSalesLine' },
  6. 6 { roleName: null, name: 'widgetSalesPie' },
  7. 7 { roleName: null, name: 'widgetSnapshot' },
  8. 8 ];
  9. 9 await this.ctx.meta.role.addRoleFunctionBatch({ roleFunctions });
  10. 10 }

We need to add the authorization record of the widget to the database when the module data version is initialized. Because the public of the widget is true, the roleName here should be null

  1. About Module Data Version, please refer to: Module Data Version
  2. About Definition of Function, please refer to: Function & Menu
  3. About Function Authorization, please refer to: Function Authorization