@ax2/xms-dashboard-module
Advanced tools
| <template> | ||
| <WidgetTemplate | ||
| :title="title" | ||
| :loading="isLoading" | ||
| :filters="filters" | ||
| @updateFilters="updateFilters" | ||
| > | ||
| <apexchart | ||
| v-if="!isLoading && datas && isEmpty !== true" | ||
| :options="datas.options" | ||
| :series="datas.series" | ||
| :height="options.style ? '100%' : 'auto'" | ||
| /> | ||
| <p v-else-if="!isLoading && error"> | ||
| {{ error }} | ||
| </p> | ||
| <p v-else-if="!isLoading && isEmpty"> | ||
| No data to display | ||
| </p> | ||
| </WidgetTemplate> | ||
| </template> | ||
| <script> | ||
| import gql from 'graphql-tag'; | ||
| import VueApexCharts from 'vue-apexcharts'; | ||
| import { get } from 'lodash'; | ||
| import filterMixins from '../mixins/filter'; | ||
| import WidgetTemplate from './WidgetTemplate'; | ||
| export default { | ||
| name: 'ChartsArea', | ||
| components: { | ||
| 'apexchart': VueApexCharts, | ||
| WidgetTemplate, | ||
| }, | ||
| mixins: [ | ||
| filterMixins, | ||
| ], | ||
| props: { | ||
| title: { | ||
| type: String, | ||
| required: false, | ||
| default: null, | ||
| }, | ||
| options: { | ||
| type: Object, | ||
| required: true, | ||
| }, | ||
| }, | ||
| data () { | ||
| return { | ||
| show: false, | ||
| responsive: [ | ||
| { | ||
| breakpoint: 2000, | ||
| options: { | ||
| legend: { | ||
| position: "bottom" | ||
| } | ||
| } | ||
| }, | ||
| ], | ||
| error: null, | ||
| query: ` | ||
| query Query($query: String, $chartType: String, $groupByVariable: String, $labels: [String]){ | ||
| datas: chart(query: $query, chartType: $chartType, groupByVariable: $groupByVariable, labels: $labels) { | ||
| options { | ||
| chart { type }, | ||
| labels, | ||
| xaxis { categories } | ||
| } | ||
| series | ||
| } | ||
| } | ||
| `, | ||
| }; | ||
| }, | ||
| apollo: { | ||
| datas: { | ||
| query () { | ||
| return gql(this.query); | ||
| }, | ||
| variables () { | ||
| return this.gqlVariables; | ||
| }, | ||
| skip () { | ||
| return this.filtersLoading; | ||
| }, | ||
| result (data) { | ||
| if (data.error) { | ||
| this.error = data.error; | ||
| return; | ||
| } | ||
| data.data.datas.options = { ...data.data.datas.options, ...{ legend: { position: 'bottom'} } }; | ||
| return data.data.datas; | ||
| } | ||
| }, | ||
| }, | ||
| computed: { | ||
| gqlVariables () { | ||
| let filter = this.options.filter ? this.options.filter : null; | ||
| if (Object.keys(this.selectedFilters).length > 0) { | ||
| filter = filter ? `${filter},${this.getFilterValue()}` : this.getFilterValue(); | ||
| } | ||
| let query = ` | ||
| query { | ||
| datas: ${this.options.id} ${filter ? `(queryInput: { filter: "${filter}" })`: ''} { | ||
| id, | ||
| ${this.options.related} | ||
| } | ||
| } | ||
| `; | ||
| if (this.options.filterValue) { | ||
| query = query.replace(`\${${this.options.filterValue.key}}`, get(this, this.options.filterValue.value)); | ||
| } | ||
| return { | ||
| query, | ||
| chartType: this.options.type, | ||
| groupByVariable: this.options.groupBy, | ||
| labels: this.options.labels | ||
| }; | ||
| }, | ||
| isLoading () { | ||
| return this.$apolloData.queries.datas.loading; | ||
| }, | ||
| isEmpty () { | ||
| switch (this.options.type) { | ||
| case 'area': | ||
| case 'pie': | ||
| return get(this.datas, 'options.xaxis.categories', []).length === 0 && get(this.datas, 'series[0].data', []).length === 0; | ||
| } | ||
| return false; | ||
| } | ||
| }, | ||
| beforeDestroy () { | ||
| this.$apollo.queries.datas.stop(); | ||
| }, | ||
| }; | ||
| </script> |
| <template> | ||
| <WidgetTemplate | ||
| :title="title" | ||
| :loading="$apolloData.queries.datas.loading" | ||
| :filters="filters" | ||
| @updateFilters="updateFilters" | ||
| > | ||
| <v-data-table | ||
| v-if="!$apolloData.queries.datas.loading" | ||
| :headers="headers" | ||
| :items="datas" | ||
| :items-per-page="5" | ||
| class="elevation-1" | ||
| /> | ||
| </WidgetTemplate> | ||
| </template> | ||
| <script> | ||
| import gql from 'graphql-tag'; | ||
| import filterMixins from '../mixins/filter'; | ||
| import WidgetTemplate from './WidgetTemplate'; | ||
| export default { | ||
| name: 'DataTable', | ||
| components: { | ||
| WidgetTemplate, | ||
| }, | ||
| mixins: [ | ||
| filterMixins, | ||
| ], | ||
| props: { | ||
| title: { | ||
| type: String, | ||
| required: false, | ||
| default: null, | ||
| }, | ||
| entity: { | ||
| type: String, | ||
| required: false, | ||
| default: null, | ||
| }, | ||
| query: { | ||
| type: String, | ||
| required: true, | ||
| }, | ||
| }, | ||
| data () { | ||
| return { | ||
| show: false, | ||
| }; | ||
| }, | ||
| apollo: { | ||
| datas: { | ||
| query () { | ||
| return gql(`query { datas: ${this.formatedQuery} }`); | ||
| }, | ||
| variables () { | ||
| return this.gqlVariables; | ||
| }, | ||
| skip () { | ||
| return this.filtersLoading; | ||
| }, | ||
| }, | ||
| }, | ||
| computed: { | ||
| formatedQuery () { | ||
| return this.query.replaceAll("${filter}", this.getFilterValue()); | ||
| }, | ||
| headers () { | ||
| return Object.keys(this.datas[0]).map(header => { | ||
| return { | ||
| text: this.entity && this.$t(`${this.entity}.${header}`) !== `${this.entity}.${header}` ? this.$t(`${this.entity}.${header}`) : this.$t(header), | ||
| value: header | ||
| }; | ||
| }); | ||
| }, | ||
| }, | ||
| beforeDestroy () { | ||
| this.$apollo.queries.datas.stop(); | ||
| }, | ||
| }; | ||
| </script> |
| <template> | ||
| <v-card> | ||
| <v-card-title v-t="title" /> | ||
| <v-card-text> | ||
| <filters-form | ||
| v-if="filtersWithOptions.length > 0" | ||
| :items="filtersWithOptions" | ||
| :value="selectedFilters" | ||
| :search="false" | ||
| :type-name="'typeName'" | ||
| class="mb-4" | ||
| @update:value="$emit('updateFilters', $event)" | ||
| /> | ||
| <slot v-if="!loading"/> | ||
| <!-- Loading spinner --> | ||
| <BaseSpinner v-else /> | ||
| </v-card-text> | ||
| </v-card> | ||
| </template> | ||
| <script> | ||
| import filterMixins from '../mixins/filter'; | ||
| export default { | ||
| name: 'WidgetTemplate', | ||
| mixins: [ | ||
| filterMixins, | ||
| ], | ||
| props: { | ||
| title: { | ||
| type: String, | ||
| required: false, | ||
| default: null, | ||
| }, | ||
| loading: { | ||
| type: Boolean, | ||
| required: false, | ||
| default: true, | ||
| }, | ||
| }, | ||
| data () { | ||
| return { | ||
| show: false, | ||
| }; | ||
| }, | ||
| }; | ||
| </script> |
| import gql from 'graphql-tag'; | ||
| export default { | ||
| props: { | ||
| filters: { | ||
| type: Array, | ||
| required: false, | ||
| default: () => [], | ||
| } | ||
| }, | ||
| data () { | ||
| return { | ||
| selectedFilters: {}, | ||
| filtersLoading: true, | ||
| }; | ||
| }, | ||
| apollo: { | ||
| filtersOptions: { | ||
| query () { | ||
| return this.createFiltersOptionsQuery(this.filters); | ||
| }, | ||
| skip () { | ||
| // Skip query if no gql query is returned | ||
| return !this.createFiltersOptionsQuery(this.filters); | ||
| }, | ||
| update (data) { | ||
| return data; | ||
| } | ||
| }, | ||
| }, | ||
| created () { | ||
| for (const filter of this.filters) { | ||
| if (filter.defaultValue !== null || filter.defaultValue !== undefined) { | ||
| this.updateFilters({ key: filter.id, value: filter.defaultValue }); | ||
| } | ||
| } | ||
| this.filtersLoading = false; | ||
| }, | ||
| methods: { | ||
| updateFilters ({ key, value }) { | ||
| const newFilter = {}; | ||
| newFilter[key] = value; | ||
| this.selectedFilters[key] = value; | ||
| this.selectedFilters = { | ||
| ... this.selectedFilters, | ||
| ...newFilter | ||
| }; | ||
| }, | ||
| getFilterValue () { | ||
| // Build array of all filters selected by user | ||
| const selectedFiltersArray = Object.keys(this.selectedFilters) | ||
| .map(selectedFilterId => { | ||
| const filterValue = this.selectedFilters[selectedFilterId]; | ||
| const filterConfig = this.filters.find(filter => filter.id === selectedFilterId); | ||
| if (selectedFilterId === 'dates' && this.selectedFilters[selectedFilterId].length > 0) { | ||
| // Handle filter dates with range values | ||
| let result = `${filterConfig.whereClause[0]}:${this.selectedFilters[selectedFilterId][0]}`; | ||
| if (this.selectedFilters[selectedFilterId][1]) { | ||
| result = `${result},${filterConfig.whereClause[1]}:${this.selectedFilters[selectedFilterId][1]} 23-59-59`; | ||
| } | ||
| return result; | ||
| } else if (typeof filterValue === 'object') { | ||
| // Handle multiple filter values | ||
| const { clauseSeparator } = filterConfig; | ||
| const separator = clauseSeparator !== undefined ? clauseSeparator : '(&&)'; | ||
| const concatValues = filterValue.map(item => { | ||
| return item.value; | ||
| }).join(separator); | ||
| return `${filterConfig.whereClause}:${concatValues}`; | ||
| } | ||
| if (filterValue === undefined || filterConfig == null) { | ||
| return null; | ||
| } | ||
| return `${filterConfig.whereClause}:${filterValue}`; | ||
| }) | ||
| .filter(filterItem => filterItem !== null); | ||
| return selectedFiltersArray.filter(filter => filter).join(','); | ||
| }, | ||
| createFiltersOptionsQuery (filters = []) { | ||
| // Get only filters that require options | ||
| const requestFilters = filters.filter(filter => { | ||
| // Won't request filters that have static options set | ||
| return filter.type === 'select' && !filter.options; | ||
| }); | ||
| if (!requestFilters.length) { | ||
| return false; | ||
| } | ||
| // Create a query for every one | ||
| const queries = requestFilters.map(filter => { | ||
| return `${filter.id}: filter (key: "${filter.id}") { | ||
| value: key, | ||
| text: value, | ||
| }`; | ||
| }); | ||
| return gql`{ ${queries.join(',')} }`; | ||
| }, | ||
| }, | ||
| computed: { | ||
| filtersWithOptions () { | ||
| /** | ||
| * Translate option function | ||
| */ | ||
| const translateOption = option => ({ | ||
| value: option.value, | ||
| text: this.$t(option.text) | ||
| }); | ||
| if (!Array.isArray(this.filters)) return []; | ||
| return this.filters.map(filterConfig => { | ||
| const filter = { ...filterConfig }; | ||
| // Fetched options take priority over static options | ||
| if (this.filtersOptions && this.filtersOptions[filter.id]) { | ||
| filter.options = this.filtersOptions[filter.id].map(translateOption); | ||
| } else if (filter.options) { | ||
| filter.options = filter.options.map(translateOption); | ||
| } | ||
| return filter; | ||
| }); | ||
| }, | ||
| }, | ||
| }; |
| import Vue from 'vue'; | ||
| const ChartsArea = () => import('../components/ChartsArea'); | ||
| function installGlobalComponents (Vue) { | ||
| Vue.component('ChartsArea', ChartsArea); | ||
| } | ||
| export default function useGlobalComponents () { | ||
| Vue.use(installGlobalComponents); | ||
| } |
+19
-0
@@ -6,2 +6,21 @@ # Change Log | ||
| # [0.4.0](https://github.com/ax2inc/xms-modules/compare/@ax2/xms-dashboard-module@0.3.1...@ax2/xms-dashboard-module@0.4.0) (2022-09-19) | ||
| ### Bug Fixes | ||
| * Fix fetch select ([71af129](https://github.com/ax2inc/xms-modules/commit/71af129d15279907aad5a6bb13df83847a45b22c)) | ||
| * Wait default filter value to be set before execute query ([35b0c8e](https://github.com/ax2inc/xms-modules/commit/35b0c8e8ca70452f89178924908309e15a3e3a23)) | ||
| ### Features | ||
| * Add dashboard widget ([5688db1](https://github.com/ax2inc/xms-modules/commit/5688db1159992c70f7561608ffa573e99d713235)) | ||
| * Add default value to filters ([b054179](https://github.com/ax2inc/xms-modules/commit/b05417955c488bbe408cda93bf90fd4e8b79218a)) | ||
| * Add dynamic charts ([8659dbe](https://github.com/ax2inc/xms-modules/commit/8659dbee9592fedf06abcba60305ef11e7f60b4a)) | ||
| ## [0.3.1](https://github.com/ax2inc/xms-modules/compare/@ax2/xms-dashboard-module@0.3.0...@ax2/xms-dashboard-module@0.3.1) (2022-02-24) | ||
@@ -8,0 +27,0 @@ |
+2
-2
| { | ||
| "name": "@ax2/xms-dashboard-module", | ||
| "version": "0.3.1", | ||
| "version": "0.4.0", | ||
| "main": "src/main.js", | ||
@@ -43,3 +43,3 @@ "publishConfig": { | ||
| }, | ||
| "gitHead": "6f0ad152c0af0c38bde1f8b94abd526ff363bd83" | ||
| "gitHead": "b879b2b253456e82092d9b442549f16718c50621" | ||
| } |
@@ -77,3 +77,6 @@ <template> | ||
| }, | ||
| destroyed () { | ||
| this.$_apollo.queries.details.stop(); | ||
| } | ||
| }; | ||
| </script> |
+2
-0
@@ -5,2 +5,3 @@ import useConfig from './plugins/config'; | ||
| import registerRoutes from './router'; | ||
| import useGlobalComponents from './plugins/global-components'; | ||
@@ -11,3 +12,4 @@ export default function loadModule (context) { | ||
| useI18n(context); | ||
| useGlobalComponents(context); | ||
| registerRoutes(context); | ||
| }; |
@@ -23,2 +23,4 @@ <template> | ||
| import Charts from '../components/Charts'; | ||
| import ChartsArea from '../components/ChartsArea'; | ||
| import DataTable from '../components/DataTable'; | ||
@@ -31,2 +33,4 @@ export default { | ||
| Charts, | ||
| ChartsArea, | ||
| DataTable, | ||
| }, | ||
@@ -33,0 +37,0 @@ computed: { |
21268
112.98%24
26.32%238
118.35%