Простой framework UI ERP c помощью Vaadin

Хабркат


Введение


Зачем это затевалось? Год назад начали писать ERP систему. И с того момента начался наш тернистый путь. Определили стек технологий с которым будем работать. Кратко описали задачу и приступили к работе.


В течении обучения и параллельной "разработки" начал вырисовывать интерфейс и будущую архитектуру приложения. В итоге я попытался написать еще один свой фреймоворк.


В этой статье попытаюсь описать что было сделано, какую структуру реализовал, описать конкретную реализацию классов, почему так, написать примеры использования самописного фреймоврка. Ну и расскажу дальнейшие планы.



Стек


Если кратко об используемом стеке, то использовали финский web-framework Vaadin 7.7. Это инструмент который дает возможность писать single page application практически на одном языке (Java). Т.е. с помощью языковых конструкций описывать элементы интерфейса, которые потом транслируются в HTML+JS код и показываются в браузере.


Приложение работает полностью на сервере, беря на себя все вычисления. Действия пользователя в браузере обрабатываются на сервере, изменили размер элемента, он перерисовался на сервере и отправил результат пользователю в браузер. В общем будет много маломощных пользовательских машин читайте телефоны, планшеты, старые офисные машины и парочка мощных серверов.


API


Цели преследуемые фреймворком следующие: быстрое добавление в общий интерфейс элементов
отображающих нужную структуру данных в уже привычном для всех интерфейсе. А также обеспечение работы через активную запись. Т.е. работа с выделенной строкой в таблице и добавление к ней необходимых связей.


Структура получилась следующая:


Пакет Название
Data DataContainer
TreeDataContainer
Elements BottomTabs
CommonLogic
CommonView
FilterPanel
Logic
Menu
MenuNavigator
Mode
Workspace
Permission ModifierAccess
PermissionAccess
PermissionAccessUI

Data


В пакете Data компоненты, которые необходимы для связывания (binding) данных с элементами UI. В текущей версии реализованы контейнеры, которые имеют дополнительные методы для быстрого присвоения данных в таблицы и деревья. В пакете два класса: DataContainer — абстрактный класс, на основании которого создаются производные контейнеры для хранения определенных классов данных. TreeDataContainer — реализует класса DataContainer для хранения элементов с указанием иерархии, и для отображения древовидных структур.


Примеры использования всех классов будут в следующих разделах.


Elements


Пакет, в котором находятся все классы описывающие элементы графики и логики системы.


Подход принятый к построению интерфейса — использование отдельных видов, в которых храниться все необходимые компоненты текущего UI. Использовались стандартные компоненты Vaddin — компонент View и его реализация CommonView, а также компонент навигации между видами Menu. Логика работы этих компонентов в связке взята из примера Vaadin, пример и как его сгенерить у себя с помощью maven archetype.


Реализация CommonView должна содержать в себе ссылку на реализацию интерфейса Logic или расширять уже имеющуюся реализацию CommonLogic.


Также есть перечисление Mode которое содержит в себе перечень режимов работы с имеющимися интерфейсами.


Основной графический элемент — Workspace. Это класс в котором имеется две таблицы (в
которые присваиваются данные DataContainer), основная (метод getTable()) содержит текущую информацию, таблица со списком всех элементов (метод getTableAll()) которые можно выбирать для добавления в текущий контейнер.


Навигация в Workspace реализует элемент MenuNavigator. Он описывает перечень стандартных методов работы c Workspace, такие как включение режимов Добавления и Удаления, Печати, включения панели фильтрации для таблиц, описанной в классе FilterPanel.


Для возможности редактирования добавленной информации в контейнер (установленный в таблицу из метода getTable()) используется класс BottomTabs, в который добавляются вкладки, которые в себе содержат интерфейс для редактирования информации: таблицы, поля, выпадающие списки и все что нужно.


Permission


Пакет содержит классы для реализации прав доступа к графическим элементам и механизмы повышения прав доступа с помощью ролей.


ModifierAccess — перечисление имеющихся уровней доступа к UI: отключен, чтение, редактирование.


PermissionAccess — класс реализующий механизмы установки прав доступа, где действует принцип повышения права. Т.е. если пользователю назначено в одной группе право для элемента на чтение, а в другой на редактирование, в итоге пользователю будет доступно максимальное право — право на редактирование.


PermissionAccessUI — интерфейс который имплементируеся в графические компоненты, на которые назначаются права.


Реализация


Класс DataContainer — класс для хранения структур данных в виде контейнера, расширяющий BeanItemContainer.


