UI для Ensemble Workflow на Angular

5aaa101429d34f80bf58021a2404dd5f.jpgТе, кто знаком с платформой для интеграции и разработки приложений InterSystems Ensemble, знают, что такое подсистема Ensemble Workflow и как она бывает полезна для автоматизации взаимодействия людей. Для тех же, кто не знаком с Ensemble (и/или Workflow), я кратко опишу её возможности (остальные могут пропустить эту часть и узнать, как они могут использовать пользовательский интерфейс Workflow на Angular.js).

InterSystems EnsembleПлатформа для интеграции и разработки приложений InterSystems Ensemble предназначена для интеграции разрозненных систем, автоматизации бизнес-процессов и создания новых композитных приложений, дополняющих функционал интегрированных приложений новой бизнес-логикой или пользовательским интерфейсом. Ensemble обеспечивает решение задач: EAI, SOA, BPM, BAM и даже BI (за счет встроенной технологии для разработки аналитических приложений InterSystems DeepSee).В Ensemble существуют следующие основные компоненты:

Адаптеры — компоненты для взаимодействия с приложениями, технологиями и источниками данных. Вместе с Ensemble поставляются технологические и прикладные интеграционные адаптеры (Web- и Rest- сервисы, File, FTP, Email, SQL, EDI, HL7, SAP, Siebel, 1C Предприятие и т.д.). Можно создавать собственные адаптеры с помощью Adapter SDK. Бизнес-службы — компоненты, преобразующие данные, поступающие от внешних систем, в сообщения Ensemble, и вызывающие на исполнение бизнес-процессы и/или бизнес-операции. Бизнес-процессы — исполняемые процессы, использующиеся для оркестровки служб и операций для автоматизации сценариев взаимодействия систем и/или людей (через подсистему Workflow). Процессы либо описываются на декларативном языке Business Process Language, либо реализуются на Caché Object Script. Логика взаимодействия процессов с внешним миром отделена от конкретной реализации взаимодействия с помощью служб и операций. Бизнес-операции — компоненты, обеспечивающие вызов/передачу сообщений внешним системам и преобразование сообщений Ensemble в формат, пригодный для передачи во внешние системы. Трансформации сообщений — компоненты Ensemble для трансформации сообщений из одного формата в другой. Для реализации используется декларативный язык Data Transformation Language. Бизнес-правила — позволяют администраторам интеграционного решения без программирования менять поведение бизнес-процессов Ensemble в указанных в процессах точках принятия решений. Управление потоками работ — подсистема Ensemble Workflow обеспечивает автоматизацию распределения задач между пользователями. Бизнес-метрики — позволяют собирать и вычислять ключевые показатели эффективности и вместе с инструментальными панелями (Dashboards) используются для создания решений по мониторингу бизнес-активности (Business Activity Monitoring, BAM). 6af9b125606e456b9864419258ba5203.pngВернемся к управлению потоками работ и рассмотрим функционал подсистемы Ensemble Workflow более подробно.

Управление потоками работ и подсистема Ensemble Workflow Согласно определению Workflow Management Coalition (www.WfMC.org), «потоки работ (Workflow) — это автоматизация бизнес процесса, полностью или частично, в рамках которого документы, информация или задачи передаются от одного участника к другому, в соответствии с набором процедурных правил.«Ключевые элементы Workflow:

Задача Workflow — «фрагмент» работы Поток работ — процедурные правила выполнения задач Пользователь Workflow — человек, выполняющий задачи в системе управления потоками работ Роль Workflow — группа пользователей, которые выполняют определенные типы задач. Подсистема управления потоками работ в Ensemble позволяет:

Автоматизировать управление потоками работ, используя бизнес-процессы Ensemble Гибко настраивать распределение работ Работать с подсистемой управления потоками работ через специализированный Workflow-портал, который поставляется вместе с Ensemble Организовать взаимодействие подсистемы управления потоками работ с интеграционными бизнес-процессами Ensemble Использовать подсистему мониторинга бизнес-активности, утилиты управления и мониторинга Ensemble Легко настраивать и расширять функционал подсистемы Workflow Простейшим примером автоматизации управления потоками работ является приложение Ensemble HelpDesk для автоматизации взаимодействия сотрудников службы поддержки, которое входит в стандартную поставку примеров Ensemble и находится в области Ensdemo. Ensemble принимает сообщение о проблеме и запускает бизнес-процесс HelpDesk.6849d70be1dd4702919a5a23d6eae29e.pngФрагмент алгоритма бизнес-процесса HelpDesk

