diff --git a/raw-cms-app/src/config/router.js b/raw-cms-app/src/config/router.js index 84c3edd0..dd392daf 100644 --- a/raw-cms-app/src/config/router.js +++ b/raw-cms-app/src/config/router.js @@ -209,6 +209,14 @@ const _router = new VueRouter({ }, ], }, + { + path: '/about', + name: 'about', + component: async (res, rej) => { + const cmp = await import('/modules/core/views/about-view/about-view.js'); + await cmp.default(res, rej); + }, + }, ], }); diff --git a/raw-cms-app/src/config/vue-chartjs.js b/raw-cms-app/src/config/vue-chartjs.js new file mode 100644 index 00000000..1c7976a5 --- /dev/null +++ b/raw-cms-app/src/config/vue-chartjs.js @@ -0,0 +1,5 @@ +const _vueChartJs = window.VueChartJs; + +export const vueChartJs = _vueChartJs; +export const mixins = _vueChartJs.mixins; +export const Pie = _vueChartJs.Pie; diff --git a/raw-cms-app/src/config/vuetify.js b/raw-cms-app/src/config/vuetify.js index 3c3878cc..5179dec8 100644 --- a/raw-cms-app/src/config/vuetify.js +++ b/raw-cms-app/src/config/vuetify.js @@ -13,3 +13,4 @@ const _vuetify = new Vuetify({ }); export const vuetify = _vuetify; +export const vuetifyColors = colors; diff --git a/raw-cms-app/src/index.html b/raw-cms-app/src/index.html index bc6324e9..6a284794 100644 --- a/raw-cms-app/src/index.html +++ b/raw-cms-app/src/index.html @@ -67,6 +67,10 @@ + + + + diff --git a/raw-cms-app/src/modules/core/assets/i18n/i18n.en.json b/raw-cms-app/src/modules/core/assets/i18n/i18n.en.json index c1258b5a..73fea83c 100644 --- a/raw-cms-app/src/modules/core/assets/i18n/i18n.en.json +++ b/raw-cms-app/src/modules/core/assets/i18n/i18n.en.json @@ -49,7 +49,12 @@ "title": "Entities" }, "home": { - "helpText": "Welcome! Please select a menu entry on the left." + "apiCallsText": "API calls (last week):", + "entitiesNumText": "Entities number", + "recordsQuotaText": "Records quota", + "title": "Home", + "totalRecordsText": "Total records", + "welcomeText": "Welcome back! Here you can find some insights about your app." }, "lambdas": { "deleteConfirmMsgTpl": "Are you sure you want to delete lambda {name}?", diff --git a/raw-cms-app/src/modules/core/assets/rawlogo.png b/raw-cms-app/src/modules/core/assets/rawlogo.png index 1179126b..1193bce1 100644 Binary files a/raw-cms-app/src/modules/core/assets/rawlogo.png and b/raw-cms-app/src/modules/core/assets/rawlogo.png differ diff --git a/raw-cms-app/src/modules/core/assets/rawlogo_small.png b/raw-cms-app/src/modules/core/assets/rawlogo_small.png new file mode 100644 index 00000000..90ecdfa4 Binary files /dev/null and b/raw-cms-app/src/modules/core/assets/rawlogo_small.png differ diff --git a/raw-cms-app/src/modules/core/components/dashboard/dashboard.js b/raw-cms-app/src/modules/core/components/dashboard/dashboard.js new file mode 100644 index 00000000..10e791ef --- /dev/null +++ b/raw-cms-app/src/modules/core/components/dashboard/dashboard.js @@ -0,0 +1,63 @@ +import { optionalChain } from '../../../../utils/object.utils.js'; +import { SimplePieChart } from '../../../shared/components/charts/simple-pie-chart/simple-pie-chart.js'; +import { dashboardService } from '../../services/dashboard.service.js'; + +const _DashboardDef = async () => { + const tpl = await RawCMS.loadComponentTpl( + '/modules/core/components/dashboard/dashboard.tpl.html' + ); + + return { + components: { + PieChart: SimplePieChart, + }, + computed: { + totalRecordsNum: function() { + const quotasObj = optionalChain(() => this.info.recordQuotas); + if (quotasObj === undefined) { + return undefined; + } + + return Object.keys(quotasObj) + .map(x => quotasObj[x]) + .reduce((acc, v) => acc + v, 0); + }, + recordQuotasChartData: function() { + const quotasObj = optionalChain(() => this.info.recordQuotas, { fallbackValue: {} }); + const labels = []; + const data = []; + Object.keys(quotasObj).forEach(x => { + labels.push(x); + data.push(quotasObj[x]); + }); + + return { data, labels }; + }, + }, + created: async function() { + this.info = await this.dashboardService.getDashboardInfo(); + this.isLoading = false; + }, + data: function() { + return { + chartOptions: { + lowerIsBetter: true, + }, + dashboardService: dashboardService, + isLoading: true, + info: undefined, + optionalChain: optionalChain, + }; + }, + template: tpl, + }; +}; + +const _Dashboard = async (res, rej) => { + const cmpDef = await _DashboardDef(); + res(cmpDef); +}; + +export const DashboardDef = _DashboardDef; +export const Dashboard = _Dashboard; +export default _Dashboard; diff --git a/raw-cms-app/src/modules/core/components/dashboard/dashboard.tpl.html b/raw-cms-app/src/modules/core/components/dashboard/dashboard.tpl.html new file mode 100644 index 00000000..dd81d418 --- /dev/null +++ b/raw-cms-app/src/modules/core/components/dashboard/dashboard.tpl.html @@ -0,0 +1,30 @@ + + + + + {{ $t('core.home.totalRecordsText') }} + + + {{ totalRecordsNum }} + + + + + {{ $t('core.home.entitiesNumText') }} + + + {{ optionalChain(() => this.info.entitiesNum) }} + + + + + + + {{ $t('core.home.recordsQuotaText') }} + + + + + + + diff --git a/raw-cms-app/src/modules/core/components/left-menu/left-menu.js b/raw-cms-app/src/modules/core/components/left-menu/left-menu.js index a12f45ee..93482d99 100644 --- a/raw-cms-app/src/modules/core/components/left-menu/left-menu.js +++ b/raw-cms-app/src/modules/core/components/left-menu/left-menu.js @@ -24,6 +24,7 @@ const _LeftMenu = async (resolve, reject) => { isVisible: false, isUserMenuVisible: false, items: [ + { icon: 'mdi-home', text: 'Home', route: 'home' }, { icon: 'mdi-account', text: 'Users', route: 'users' }, { icon: 'mdi-cube', text: 'Entities', route: 'entities' }, { icon: 'mdi-book-open', text: 'Collections', route: 'collections' }, @@ -35,6 +36,7 @@ const _LeftMenu = async (resolve, reject) => { extLink: RawCMS.env.api.baseUrl, }, ], + bottomItem: { icon: 'mdi-information', text: 'About', route: 'about' }, }; }, methods: { diff --git a/raw-cms-app/src/modules/core/components/left-menu/left-menu.tpl.html b/raw-cms-app/src/modules/core/components/left-menu/left-menu.tpl.html index 96f005ec..f3063454 100644 --- a/raw-cms-app/src/modules/core/components/left-menu/left-menu.tpl.html +++ b/raw-cms-app/src/modules/core/components/left-menu/left-menu.tpl.html @@ -103,4 +103,20 @@ + + diff --git a/raw-cms-app/src/modules/core/components/top-bar/top-bar.tpl.html b/raw-cms-app/src/modules/core/components/top-bar/top-bar.tpl.html index f3b0921e..74b04867 100644 --- a/raw-cms-app/src/modules/core/components/top-bar/top-bar.tpl.html +++ b/raw-cms-app/src/modules/core/components/top-bar/top-bar.tpl.html @@ -5,4 +5,8 @@ +
+ + RawCMS Logo + diff --git a/raw-cms-app/src/modules/core/services/dashboard.service.js b/raw-cms-app/src/modules/core/services/dashboard.service.js new file mode 100644 index 00000000..f0d0ce14 --- /dev/null +++ b/raw-cms-app/src/modules/core/services/dashboard.service.js @@ -0,0 +1,32 @@ +import { sleep } from '../../../utils/time.utils.js'; +import { BaseApiService } from '../../shared/services/base-api-service.js'; + +class DashboardService extends BaseApiService { + constructor() { + super(); + } + + async getDashboardInfo() { + // FIXME: For now we use mock data + + await sleep(5000); + + const quota = { + TEST: Math.floor(Math.random() * 100), + Items1: Math.floor(Math.random() * 100), + Items2: Math.floor(Math.random() * 100), + Items3: Math.floor(Math.random() * 100), + Items4: Math.floor(Math.random() * 100), + Items5: Math.floor(Math.random() * 100), + Items6: Math.floor(Math.random() * 100), + }; + return { + recordQuotas: quota, + entitiesNum: Object.keys(quota).length, + lastWeekCallsNum: Math.floor(Math.random() * 500), + }; + } +} + +export const dashboardService = new DashboardService(); +export default dashboardService; diff --git a/raw-cms-app/src/modules/core/views/about-view/about-view.js b/raw-cms-app/src/modules/core/views/about-view/about-view.js new file mode 100644 index 00000000..c5389861 --- /dev/null +++ b/raw-cms-app/src/modules/core/views/about-view/about-view.js @@ -0,0 +1,10 @@ +const _AboutView = async (res, rej) => { + const tpl = await RawCMS.loadComponentTpl('/modules/core/views/about-view/about-view.tpl.html'); + + res({ + template: tpl, + }); +}; + +export const AboutView = _AboutView; +export default _AboutView; diff --git a/raw-cms-app/src/modules/core/views/about-view/about-view.tpl.html b/raw-cms-app/src/modules/core/views/about-view/about-view.tpl.html new file mode 100644 index 00000000..8eb379c6 --- /dev/null +++ b/raw-cms-app/src/modules/core/views/about-view/about-view.tpl.html @@ -0,0 +1,5 @@ + + + + + diff --git a/raw-cms-app/src/modules/core/views/home-view/home-view.js b/raw-cms-app/src/modules/core/views/home-view/home-view.js index 6cd7427d..c35e9b33 100644 --- a/raw-cms-app/src/modules/core/views/home-view/home-view.js +++ b/raw-cms-app/src/modules/core/views/home-view/home-view.js @@ -1,7 +1,17 @@ +import { vuexStore } from '../../../../config/vuex.js'; +import { DashboardDef } from '../../components/dashboard/dashboard.js'; + const _HomeView = async (res, rej) => { const tpl = await RawCMS.loadComponentTpl('/modules/core/views/home-view/home-view.tpl.html'); + const dashboardDef = await DashboardDef(); res({ + components: { + Dashboard: dashboardDef, + }, + mounted() { + vuexStore.dispatch('core/updateTopBarTitle', this.$t('core.home.title')); + }, template: tpl, }); }; diff --git a/raw-cms-app/src/modules/core/views/home-view/home-view.tpl.html b/raw-cms-app/src/modules/core/views/home-view/home-view.tpl.html index 8eb379c6..3d1c0820 100644 --- a/raw-cms-app/src/modules/core/views/home-view/home-view.tpl.html +++ b/raw-cms-app/src/modules/core/views/home-view/home-view.tpl.html @@ -1,5 +1,8 @@ - - - + + + + {{ $t('core.home.welcomeText') }} + + diff --git a/raw-cms-app/src/modules/shared/components/charts/charts.utils.js b/raw-cms-app/src/modules/shared/components/charts/charts.utils.js new file mode 100644 index 00000000..01390574 --- /dev/null +++ b/raw-cms-app/src/modules/shared/components/charts/charts.utils.js @@ -0,0 +1,26 @@ +import { vuetifyColors } from '../../../../config/vuetify.js'; +import { optionalChain } from '../../../../utils/object.utils.js'; + +const _transparentize = function(color, opacity) { + const alpha = opacity === undefined ? 0.5 : 1 - opacity; + return Color(color) + .alpha(alpha) + .rgbString(); +}; + +const _colorize = function(value, { range = [0, 100], lowerIsBetter = false } = {}) { + const min = optionalChain(() => range[0], 0); + const max = optionalChain(() => range[1], 100); + let colors = [vuetifyColors.red.darken4, vuetifyColors.orange.base, vuetifyColors.green.base]; + if (lowerIsBetter) { + colors = colors.reverse(); + } + const colorMap = d3.piecewise(d3.interpolate, colors); + const interpolationValue = (value - min) / (max - min); + + const c = colorMap(interpolationValue); + return c; +}; + +export const colorize = _colorize; +export const transparentize = _transparentize; diff --git a/raw-cms-app/src/modules/shared/components/charts/simple-pie-chart/simple-pie-chart.js b/raw-cms-app/src/modules/shared/components/charts/simple-pie-chart/simple-pie-chart.js new file mode 100644 index 00000000..efa60814 --- /dev/null +++ b/raw-cms-app/src/modules/shared/components/charts/simple-pie-chart/simple-pie-chart.js @@ -0,0 +1,82 @@ +import { Pie } from '../../../../../config/vue-chartjs.js'; +import { optionalChain } from '../../../../../utils/object.utils.js'; +import { colorize, transparentize } from '../charts.utils.js'; + +const _defaultChartOptions = { + lowerIsBetter: false, +}; + +const _SimplePieChart = { + computed: { + chartData: function() { + const data = this.sortedData; + const backColors = data.map(x => this.normalColorize(x)); + + const res = { + datasets: [ + { + data: data, + backgroundColor: backColors, + hoverColor: backColors.map(x => this.hoverColorize(x)), + }, + ], + labels: this.context.labels, + }; + + return res; + }, + sortedData: function() { + let data = optionalChain(() => [...this.context.data], { fallbackValue: [] }).sort( + (a, b) => a - b + ); + + if (this.options.lowerIsBetter) { + data = data.reverse(); + } + + return data; + }, + }, + extends: Pie, + methods: { + normalColorize: function(value) { + const data = this.sortedData; + const min = Math.min(...data) || 0; + const max = Math.max(...data) || 100; + return colorize(value, { range: [min, max], lowerIsBetter: this.options.lowerIsBetter }); + }, + hoverColorize: function(color) { + return transparentize(color); + }, + refresh: function() { + this.styles = { width: '100%', height: '100%', position: 'relative', ...this.styles }; + this.renderChart(this.chartData, { + maintainAspectRatio: false, + }); + }, + }, + mounted() { + this.refresh(); + }, + props: { + context: { + type: Object, + default: { + data: [], + labels: [], + }, + }, + options: { + type: Object, + default: _defaultChartOptions, + }, + }, + watch: { + context: function() { + this.refresh(); + }, + }, +}; + +export const SimplePieChart = _SimplePieChart; +export default _SimplePieChart; diff --git a/raw-cms-app/src/modules/shared/services/base-api-service.js b/raw-cms-app/src/modules/shared/services/base-api-service.js new file mode 100644 index 00000000..0c2c5d5a --- /dev/null +++ b/raw-cms-app/src/modules/shared/services/base-api-service.js @@ -0,0 +1,17 @@ +import { apiClient } from '../../core/api/api-client.js'; + +export class BaseApiService { + _apiClient; + + constructor() { + this._apiClient = apiClient; + } + + _checkGenericError(axiosRes) { + if (axiosRes.status !== 200) { + return false; + } + + return true; + } +} diff --git a/raw-cms-app/src/modules/shared/services/base-crud-service.js b/raw-cms-app/src/modules/shared/services/base-crud-service.js index 672db777..4a339a89 100644 --- a/raw-cms-app/src/modules/shared/services/base-crud-service.js +++ b/raw-cms-app/src/modules/shared/services/base-crud-service.js @@ -1,14 +1,13 @@ +import { mix } from '../../../utils/inheritance.utils.js'; import { optionalChain } from '../../../utils/object.utils.js'; -import { apiClient } from '../../core/api/api-client.js'; +import { BaseApiService } from './base-api-service.js'; import { ICrudService } from './crud-service.js'; -export class BaseCrudService extends ICrudService { - _apiClient; +export class BaseCrudService extends mix(BaseApiService, ICrudService) { _basePath; constructor({ basePath }) { super(); - this._apiClient = apiClient; this._basePath = basePath; } @@ -77,12 +76,4 @@ export class BaseCrudService extends ICrudService { const res = await this._apiClient.delete(`${this._basePath}/${id}`); return this._checkGenericError(res); } - - _checkGenericError(axiosRes) { - if (axiosRes.status !== 200) { - return false; - } - - return true; - } } diff --git a/raw-cms-app/src/modules/shared/services/crud-service.js b/raw-cms-app/src/modules/shared/services/crud-service.js index 6fb7ca53..93cafe69 100644 --- a/raw-cms-app/src/modules/shared/services/crud-service.js +++ b/raw-cms-app/src/modules/shared/services/crud-service.js @@ -1,12 +1,5 @@ -import { checkAbstractImplementation } from '../../../utils/inheritance.js'; - export class ICrudService { - constructor() { - checkAbstractImplementation({ - baseClazz: ICrudService, - targetClazz: new.target, - }); - } + constructor() {} async getAll() { throw new Error(`Please Provide an implementation for ${this.getAll.name}`); diff --git a/raw-cms-app/src/styles.css b/raw-cms-app/src/styles.css index 1dc8f2b4..d43bd468 100644 --- a/raw-cms-app/src/styles.css +++ b/raw-cms-app/src/styles.css @@ -3,6 +3,7 @@ fieldset { width: 100%; } +/* FIXME: Replace these with Vuetify spacing utils */ .add-padding.add-padding--left { padding-left: 12px; } diff --git a/raw-cms-app/src/utils/inheritance.js b/raw-cms-app/src/utils/inheritance.js deleted file mode 100644 index f16b45a1..00000000 --- a/raw-cms-app/src/utils/inheritance.js +++ /dev/null @@ -1,11 +0,0 @@ -const _checkAbstractImplementation = ({ baseClazz, targetClazz }) => { - if (baseClazz === undefined || targetClazz === baseClazz || self === undefined) { - throw new ArgumentError('You must specify: baseClazz, targetClazz (usually `new.target`)!'); - } - - if (targetClazz === baseClazz) { - throw new TypeError(`Cannot construct ${targetClazz.name} instances directly.`); - } -}; - -export const checkAbstractImplementation = _checkAbstractImplementation; diff --git a/raw-cms-app/src/utils/inheritance.utils.js b/raw-cms-app/src/utils/inheritance.utils.js new file mode 100644 index 00000000..c0499662 --- /dev/null +++ b/raw-cms-app/src/utils/inheritance.utils.js @@ -0,0 +1,33 @@ +const _mix = (baseClass, ...mixins) => { + class base extends baseClass { + constructor(...args) { + super(...args); + mixins.forEach(mixin => { + copyProps(this, new mixin()); + }); + } + } + + // this function copies all properties and symbols, filtering out some special ones + const copyProps = (target, source) => { + Object.getOwnPropertyNames(source) + .concat(Object.getOwnPropertySymbols(source)) + .forEach(prop => { + if ( + !prop.match( + /^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/ + ) + ) + Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop)); + }); + }; + + // outside contructor() to allow aggregation(A,B,C).staticFunction() to be called etc. + mixins.forEach(mixin => { + copyProps(base.prototype, mixin.prototype); + copyProps(base, mixin); + }); + return base; +}; + +export const mix = _mix;