abstract public class DataContainer<T> extends BeanItemContainer<T> {

    private ArrayList<String> captions = new ArrayList<>();
    private ArrayList<Boolean> visible = new ArrayList<>();
    private final ArrayList<String> headers = new ArrayList<>();

    public DataContainer(Class<T> type) {
        super(type);
        if (validCaption())
            initHeaders();
    }

    private boolean validCaption() {
        return captions.size() == visible.size() &&
                captions.size() == headers.size();
    }

    abstract protected void initHeaders();

    abstract public DataContainer loadAllData();

    //....
}

Создан для удобного присвоения контейнера в таблицы и деревья, за счет списков captions,
headers, visible в которых описываются какие property класса будут отображаться в виде столбцов, какие у них будут заголовки и какие из них будут свернуты.


Механизм присвоения контейнера в таблицу реализован в CommonLogic:


abstract public class CommonLogic implements Logic {

    private View view;

    public CommonLogic(View view){
        this.view = view;
    }

    public View getView(){
        return this.view;
    }

    public void setDataToTable(DataContainer container, CustomTable table) {
        if (container == null || table == null) return;

        table.setContainerDataSource(container);
        table.setVisibleColumns(container.getCaption());
        table.setColumnHeaders(container.getHeaders());
        table.setColumnCollapsingAllowed(true);
        for (int i = 0; i < container.getCaption().length; i++) {
            table.setColumnCollapsed(container.getCaption()[i],
                    container.getVisible()[i].booleanValue());
        }
    }
}

Workspace реализует в себе следующий код:



abstract public class Workspace extends CssLayout implements PermissionAccessUI {
    private Logic logic;

    private Float splitPosition = 50f;

    private Mode mode = Mode.NORMAL;

    public String CAPTION = "";
    public ThemeResource PICTURE = null;

    private FilterTable table = null;
    private FilterTable tableAll = null;

    private ItemClickEvent.ItemClickListener editItemClickListener;
    private ItemClickEvent.ItemClickListener editItemClickListenerAll;

    private VerticalSplitPanel verticalSplitPanel = null;
    private HorizontalSplitPanel horizontalSplitPanel = null;

    private BottomTabs bottomTabs = null;

    private MenuNavigator navigator = null;

    private FilterPanel filterPanel = null;

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;

    private VerticalLayout layout;
    private ItemClickEvent.ItemClickListener selectItemClickListener;
    private ItemClickEvent.ItemClickListener selectItemClickListenerAll;

    public Workspace(Logic logic) {
        this.logic = logic;
        table();
        tableAll();
        navigatorLayout();
        filterPanel();
        horizontalSplitPanel();
        verticalSplitPanel();
        addComponent(verticalSplitPanel);

        editOff();
        setSizeFull();
    }
    //...
}

Где table() и tableAll() методы построения таблицы для текущего контейнера и для контейнера со всеми записями (справочника). navigatorLayout() создает меню для навигации (оно же MenuNavigator) и работы с текущим экземпляром Workspace. filterPanel() — создает панель фильтрации для таблицы с текущим контейнером. В veritcalSplitPanel() описывается создание нижней панели с закладками tabs для редактирования выбранных элементов в таблице созданной в table().


Класс MenuNavigator дает стандартный набор методов для работы с имплементацией Workspace:



public abstract class MenuNavigator extends MenuBar implements PermissionAccessUI {

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;

    private MenuItem add;
    private MenuItem delete;
    private MenuItem print;
    private MenuItem filter;

    public static final String ENABLE_BUTTON_STYLE ="highlight";

    private Workspace parent;

    public MenuNavigator(String caption, Workspace parent) {
        this.parent = parent;
        setWidth("100%");
        Command addCommand = menuItem -> add();

        Command deleteCommand = menuItem -> delete();

        Command printCommand = menuItem -> print();

        Command filterCommand = menuItem -> filter();

        add = this.addItem("add" + caption, 
                                     new ThemeResource("ico16/add.png"), 
                                     addCommand);
        add.setDescription("Добавить");

        delete = this.addItem("delete" + caption, 
                                     new ThemeResource("ico16/delete.png"), 
                                     deleteCommand);
        delete.setDescription("Удалить");

        print = this.addItem("print" + caption, 
                                     new ThemeResource("ico16/printer.png"), 
                                     printCommand);
        print.setDescription("Печать");

        filter = this.addItem("filter" + caption, 
                                     new ThemeResource("ico16/filter.png"), 
                                     filterCommand);
        filter.setDescription("Сортировать");

        this.setStyleName("v-menubar-menuitem-caption-null-size");
        this.addStyleName("menu-navigator");
    }
    //...
}