Бизнес-процесс отправляет пользователям роли Demo-Development задачу с помощью сообщения класса EnsLib.Workflow.TaskRequest, в котором определены возможные действия («Исправлено» или «Проигнорировано»), а так же поле «Комментарий». В тело сообщения также включена информация об ошибке и пользователе, сообщившем о проблеме. После этого в Workflow-портале любого пользователя роли Demo-Development появляется соответствующая задача.9f9311f26389470881b20edf33e891bf.png

Первоначально (если это не задано в сообщении TaskRequest) задача не ассоциирована ни с одним пользователем (а только с ролью), поэтому пользователю нужно ее принять, нажав соответствующую кнопку. Так же в любой момент можно отказаться от задачи, нажав кнопку «Уступить».

После этого можно совершать доступные для конкретной задачи действия. В нашем случае мы можем нажать кнопку «Исправлено», предварительно указав комментарий в соответствующем поле. Бизнес-процесс HelpDesk обработает это событие и отправит новое сообщение пользователям роли Demo-Testing, сигнализируя о необходимости тестирования произведенных исправлений. Если нажать кнопку «Проигнорировано», то задача будет просто помечена как «Not a problem» и процесс обработки завершится.

Как видно из примера, Ensemble Workflow является простой и интуитивно понятной системой для организации потоков работ пользователей. Более подробную информацию о подсистеме Ensemble Workflow можно в документации Ensemble в разделе Defining Workflow.

Функциональность подсистемы Ensemble Workflow может быть легко расширена и встроена во внешнее композитное приложение на InterSystems Ensemble. В качестве примера рассмотрим реализацию функциональности пользовательского интерфейса Ensemble Workflow во внешнем композитном приложении, разработанном на Angular.js + REST API.

Интерфейс Ensemble Workflow на Angular.js. Для работы пользовательского интерфейса Workflow на Angular.js необходимо установить на сервер Ensemble приложения: Процесс установки описан в Readme указанных репозиториев.На данный момент в приложении реализована вся базовая функциональность Ensemble Workflow: отображение списка задач, дополнительных полей и действий, сортировка, полнотекстовый поиск по задачам. Пользователь может принимать/отклонять задачи, подробная информация о задаче выводится в модальном окне.

Так же в ближайшее время в планах добавить в приложение возможность смены области (на данный момент приложение работает только в той области, в которой оно установлено).

На момент написания статьи приложение выглядит следующим образом: 1b15ea25ccbb41c89e3a4be6f3b35cfb.png

f703550305be4c3d85ed2202019f196c.pngДля последующей модификации интерфейса при необходимости, был использован Twitter Bootstrap

Некоторые технические детали реализации В UI используются следующие библиотеки и фреймворки: js-фреймворк Angular.js, css-фреймворк Twitter Bootstrap, js-библиотека jQuery, а так же иконочные шрифты FontAwesome.Приложение имеет 4 Angular-сервиса (RESTSrvc, SessionSrvc, UtilSrvc и WorklistSrvc), 3 контроллера (MainCtrl, TaskCtrl, TasksGridCtrl), главную страницу (index.csp) и 2 шаблона (task.csp и tasks.csp).

Сервис RESTSrvc имеет всего один метод getPromise и является оберткой вокруг сервиса $http Angular.js. Единственное предназначение RESTSrvc — отправлять HTTP-запросы на сервер и возвращать объекты promise этих запросов. Остальные сервисы используют RESTSrvc для осуществления запросов и их разделение носит, по существу, функциональный характер.

RESTSrvc.js 'use strict';

function RESTSrvc ($http, $q) { return { getPromise: function (config) { var deferred = $q.defer ();

$http (config). success (function (data, status, headers, config) { deferred.resolve (data); }). error (function (data, status, headers, config) { deferred.reject (data, status, headers, config); });

return deferred.promise; } } };

// resolving minification problems RESTSrvc.$inject = ['$http', '$q']; servicesModule.factory ('RESTSrvc', RESTSrvc); SessionSrvc — содержит всего один метод, отвечающий за закрытие сессии. Аутентификация в приложении выполнена с помощью Basic access authetication (http://en.wikipedia.org/wiki/Basic_access_authentication), поэтому нет необходимости в аутентифицирующем методе, так как каждый запрос имеет в header«е токен авторизации.SessionSrvc.js 'use strict';

// Session service function SessionSrvc (RESTSrvc) { return { // save worklist object logout: function (baseAuthToken) { return RESTSrvc.getPromise ({method: 'GET', url: RESTWebApp.appName + '/logout', headers: {'Authorization' : baseAuthToken} }); } } };

