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 @@
+
+
+
+
+
+ {{ bottomItem.icon }}
+
+
+ {{ bottomItem.text }}
+
+
+
+
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 @@
+
+
+
+
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;