В классе создаются общие элементы меню, описывается логика поведения в квази-модальном режиме и обязует реализующего этот класс описать нужную логику работы.


Редактирование выделенных записей в таблице созданной в table() происходит с помощью элементов добавленных в UI BottomTabs:



abstract public class BottomTabs extends TabSheet implements PermissionAccessUI {

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;
    private final List<String> captions = new ArrayList<>();
    private final List<Component> components = new ArrayList<>();
    private final List<Resource> resources = new ArrayList<>();

    public BottomTabs() {
        captions.removeAll(captions);
        components.removeAll(components);
        resources.removeAll(resources);
        setSizeFull();
        init();
    }

    private void init() {
        initTabs();
        for (int i = 0; i < this.components.size(); i++) {
            if (i < resources.size() && i < captions.size()) {
                this.addTab(this.components.get(i)
                        , this.captions.get(i)
                        , this.resources.get(i));
            }
        }
    }
    //...
}

Здесь также реализованы списки для более быстрого добавления компонента в закладки: captions — описание заголовков закладок, components — какой элемент будет находиться в этой закладке и resource — какая иконка для него будет отображаться.


Для реализации прав доступа нужно имплементировать PermissionAccessUI и реализовать в нем методы которые должны показывать что активно в этом классе, а что нет, в зависимости от уровня доступа:



public interface PermissionAccessUI {

    void setPermissionAccess(ModifierAccess permission);

    void replacePermissionAccess(ModifierAccess permissionAccess);

    ModifierAccess getModifierAccess();

}

и ниже реализация этих методов в классе Workspace:


//...
    public void setPermissionAccess(ModifierAccess permission) {
        if (navigator != null) {
            navigator.replacePermissionAccess(permission);
        }
        if (bottomTabs != null) {
            bottomTabs.replacePermissionAccess(permission);
        }

        this.permissionAccess = permission;

        switch (permission) {
            case EDIT: {
                this.setVisible(true);
                this.setEnabled(true);
                break;
            }
            case READ: {
                this.setVisible(true);
                this.setEnabled(false);
                break;
            }
            case HIDE: {
                this.setVisible(false);
                this.setEnabled(false);
                break;
            }
        }
    }

    public void replacePermissionAccess(ModifierAccess permissionAccess) {
        PermissionAccess.replacePermissionAccess(this, permissionAccess);
    }

    public ModifierAccess getModifierAccess() {
        return permissionAccess;
    }
//...

Класс PermissionAccess — это final класс, выполняющий функцию утильного Utils класса самому не нравиться но другой реализации пока не придумал, он берет компонент PermissionAccessUI и в соответствии с заданной логикой повышает уровень доступа:



public final class PermissionAccess {
    //...
    public static void replacePermissionAccess(PermissionAccessUI component,
             ModifierAccess newValue) {
        switch (component.getModifierAccess()) {
            case EDIT: {
                if (newValue.equals(ModifierAccess.HIDE) 
                                   || newValue.equals(ModifierAccess.READ)) break;
                component.setPermissionAccess(newValue);
                break;
            }
            case READ: {
                if (newValue.equals(ModifierAccess.HIDE)) break;
                component.setPermissionAccess(newValue);
                break;
            }
            case HIDE: {
                component.setPermissionAccess(newValue);
                break;
            }
        }
    }
    //...
}

Примеры


Данные


Пример создания контейнера какого-то абстрактного класса описывающего предметную область (он же является Bean), назовем его Element:



public class Element implements Serializable {
    private Integer id = 0;
    private String name = "element";
    private Float price = 0.0F;

    public Element(Integer id, String name, Float price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Element element = (Element) o;
        return Objects.equals(id, element.id) &&
                Objects.equals(name, element.name) &&
                Objects.equals(price, element.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, price);
    }
}

Классическая реализация в соответствии со спецификацией для Bean.


Для него создадим контейнер, который будет содержать в себе все записи.



public class ElementContainer extends DataContainer<Element> {
    public ElementContainer() {
        super(Element.class);
    }

    @Override
    protected void initHeaders() {
        addCaption("id", "name", "price");
        addHeader("ID", "Название", "Цена");
        addCollapsed(true, false, false);
    }