// resolving minification problems SessionSrvc.$inject = ['RESTSrvc']; servicesModule.factory ('SessionSrvc', SessionSrvc); UtilSrvc — содержит вспомогательные методы, такие как получение значения cookie по имени, получение значения свойства объекта по имени.UtilSrvc.js 'use strict';

// Utils service function UtilSrvc ($cookies) { return { // get cookie by name readCookie: function (name) { return $cookies[name]; }, // Function to get value of property of the object by name // Example: // var obj = {car: {body: {company: {name: 'Mazda'}}}}; // getPropertyValue (obj, 'car.body.company.name') getPropertyValue: function (item, propertyStr) { var value = item;

try { var properties = propertyStr.split ('.'); for (var i = 0; i < properties.length; i++) { value = value[properties[i]]; if (value !== Object(value)) break; } } catch(ex) { console.log('Something goes wrong :/'); }

return value == undefined? '' : value; } } };

// resolving minification problems UtilSrvc.$inject = ['$cookies']; servicesModule.factory ('UtilSrvc', UtilSrvc); WorklistSrvc отвечает за выполнение запросов, связанных с данными списка задач.WorklistSrvc.js 'use strict';

// Worklist service function WorklistSrvc (RESTSrvc) { return { // save worklist object save: function (worklist, baseAuthToken) { return RESTSrvc.getPromise ({method: 'POST', url: RESTWebApp.appName + '/tasks/' + worklist._id, data: worklist, headers: {'Authorization' : baseAuthToken} }); }, // get worklist by id get: function (id, baseAuthToken) { return RESTSrvc.getPromise ({method: 'GET', url: RESTWebApp.appName + '/tasks/' + id, headers: {'Authorization' : baseAuthToken} }); }, // get all worklists for current user getAll: function (baseAuthToken) { return RESTSrvc.getPromise ({method: 'GET', url: RESTWebApp.appName + '/tasks', headers: {'Authorization' : baseAuthToken} }); } } };

// resolving minification problems WorklistSrvc.$inject = ['RESTSrvc']; servicesModule.factory ('WorklistSrvc', WorklistSrvc); MainCtrl — главный контроллер приложения, отвечает за аутентификацию пользователя.MainCtrl.js 'use strict';

// Main controller // Controls the authentication. Loads all the worklists for user. function MainCtrl ($scope, $location, $cookies, WorklistSrvc, SessionSrvc, UtilSrvc) { $scope.page = {}; $scope.page.alerts = []; $scope.utils = UtilSrvc; $scope.page.loading = false; $scope.page.loginState = $cookies['Token'] ? 1: 0; $scope.page.authToken = $cookies['Token'];

$scope.page.closeAlert = function (index) { if ($scope.page.alerts.length) { $('.alert: nth-child ('+(index+1)+')').animate ({opacity: 0, top:»-=150» }, 400, function () { $scope.page.alerts.splice (index, 1); $scope.$apply (); }); } }; $scope.page.addAlert = function (alert) { $scope.page.alerts.push (alert); if ($scope.page.alerts.length > 5) { $scope.page.closeAlert (0); } }; /* Authentication section */ $scope.page.makeBaseAuth = function (user, password) { var token = user + ':' + password; var hash = Base64.encode (token); return «Basic » + hash; } // login $scope.page.doLogin = function (login, password) { var authToken = $scope.page.makeBaseAuth (login, password); $scope.page.loading = true; WorklistSrvc.getAll (authToken).then ( function (data) { $scope.page.alerts = []; $scope.page.loginState = 1; $scope.page.authToken = authToken; // set cookie to restore loginState after page reload $cookies['User'] = login.toLowerCase (); $cookies['Token'] = $scope.page.authToken; // refresh the data on page $scope.page.loadSuccess (data); }, function (data, status, headers, config) { if (data.Error) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); } else { $scope.page.addAlert ({type: 'danger', msg: «Login unsuccessful»}); } }) .then (function () { $scope.page.loading = false; }) };

// logout $scope.page.doExit = function () { SessionSrvc.logout ($scope.page.authToken).then ( function (data) { $scope.page.loginState = 0; $scope.page.grid.items = null; $scope.page.loading = false; // clear cookies delete $cookies['User']; delete $cookies['Token']; document.cookie = «CacheBrowserId» + »=; Path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;»; document.cookie = «CSPSESSIONID» + »=; Path=» + RESTWebApp.appName + »; expires=Thu, 01 Jan 1970 00:00:01 GMT;»; document.cookie = «CSPWSERVERID» + »=; Path=» + RESTWebApp.appName + »; expires=Thu, 01 Jan 1970 00:00:01 GMT;»; }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }); };

}

