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