    @Override
    public DataContainer loadAllData() {
        add(new Element(1, "name1", 1.0f));
        add(new Element(2, "name2", 2.0f));
        add(new Element(3, "name3", 3.0f));
        add(new Element(4, "name4", 4.0f));
        add(new Element(5, "name5", 5.0f));
        add(new Element(6, "name6", 6.0f));
        add(new Element(7, "name7", 7.0f));
        add(new Element(8, "name8", 8.0f));
        add(new Element(9, "name9", 9.0f));
        add(new Element(10, "name10", 10.0f));
        add(new Element(11, "name11", 11.0f));
        return this;
    }
}

Где в методах addCaption, addHeader, addCollapsed перечисляются property класса Element, которые будут использоваться в виде колонок, в какой последовательности, какие заголовки и какие из них будут скрыты.


Реализация классов для UI


Реализация класса Workspace в виде класса MyLayout:


public class MyLayout extends Workspace {
    private ElementContainer container = new ElementContainer();
    private MyTabSheet tabSheet;
    private MyMenu menu;

    public MyLayout(Logic logic) {
        super(logic);
        tabSheet = new MyTabSheet();
        menu = new MyMenu("myMenu", this);
        logic.setDataToTable(container.loadAllData(), getTable()); 
        setBottomTabs(tabSheet);
        setNavigator(menu);
    }

    @Override
    protected ItemClickEvent.ItemClickListener editTableItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener selectTableItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener editTableAllItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener selectTableAllItemClick() {
        return itemClickEvent -> {
        };
    }
}

Где описывается поведение при выборе записи в таблице со всеми компонентами и текущим контейнером (методы ItemClickEvent.ItemClickListener), здесь они пустые. Также logic.setDataToTable(container.loadAllData(), getTable()) здесь описывается приме установки текущего контейнера в таблицу.


Реализация MenuNavigator в классе MyMenu:


public class MyMenu extends MenuNavigator {

    public MyMenu(String caption, Workspace parent) {
        super(caption, parent);
    }

    @Override
    public void add() {
        if (getAdd().getStyleName() == null)
            getAdd().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getAdd().setStyleName(null);
    }

    @Override
    public void delete() {
        if (getDelete().getStyleName() == null)
            getDelete().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getDelete().setStyleName(null);
    }

    @Override
    public void print() {
        if (getPrint().getStyleName() == null)
            getPrint().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getPrint().setStyleName(null);

    }
}

Где описывает изменение стиля нажатой кнопки и тем самым должен включаться другой режим.


И последний элемент описывающий графику MyTabSheet — реализация BottomTabs:


public class MyTabSheet extends BottomTabs {
    public MyTabSheet() {
        super();
    }

    @Override
    public void initTabs() {
        addCaption("Tab1", "Tab2", "Tab3", "Tab4");

        addComponent(new Label("label1"),
                new Label("label2"),
                new Label("label3"),
                new Label("label4"));

        addResource(FontAwesome.AMAZON,
                FontAwesome.AMAZON,
                FontAwesome.AMAZON,
                FontAwesome.AMAZON
        );
    }
}

Где создаются 4 закладки, в которые устанавливаются компоненты Label, и на все закладки ставится значок Amazon, не сочтите за рекламу, просто буква А идет первой.


В итоге получается вот такой интерфейс:


Картинка с GitHub


Заключение


Что-то много получилось для первого раза. Но да ладно В итоге получился простенький фреймворк, который позволяет быстро создавать новые интерфейсы для отображения разного набора данных и описывать логику работы с ними. Также планируется добавить компоненты которые позволять создавать редакторы справочников(списков), что будет являться интерфейсом для наполнения баз данных. Написать много-много тестов (буду рад предложениям о том, как тестировать такие штуки, потому что идей пока не появилось), а так же улучшать API и набор функций. Также создать функционал работы с базами данных.


PS


Данная реализация возникла в ходе производственной ситуации, и основное применение носит для конкретного заказчика, но я думаю этот проект можно использовать для решения других задач.


Благодарности


@djeckson за разработку класса для фильтрации FilterPanel и активное участие в проекте.


Ссылки


Ссылка на репозиторий, там же описание как подключить.

Share post

