diff --git a/debian/wb-mqtt-homeui.postinst b/debian/wb-mqtt-homeui.postinst index a4e19782a..92762deeb 100644 --- a/debian/wb-mqtt-homeui.postinst +++ b/debian/wb-mqtt-homeui.postinst @@ -22,12 +22,19 @@ else fi BOARD_CONF="/usr/share/wb-mqtt-homeui/config.${CONF_SUFFIX}.json" CONFFILE="/etc/wb-webui.conf" +TEMPLATES_CONF="/usr/share/wb-mqtt-homeui/templatesConfig.default.json" +TEMPLATES_CONFFILE="/etc/wb-templates.conf" function install_default_config() { ucf --debconf-ok $BOARD_CONF $CONFFILE } +function install_default_templates_config() +{ + ucf --debconf-ok $TEMPLATES_CONF $TEMPLATES_CONFFILE +} + function is_config_untouched() { for fname in $(ls /usr/share/wb-mqtt-homeui/config.*.json); do @@ -53,6 +60,10 @@ case "$1" in install_default_config fi fi + + if [ ! -e "$TEMPLATES_CONFFILE" ]; then + install_default_templates_config + fi ;; esac diff --git a/frontend/Makefile b/frontend/Makefile index cb5a56c18..e1bcd536a 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -23,6 +23,7 @@ configs: minify configs/$$wb.svg > dist/configs/$$wb.svg; \ j2 configs/config.$$wb.json.jinja > dist/configs/config.$$wb.json; \ done + cp -a configs/templatesConfig.default.json dist/configs install: configs install -d -m 0777 $(DESTDIR)/var/www/css @@ -46,6 +47,7 @@ install: configs install -Dm0644 dist/configs/*.json -t $(DESTDIR)/usr/share/wb-mqtt-homeui install -Dm0644 webui.schema.json -t $(DESTDIR)/usr/share/wb-mqtt-confed/schemas + install -Dm0644 templates.schema.json -t $(DESTDIR)/usr/share/wb-mqtt-confed/schemas install -Dm0644 wb-configs.rules $(DESTDIR)/etc/wb-configs.d/20wb-mqtt-homeui uninstall: diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 3679f7512..3fc9f961b 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -49,6 +49,7 @@ import dumbTemplateModule from './services/dumbtemplate'; import pageStateService from './services/pagestate'; import deviceDataService from './services/devicedata'; import uiConfigService from './services/uiconfig'; +import templatesConfigService from './services/templatesConfig'; import hiliteService from './services/hilite'; import userAgentFactory from './services/userAgent.factory'; import rolesFactoryService from './services/roles.factory'; @@ -141,7 +142,8 @@ const module = angular .value('historyMaxPoints', 1000) .value('logsMaxRows', 50) .value('webuiConfigPath', '/etc/wb-webui.conf') - .value('configSaveDebounceMs', 300); + .value('configSaveDebounceMs', 300) + .value('templatesConfigPath', '/etc/wb-templates.conf'); // Register services module @@ -174,6 +176,7 @@ module // make sure DeviceData is loaded at the startup so no MQTT messages are missed }) .factory('uiConfig', uiConfigService) + .factory('templatesConfig', templatesConfigService) .filter('hilite', hiliteService); // Register controllers @@ -381,9 +384,11 @@ const realApp = angular mqttClient, ConfigEditorProxy, webuiConfigPath, + templatesConfigPath, errors, whenMqttReady, uiConfig, + templatesConfig, $timeout, configSaveDebounceMs, ngToast, @@ -417,9 +422,11 @@ const realApp = angular mqttClient, ConfigEditorProxy, webuiConfigPath, + templatesConfigPath, errors, whenMqttReady, - uiConfig + uiConfig, + templatesConfig ) { return function (loginData) { if (loginData.url) { @@ -443,6 +450,17 @@ const realApp = angular }) .catch(errors.catch('app.errors.load')); + // Try to obtain Templates configs + whenMqttReady() + .then(() => { + return ConfigEditorProxy.Load({ path: templatesConfigPath }); + }) + .then(result => { + console.log('LOAD TEMPLATES CONF: %o', result.content); + templatesConfig.ready(result.content); + }) + .catch(errors.catch('app.errors.load')); + return true; //..................................................................... @@ -461,9 +479,11 @@ const realApp = angular mqttClient, ConfigEditorProxy, webuiConfigPath, + templatesConfigPath, errors, whenMqttReady, - uiConfig + uiConfig, + templatesConfig ); //......................................................................... @@ -492,6 +512,7 @@ const realApp = angular // TBD: the following should be handled by config sync service var configSaveDebounce = null; + var templatesConfigSaveDebounce = null; var firstBootstrap = true; // Watch for WebUI config changes @@ -527,6 +548,39 @@ const realApp = angular true ); + // Watch for templates config changes + $rootScope.$watch( + () => templatesConfig.data, + (newData, oldData) => { + if (angular.equals(newData, oldData)) { + return; + } + if (firstBootstrap) { + firstBootstrap = false; + return; + } + console.log('new templates data: %o', newData); + if (templatesConfigSaveDebounce) { + $timeout.cancel(templatesConfigSaveDebounce); + } + templatesConfigSaveDebounce = $timeout(() => { + errors.hideError(); + ConfigEditorProxy.Save({ path: templatesConfigPath, content: newData }) + .then(() => { + console.log('templates config saved'); + }) + .catch(err => { + if (err.name === 'QuotaExceededError') { + errors.showError('app.errors.overflow'); + } else { + errors.showError('app.errors.save', err); + } + }); + }, configSaveDebounceMs); + }, + true + ); + whenMqttReady() .then(() => { if (rolesFactory.checkRights(rolesFactory.ROLE_THREE)) { diff --git a/frontend/app/scripts/controllers/historyController.js b/frontend/app/scripts/controllers/historyController.js index c96e0447d..2028a020f 100644 --- a/frontend/app/scripts/controllers/historyController.js +++ b/frontend/app/scripts/controllers/historyController.js @@ -115,6 +115,7 @@ class HistoryCtrl { var errors = $injector.get('errors'); var historyMaxPoints = $injector.get('historyMaxPoints'); var uiConfig = $injector.get('uiConfig'); + this.templatesConfig = $injector.get('templatesConfig'); this.orderByFilter = $injector.get('orderByFilter'); this.$timeout = $injector.get('$timeout'); this.$state = $injector.get('$state'); @@ -184,6 +185,11 @@ class HistoryCtrl { this.dataPointsMultiple = []; + // Templates props + this.templates = []; + this.selectedTemplate = null; + this.newTemplateName = ""; + // 4. Setup this.updateTranslations() .then(() => uiConfig.whenReady()) @@ -192,6 +198,13 @@ class HistoryCtrl { this.setSelectedControlsAndStartLoading(stateFromUrl.c); }); + this.updateTranslations() + .then(() => this.templatesConfig.whenReady()) + .then(data => { + this.templates = data.templates; + console.log("[LOG]: Templates are set up: %o", this.templates); + }); + this.plotlyEvents = graph => { // !!!!! метод обязательно должен быть в конструкторе иначе контекст будет непонятно чей graph.on('plotly_relayout', event => { @@ -978,6 +991,84 @@ class HistoryCtrl { downloadLink.click(); } + + saveTemplate() { + if (!this.newTemplateName || !this.selectedControls.length) return; + if (this.templates.find(elem => elem.name === this.newTemplateName)) { + alert(this.$translate.instant('history.errors.template_exists')) + return; + } + + // NOTE: Probably should add more fields of the control + // because (deviceId, controlId) can duplicate? + const newTemplate = { + name: this.newTemplateName, + data: this.selectedControls.map(control => ({ + deviceId: control.deviceId, + controlId: control.controlId + })) + }; + console.log(`[LOG]: New template was created: ${newTemplate.name}`); + console.log('[LOG]: New template data: %o', newTemplate.data); + + // Save the template locally and globally + // (because this.templates are linked to data from templatesConfig) + this.templates.push(newTemplate); + this.newTemplateName = ""; + console.log("[LOG]: New template was saved"); + } + + applyTemplate() { + if (!this.selectedTemplate) return; + console.log("[LOG]: selectedTemplate: %o", this.selectedTemplate); + const template = this.templates.find((element) => element.name === this.selectedTemplate.name); + this.selectedControls = template.data.map( + data => { + const control = this.controls.find( + c => + c.deviceId === data.deviceId && + c.controlId === data.controlId + ); + return control || null; + } + ) + .filter(c => c) + + console.log("[LOG]: Template was successfully applied"); + } + + updateTemplate() { + if (!this.selectedTemplate || !this.selectedControls.length) return; + const updatedTemplate = { + name: this.selectedTemplate.name, + data: this.selectedControls.map(control => ({ + deviceId: control.deviceId, + controlId: control.controlId, + })), + }; + + console.log("[LOG]: Obtained updated template"); + console.log(`[LOG]: New template name: ${updatedTemplate.name}`); + + // Updating the template by index + const index = this.templates.findIndex(t => t.name === this.selectedTemplate.name); + console.log(`[LOG]: Existing template found with index ${index}`); + if (index !== -1) { + this.templates[index] = updatedTemplate; + console.log("[LOG]: Template was updated"); + } + } + + deleteTemplate() { + if (!this.selectedTemplate) return; + + const index = this.templates.findIndex((element) => element.name === this.selectedTemplate.name); + this.templates.splice(index, 1); + this.selectedTemplate = null; + this.selectedControls = [null]; + + console.log("[LOG]: Template was deleted"); + } } // class HistoryCtrl //----------------------------------------------------------------------------- diff --git a/frontend/app/scripts/i18n/history/en.json b/frontend/app/scripts/i18n/history/en.json index 7ec5d38c7..871d13637 100644 --- a/frontend/app/scripts/i18n/history/en.json +++ b/frontend/app/scripts/i18n/history/en.json @@ -9,7 +9,9 @@ "nothing": "No data points to display", "date": "Date and time", "all_channels": "All channels: ", - "widget_channels": "Channels used in widgets: " + "widget_channels": "Channels used in widgets: ", + "choose_template": "- Choose template -", + "new_template": "- Name for a new template -" }, "buttons": { "delete": "delete", @@ -17,15 +19,20 @@ "load": "Load data", "reset": "Reset", "stop": "Stop loading", - "download": "Download history as CSV" + "download": "Download history as CSV", + "apply_template": "Apply template", + "delete_template": "Delete template", + "update_template": "Update template", + "save_template": "Save template" }, "errors": { "dates": "Date range is invalid. Change one of dates", "load": "Error getting history", - "warning": "Warning" + "warning": "Warning", + "template_exists": "This template already exists" }, "format": { "date_with_ms": "MMM d, yyyy h:mm:ss:sss a" } } -} \ No newline at end of file +} diff --git a/frontend/app/scripts/i18n/history/ru.json b/frontend/app/scripts/i18n/history/ru.json index a417aa168..37d22ec54 100644 --- a/frontend/app/scripts/i18n/history/ru.json +++ b/frontend/app/scripts/i18n/history/ru.json @@ -9,7 +9,9 @@ "nothing": "Нет записей для отображения", "date": "Дата и время", "all_channels": "Все каналы: ", - "widget_channels": "Каналы из виджетов: " + "widget_channels": "Каналы из виджетов: ", + "choose_template": "- Выберите шаблон -", + "new_template": "- Имя шаблона -" }, "buttons": { "delete": "удалить", @@ -17,15 +19,20 @@ "load": "Загрузить данные", "reset": "Сбросить", "stop": "Остановить загрузку", - "download": "Скачать данные в виде CSV" + "download": "Скачать данные в виде CSV", + "apply_template": "Применить шаблон", + "delete_template": "Удалить шаблон", + "update_template": "Изменить шаблон", + "save_template": "Сохранить шаблон" }, "errors": { "dates": "Некорректный временной интервал, измените одну из дат", "load": "Ошибка получения данных", - "warning": "Предупреждение" + "warning": "Предупреждение", + "template_exists": "Такой шаблон уже существует" }, "format": { "date_with_ms": "d MMM yyyy г. H:mm:ss:sss" } } -} \ No newline at end of file +} diff --git a/frontend/app/scripts/services/templatesConfig.js b/frontend/app/scripts/services/templatesConfig.js new file mode 100644 index 000000000..fbf55d8e8 --- /dev/null +++ b/frontend/app/scripts/services/templatesConfig.js @@ -0,0 +1,29 @@ +'use strict'; + +// Service used to keep track of templates data +function templatesConfigService($q) { + 'ngInject'; + + var data = { + templates: [] + }; + + var deferReady = $q.defer(); + + return { + data, + + whenReady() { + return deferReady.promise; + }, + + ready(changes) { + if (changes) { + angular.extend(data, changes); + } + deferReady.resolve(data); + } + } +} + +export default templatesConfigService; diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index c86acbe07..6282121df 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -1294,3 +1294,9 @@ body #https-setup-label { left: 0; right: 0; } + +/* Templates styling */ +.form-group { + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/frontend/app/views/history.html b/frontend/app/views/history.html index cc2906139..556ec8b0f 100644 --- a/frontend/app/views/history.html +++ b/frontend/app/views/history.html @@ -36,6 +36,41 @@