// resolving minification problems MainCtrl.$inject = ['$scope', '$location', '$cookies', 'WorklistSrvc', 'SessionSrvc', 'UtilSrvc']; controllersModule.controller ('MainCtrl', MainCtrl); TasksGridCtrl — контроллер, отвечающий за таблицу списка задач и действия с ней. Он инициализирует таблицу списка задач, содержит методы для загрузки списка задач и конкретной задачи, а так же методы обработки действий пользователя (нажатие кнопок, сортировка таблицы, выделение строки таблицы, фильтрация).TasksGridCtrl.js 'use strict';

// TasksGrid controller // dependency injection function TasksGridCtrl ($scope, $window, $modal, $cookies, WorklistSrvc) { // Initialize grid. // grid data: // grid title, css grid class, column names $scope.page.grid = { caption: 'Inbox Tasks', cssClass:'table table-condensed table-bordered table-hover', columns: [{name: '', property: 'New', align: 'center'}, {name: 'Priority', property: 'Priority'}, {name: 'Subject', property: 'Subject'}, {name: 'Message', property: 'Message'}, {name: 'Role', property: 'RoleName'}, {name: 'Assigned To', property: 'AssignedTo'}, {name: 'Time Created', property: 'TimeCreated'}, {name: 'Age', property: 'Age'}] }; // data initialization for Worklist $scope.page.dataInit = function () { if ($scope.page.loginState) { $scope.page.loadTasks (); } };

$scope.page.loadSuccess = function (data) { $scope.page.grid.items = data.children; // if we get data for other user — logout if (!$scope.page.checkUserValidity ()) { $scope.page.doExit (); } var date = new Date ();

var hours = (date.getHours () > 9) ? date.getHours () : '0' + date.getHours (); var minutes = (date.getMinutes () > 9) ? date.getMinutes () : '0' + date.getMinutes (); var secs = (date.getSeconds () > 9) ? date.getSeconds () : '0' + date.getSeconds (); $('#updateTime').animate ({ opacity: 0 }, 100, function () { $('#updateTime').animate ({ opacity: 1 }, 1000);}); $scope.page.grid.updateTime = ' [Last Update: ' + hours; $scope.page.grid.updateTime += ':' + minutes + ':' + secs + ']'; };

// all user’s tasks loading $scope.page.loadTasks = function () { $scope.page.loading = true; WorklistSrvc.getAll ($scope.page.authToken).then ( function (data) { $scope.page.loadSuccess (data); }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }) .then (function () { $scope.page.loading = false; }) }; // load task (worklist) by id $scope.page.loadTask = function (id) { WorklistSrvc.get (id, $scope.page.authToken).then ( function (data) { $scope.page.task = data; }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }); }; // 'Accept' button handler. // Send worklist object with '$Accept' action to server. $scope.page.accept = function (id) { // nothing to do, if no id if (! id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get (id).then ( function (data) { data.Task[»%Action»] = »$Accept»; $scope.page.submit (data); }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }); }; // 'Yield' button handler. // Send worklist object with '$Relinquish' action to server. $scope.page.yield = function (id) { // nothing to do, if no id if (! id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get (id).then ( function (data) { data.Task[»%Action»] = »$Relinquish»; $scope.page.submit (data); }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }); }; // submit the worklist object $scope.page.submit = function (worklist) { // send object to server. If ok, refresh data on page. WorklistSrvc.save (worklist, $scope.page.authToken).then ( function (data) { $scope.page.dataInit (); }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); } ); }; /* table section */ // sorting table $scope.page.sort = function (property, isUp) { $scope.page.predicate = property; $scope.page.isUp = ! isUp; // change sorting icon $scope.page.sortIcon = 'fa fa-sort-' + ($scope.page.isUp? 'up':'down') + ' pull-right'; }; // selecting row in table $scope.page.select = function (item) { if ($scope.page.grid.selected) { $scope.page.grid.selected.rowCss = ''; if ($scope.page.grid.selected == item) { $scope.page.grid.selected = null; return; } } $scope.page.grid.selected = item; // change css class to highlight the row $scope.page.grid.selected.rowCss = 'info'; };

// count currently displayed tasks $scope.page.totalCnt = function () { return $window.document.getElementById ('tasksTable').getElementsByTagName ('TR').length — 2; }; // if AssignedTo matches with current user — return 'true' $scope.page.isAssigned = function (selected) { if (selected) { if (selected.AssignedTo.toLowerCase () === $cookies['User'].toLowerCase ()) return true; } return false; }; // watching for changes in 'Search' input // if there is change, reset the selection. $scope.$watch ('query', function () { if ($scope.page.grid.selected) { $scope.page.select ($scope.page.grid.selected); } });