Comments 15

    +1
    а вот куба (cuba-platofrm на гитхабе) есть. не тыкали? а то у меня всё руки не дойдут…
    тоже ваадин для веб гуи, кстати.
      0

      Нет, но беглый просмотр сайта показал что интересная штука. Сейчас будет новый год, надо будет обязательно попробовать, посмотреть как они работу с БД реализовали? Просто мы для себя тоже парочку библиотек написали, надо будет их в public выложить и статью про них написать.


      По интерфейсу — платформа напоминает 1С, слишком много полей, а хотелось сделать чтобы доступ к любой информации был в 3 клика и менее нагруженный интерфейс.

        0
        там спринг и эклипслинк, судя по рекламным проспектам. надо код смотреть )
          0

          Ну в целом достаточно интересная вещь заправленная соусом импортозамещения. Если бы знал про нее год назад, может велосипед и не стали изобретать.

          0
          Если нужен custom-интерфейс, то вполне можно делать, Благо компонентов много: https://www.cuba-platform.com/online-demo
      0
      Практические реализации — это, конечно, очень хорошо, но что с производительностью? Как чувствует себя сервер при трансляции кода? Проект разрабатывается с 2002 года, но я еще не видел более или менее рабочих реализаций на нем (как и на остальных решений в виде Моцарта в плане CMS и других). Однако если брать не PHP-решения, то та же Нода просто взорвала интернеты уже через несколько месяцев после своего создания.
        0

        А тут встает вопрос, какая она должна быть?


        Где сейчас это планируется использоваться — это 500 машин на всем предприятии. Ну и 50 из них — максимум для онлайна в этом сервисе. По сравнению с тем же 1С — тут и производительность будет нормальная и есть места где можно оптимизировать программно.


        Ну и мы еще даже не вышли на нормальный уровень для пилотной эксплуатации. Но по нагрузочным тестам, вроде потянуть должно.

          0
          Ах это для десктопа пишется, не для веба? Ну тогда другое дело, да. А-то я уже стал представлять, что было бы с сервером, когда на такой сайт ломилось бы под сто тысяч юзеров одновременно. Да тут и тысячу думаю со скрипом переварит…
        0
        Как я понял, для нормальной работы Vaadin необходим хороший и стабильный интернет? Это по мне более критично, чем «мощные» клиенские машины. Попробовал демо на их сайте с эмуляцией слабого интернета и пару кейсов с пропаданием интернета — интерфеис начинает безумно скакать, передвинутое 2-3 раза окно оказывается не в конечной точке, а прыгает по мере того, как приходят ответы с сервера, а если не доходит, то и вообще оказывается на пару шагов позади. Как Вы решаете подобного рода проблемы?
          0

          Нет, интернет не нужен. Нужна хорошая стабильная сеть. Классически разворачивается локальный сервер приложений (WildFly, TomCat) и на него загружается ваше приложение (артифакт, он же war файл) и работа идет только с ним. Т.е. вы работаете в рамках своей локальной сети. А то что вы смотрели — это были demo из Интернета.


          ps
          Ну и при хорошем проектировании интерфейсов, лучше избегать окошек в браузерах (сугубо мое личное мнение).

            0
            Вы уверены, что для web приложения интернет не нужен? ))
            Вообще-то никто и никогда, и тем более авторы vaadin не говорили, что это только интранет решение.
            Тут вы очень сильно заблуждаетесь.
            Мы делалали на Vaadin достаточно сложный интерфейс (на уровне 1С решений). Интернет нужен больше стабильный чем быстрый. Некоторые сотрудники вполне с планшетиков через мобильный интерент работали без проблем.
            Собственно все описываемые yorlin проблемы, следствие серверно ориентированной архитектуры vaadin.
            У такого решения есть как плюсы так и минусы. Решать приходится в каждом конкретном случае.
              0

              Нет, вы полностью правы. Мое мнение основано на опыте использования в рамках интранет, ну и фреймворк тоже разрабатывался для работы в интранет. Инертность мышления, так сказать.


              Ну а в рамках корпоративных решений — интернет уходит на второй план.

              0
              «User interface components for web apps» — это с официального сайта. Для меня web — это все же больше интернет. Интересуюсь больше потому, что очень много вакансий последнее время стало, где опыт работы с vaadin желателен, но как то руки все не доходили. К сожалению даже интранет решения за частую не ограничиваются интранетом. Всегда есть отделы, которые желали бы иметь доступ к ERP через интернет(например через VPN). А быстрый и при том еще стабильный интернет, как показывает практика, не всегда имеется под рукой. И одно дело когда данные передаются медленно, а другое когда еще и интерфес тупит и прыгает.
              Идея конечно хорошая, но уж очень, по мне, специфичная.
                0

                Вы правы, придерживаясь своего предыдущего комментария скажу еще раз — инертность мышления.


                А насчет вакансий, есть интересный опрос о инструментах Java разработчиков в 2016 году, где Vaadin занимает 4 из 12 мест. Наверное это сказывается.

          Only users with full accounts can post comments. Log in, please.