/* modal window open */ $scope.page.modalOpen = function (size, id) { // if no id — nothing to do if (! id) return; // obtainig the full object by id. If ok — open modal. WorklistSrvc.get (id).then ( function (data) { // see http://angular-ui.github.io/bootstrap/ for more options var modalInstance = $modal.open ({ templateUrl: 'partials/task.csp', controller: 'TaskCtrl', size: size, backdrop: true, resolve: { task: function () { return data; }, submit: function () { return $scope.page.submit } } }); // onResult modalInstance.result.then ( function (reason) { if (reason === 'save') { $scope.page.addAlert ({type: 'success', msg: 'Task saved'}); } }, function () {}); }, function (data, status, headers, config) { $scope.page.addAlert ({type: 'danger', msg: data.Error}); }); }; /* User’s validity checking. */

// If we get the data for other user, logout immediately $scope.page.checkUserValidity = function () { var user = $cookies['User']; for (var i = 0; i < $scope.page.grid.items.length; i++) { if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() !== $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return false; } else if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() == $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return true; } } return true; }; // Check user's validity every 10 minutes. setInterval(function() { $scope.page.dataInit() }, 600000);

/* Initialize */ // sort table (by Age, asc) // to change sorting column change 'columns[]' $scope.page.sort ($scope.page.grid.columns[7].property, true); $scope.page.dataInit (); }

// resolving minification problems TasksGridCtrl.$inject = ['$scope', '$window', '$modal', '$cookies', 'WorklistSrvc']; controllersModule.controller ('TasksGridCtrl', TasksGridCtrl); TaskCtrl — контроллер модального окна, содержащего подробную информацию о задаче. Формирует список полей и действий пользователя, а так же обрабатывает нажатия кнопок модального окна.TaskCtrl.js 'use strict';

// Task controller // dependency injection function TaskCtrl ($scope, $routeParams, $location, $modalInstance, WorklistSrvc, task, submit) { $scope.page = { task:{} }; $scope.page.task = task; $scope.page.actions = »; $scope.page.formFields = »; $scope.page.formValues = task.Task['%FormValues']; if (task.Task['%TaskStatus'].Request['%Actions']) { $scope.page.actions = task.Task['%TaskStatus'].Request['%Actions'].split (','); } if (task.Task['%TaskStatus'].Request['%FormFields']) { $scope.page.formFields = task.Task['%TaskStatus'].Request['%FormFields'].split (','); } // dismiss modal $scope.page.cancel = function () { $modalInstance.dismiss ('cancel'); }; // perform a specified action $scope.page.doAction = function (action) { $scope.page.task.Task[»%Action»] = action; $scope.page.task.Task['%FormValues'] = $scope.page.formValues;

submit ($scope.page.task); $modalInstance.close (action); }

}

// resolving minification problems TaskCtrl.$inject = ['$scope', '$routeParams', '$location', '$modalInstance', 'WorklistSrvc', 'task', 'submit']; controllersModule.controller ('TaskCtrl', TaskCtrl); app.js — файл, содержащий все модули приложения.app.js 'use strict'; /* Adding routes (when). [route], {[template path for ng-view], [controller for this template]}

otherwise Set default route.

$routeParams.id — : id parameter. */

var servicesModule = angular.module ('servicesModule',[]); var controllersModule = angular.module ('controllersModule', []); var app = angular.module ('app', ['ngRoute', 'ngCookies', 'ui.bootstrap', 'servicesModule', 'controllersModule']);

app.config ([ '$routeProvider', function ($routeProvider) { $routeProvider.when ('/tasks', {templateUrl: 'partials/tasks.csp'}); $routeProvider.when ('/tasks/: id', {templateUrl: 'partials/task.csp', controller: 'TaskCtrl'}); $routeProvider.otherwise ({redirectTo: '/tasks'}); }]); index.csp — главная страница приложения.index.csp

Ensemble Workflow

Loading
{{alert.msg}}

Please, Log In first.

tasks.csp — шаблон таблицы списка задач.tasks.csp

# Action

There is no task (s) for current user.

Showing {{page.totalCnt ()}} of {{page.grid.items.length}} task (s).
New

task.csp — шаблон модального окна.task.csp Так же, никто не запрещает использовать наш REST API для своего UI, тем более он довольно прост.URL map нашего REST API Вы можете опробовать пользовательский интерфейс на нашем тестовом сервере, на котором запущено приложение HelpDesk. Login: dev / Pass: 123

© Habrahabr.ru