Atlassian Jira. Путь костылье. Часть 1

Привет!

В этой серии статей я покажу Вам, как можно решить одну и ту же задачу в Atlassian Jira Server/Data Center разными способами.

В каждом решении я буду использовать разный набор знаний. Начну с решения низкого уровня (некачественное), которое можно сделать, не зная ничего про Atlassian Jira. Затем я объясню, почему это решение некачественное, и буду потихонечку двигаться к решению более высокого уровня.

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

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

Задача

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

Вот такой будет бизнес-процесс для тикетов:

А вот требования к процессу согласования:

  • во время создания тикета в поле Approver должен быть записан менеджер автора тикета (автор тикета находится в поле Reporter). Поле Approver должно быть не редактируемым во время создания тикета.
  • менеджера для каждого пользователя необходимо получать из справочника соответствий менеджера и пользователя. Этот справочник должен редактироваться пользователем lookupuser. У пользователя lookupuser нет доступа администратора к Jira и нет доступа к базе данных Jira.
  • только пользователь, который указан в поле Approver, может переводить тикет из статуса “On Approval” в статус “Ready For Work”.
  • если пользователь в поле Approver ушел из компании (пользователя деактивировали), то в каждом существующем тикете, где значение в поле Approver равно деактивированному пользователю, нужно изменить это значение на пользователя supermanager.

Решение самого низкого качества

Итак, мы ничего не знаем про Jira. Как будем решать задачу? Менеджер нам ее поставил и ждет результат к обеду.

Таблица в БД

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

Хорошо. Я использую базу данных Postgres, поэтому открою GUI для работы с этой БД и создам таблицу user_managers с полями user и manager:

Так, таблица есть. Теперь нам нужно ее как-то вести. По условию задачи вести ее должен отдельный пользователь, у которого нет доступа к БД.

Хорошо. Как будем делать?

JSP

Atlassian Jira работает на Tomcat. Может быть, как-то можно в него положить свою страницу HTML и затем вызвать ее из браузера?

Посмотрим в Tomcat:

Ага. Там есть файл login.jsp. Jsp это отличный для нас вариант: мы можем добавлять html, javascript, да еще и Java код на сервере выполнять. Вот это удача!

Давайте попробуем вызвать файл login.jsp напрямую:

Как видно, я вызвал login.jsp по вот такому адресу http://localhost:2990/jira/login.jsp, и страница отобразилась.

Так. А теперь давайте создадим свою jsp страницу и ее вызовем. Страницу назовем mypage.jsp. Там будет вот такой код:

<%@ page import="com.atlassian.jira.component.ComponentAccessor" %>
<%@ page import="com.atlassian.jira.util.mobile.JiraMobileUtils" %>
<%@ taglib uri="webwork" prefix="ww" %>
<%@ taglib uri="webwork" prefix="ui" %>
<%@ taglib prefix="page" uri="sitemesh-page" %>
<html>
<head>
	<title><ww:text name="'common.words.login.caps'"/></title>
    <meta name="decorator" content="login" />
</head>
<body>
    <p>Here is my JSP Page</p>
</body>
</html>

Все, что делает эта страница, это выводит сообщение “Here is my JSP page”. А теперь положим ее в Tomcat:

И теперь вызовем mypage.jsp по вот такому адресу http://localhost:2990/jira/mypage.jsp

Как видно, страница не была найдена. Означает ли это, что мы не можем вызывать наши собственные jsp страницы из Jira? Конечно, нет! В таких случаях помогает перезапуск Jira. Давайте, перезапустим Jira и вызовем mypage.jsp снова.

Ура! На этот раз наше сообщение было выведено на экран, что означает, что Jira увидела нашу jsp страницу.

Хорошо, теперь нам нужно добавить в нашу jsp страницу, какую-нибудь форму по ведению справочника менеджеров и пользователей.

Я не буду вручную создавать форму, а воспользуюсь каким-нибудь JavaScript фреймворком. Например, webix.

Я скачал все необходимые JavaScript файлы для webix и положил их в папку webix в Tomcat. Затем изменил содержимое файла mypage.jsp:

<%@ page import="com.atlassian.jira.component.ComponentAccessor" %>
<%@ page import="com.atlassian.jira.util.mobile.JiraMobileUtils" %>
<%@ taglib uri="webwork" prefix="ww" %>
<%@ taglib uri="webwork" prefix="ui" %>
<%@ taglib prefix="page" uri="sitemesh-page" %>
<%
String tableData = "[{ \"id\":1, \"user\":\"User1\", \"manager\":\"Manager1\"},{ \"id\":2, \"user\":\"User2\", \"manager\":\"Manager2\"}]";
%>
<html>
<head>
	<meta name="decorator" content="login" />
    <title>Set Managers for Users</title>
    <link rel="stylesheet" href="webix/codebase/webix.css?v=7.3.0" type="text/css" charset="utf-8">
    <script src="webix/codebase/webix.js?v=7.3.0" type="text/javascript" charset="utf-8"></script>
</head>
<body>
    <div class='header_comment'>Managers</div>
		<div id="testA" style='height:600px'></div>
		<hr>
		
		<script type="text/javascript" charset="utf-8">

		webix.ready(function(){
			grida = webix.ui({
				container:"testA",
				view:"datatable",
				columns:[
					{ id:"user",	header:"User", 			width:200 },
					{ id:"manager",	header:"Manager",		width:120 }
				],
				
				editable:true,
				editaction:"dblclick",
				autoheight:true,
				autowidth:true,

				data:'<%= tableData%>'
			});	
		});
		</script>
</body>
</html>

Этим кодом я вывожу элемент datatable из webix:

  • я объявил переменную tableData и присвоил ей json с мэппингом менеджеров и пользователей. В последствии мы будем брать эту информацию из нашей таблицы БД
  • я импортировал все необходмые JavaScript файлы для того, чтобы webix заработал
  • отобразил таблицу

Мы можем видеть данные в этой таблице и редактировать эти данные.

Теперь нам все-таки нужно выбрать данные из таблицы user_managers, а не зашивать данные в код. Как это сделать?

Посмотрите вот на этот код в файле mypage.jsp:

<%@ page import="com.atlassian.jira.component.ComponentAccessor" %>
<%@ page import="com.atlassian.jira.util.mobile.JiraMobileUtils" %>
<%@ taglib uri="webwork" prefix="ww" %>
<%@ taglib uri="webwork" prefix="ui" %>
<%@ taglib prefix="page" uri="sitemesh-page" %>
<%
String tableData = "[{ \"id\":1, \"user\":\"User1\", \"manager\":\"Manager1\"},{ \"id\":2, \"user\":\"User2\", \"manager\":\"Manager2\"}]";
%>
<%@ page import="com.atlassian.jira.component.ComponentAccessor" %>

Мы импортируем ComponentAccessor, который является классом Jira Java Api. После того, как мы этот класс импортировали, мы можем его использовать в коде нашего mypage.jsp:

<%
JiraAuthenticationContext jac = ComponentAccessor.getJiraAuthenticationContext();
%>

Конечно, чтобы объявить переменную класса JiraAuthenticationContext, мы должны импортировать и класс JiraAuthenticationContext. Но это уже детали. Из этого кода мы видим, что мы легко можем работать с Jira Java API.

Кроме того, мы можем импортировать и классы из пакета java.sql, для того, чтобы выбрать данные из таблицы user_managers.

Вот код для получения данных из таблицы user_managers:

<%@ page import="com.atlassian.jira.component.ComponentAccessor" %>
<%@ page import="com.atlassian.jira.util.mobile.JiraMobileUtils" %>
<%@ page import="import java.sql.Connection" %>
<%@ page import="import java.sql.DriverManager" %>
<%@ page import="import java.sql.PreparedStatement" %>
<%@ page import="import java.sql.ResultSet" %>
<%@ taglib uri="webwork" prefix="ww" %>
<%@ taglib uri="webwork" prefix="ui" %>
<%@ taglib prefix="page" uri="sitemesh-page" %>
<%
try {

                String tableData = "";
                String user = "";
                String manager = "";
                Connection dbc = null; 
                String url = "jdbc:postgresql://localhost:5432/jira?user=jira&password=secret";
                Connection dbc = DriverManager.getConnection(url);
                ResultSet rs = null;
                String sql;
                PreparedStatement pst;
                sql = "select user, manager from user_managers";
                pst = dbc.prepareStatement(sql);
                rs = pst.executeQuery();

    
                while (rs.next()) {
                   /* 
Form the tableData variable here
                   */
                }
    
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
     
%>
<html>
<head>
	<meta name="decorator" content="login" />
    <title>Set Managers for Users</title>
    <link rel="stylesheet" href="webix/codebase/webix.css?v=7.3.0" type="text/css" charset="utf-8">
    <script src="webix/codebase/webix.js?v=7.3.0" type="text/javascript" charset="utf-8"></script>
</head>
<body>
    <p> <%= user%> <%= manager%></p>
    <div class='header_comment'>Managers</div>
		<div id="testA" style='height:600px'></div>
		<hr>
		
		<script type="text/javascript" charset="utf-8">

		webix.ready(function(){
			grida = webix.ui({
				container:"testA",
				view:"datatable",
				columns:[
					{ id:"user",	header:"User", 			width:200 },
					{ id:"manager",	header:"Manager",		width:120 }
				],
				
				editable:true,
				autoheight:true,
				autowidth:true,

				data:'<%= tableData%>'
			});	
		});
		</script>
</body>
</html>

Я не буду делать работающее решение. Я думаю, из того, что я рассказал про jsp, Вы понимаете, как это работает.

Для того, чтобы сделать решение работающим, нужно добавить код по получению текущего пользователя, стравнить его с lookupuser. Если пользователь lookupuser, то выводить таблицу с мэппингом менеджеров и пользователей, если пользователь не lookupuser, то выводить какое-нибудь сообщение об ошибке. Дальше нужно добавить возможность по добавлению, удалению и сохранению данных в таблицу. И после этого у Вас будет рабочее решение по ведению справочника менеджеров и пользователей через UI Jira.

JavaScript

Хорошо. Мы можем вести справочник менеджеров и пользователей. Теперь давайте заполним поле Approver менеджером из справочника для пользователя, указанного в поле Reporter.

Это мы будем делать при создании тикета. И вот так выглядит экран создания тикета:

Как Вы видите, у нас есть поле Reporter и Approver. Если в поле Reporter указан Alexey Matveev, то нам нужно в поле Approver записать менеджера Алексея Матвеева из справочника менеджеров и пользователей.

Мы знаем, что экран создания тикета, это HTML и JavaScript. Мы можем открыть код этого экрана в Developer Tools в браузере Chrome и посмотреть, как там все устроено:

Мы видим, что в поле Reporter, у нас сейчас записано значение admin, и если я захочу найти это поле с помощью JQuery, то я могу найти его по тегу “optgroup” с id “reporter-group-suggested”, а потом по потомку с тегом “option” и атрибутом “selected”.

Теперь давайте посмотрим на поле Approver:

Как Вы видите, значение этого поля пустое, и я могу найти это поле по id customfield_10300.

Хорошо. Добавим менеджера для пользователя admin в наш справочник.

Далее нам нужно написать код на JavaScript, который найдет элемент с тегом “option” с атрибутом “selected” и этот элемент должен быть потомком элемента с тегом “optgroup” с id “reporter-group-suggested”. После того, как мы нашли этот элемент, мы возьмем значение из этого элемента (это значение в поле Reporter), выберем менеджера для этого пользователя в справочнике и запишем этого менеджера в элемент с id “customfield_10300”.

Хорошо. Код напишем, а как его потом сделать так, чтобы Jira его выполняла при отображении страницы?

Есть несколько способов. Наиболее используемый это записать этот код в announcement banner. Так и сделаем.

Сначала напишем JavaScript код:

<script>
      setInterval(function(){ 
         if ($("optgroup#reporter-group-suggested > option:selected").val()) {
             $("#customfield_10300").val("manager1");
         }
      }, 3000);
</script>

Этот JavaScript код делает следующее:

  • запускает цикл. Если мы не запустим цикл, то скрипт отработает еще до появления поля Reporter. В результате менеджер не выберется и поле Approver не заполнится.
  • каждые 3 секунды, мы проверяем, а не появилось ли поле Reporter, и если оно появилось, то мы берем значение из этого поля, а в поле Approver устанавливаем значение manager1. Да, нам нужно брать значение для поля Approver из базы данных, но это мы потом допишем.

Добавляем код в announcement banner:

И теперь если я буду создавать тикет, то поле Approver заполнится значением manager1:

Нажем на кнопку Create и посмотрим на созданный тикет:

Мы видим, что в поле Approver записано manager1. Все хорошо.

Теперь нам нужно сделать так, чтобы поле Approver нельзя было бы редактировать. Для этого добавим полю Approver свойство disabled:

$("#customfield_10300").prop( "disabled", true );

Вот полный код:

<script>
      setInterval(function(){ 
         if ($("optgroup#reporter-group-suggested > option:selected").val()) {
            $("#customfield_10300").val("manager1");
            $("#customfield_10300").prop( "disabled", true );
 
         }
      }, 3000);
</script>

И теперь при создании тикета поле Approver недоступно для редактирования:

Отлично! Нажмем на кнопку Create и посмотрим на созданный тикет:

Хммм. Поле Approver не заполнено. Похоже проблема в нашем добавленном свойстве disabled.

Не пойдет! Сейчас что-нибудь придумаем!

Cразу вспоминается аватар Андрея Маркелова на его AM utils:

Ладно, вернемся к задаче.

А давайте, будем не свойство disabled добавлять, а просто не давать устанавливать фокус в это поле. Тогда значение в этом поле тоже нельзя будет отредактировать.

Хорошая мысль! А вот и код:

<script>
      setInterval(function(){ 
         if ($("optgroup#reporter-group-suggested > option:selected").val()) {
            $("#customfield_10300").val("manager1");
            $("#customfield_10300").focus(function() {
                 this.blur();
            });
         }
      }, 3000);
</script>

Посмотрим на экран создания тикета:

Мы не можем редактировать поле Approver.

Теперь нажмем на кнопку Create и посмотрим на созданный тикет:

Отлично! Поле Approver заполнилось.

И еще немного JS

Ну, что ж. Теперь мы короли Jira. Мы можем поправить весь интерфейс в Jira. Давайте еще что-нибудь на ггг кодим.

Нам нужно сделать так, чтобы только пользователь в поле Approver смог переводить тикет из статуса “On Approval” в статус “Ready For Work”.

Да, это просто. Нужно просто проверить, что текущий пользователь равен пользователю в поле Approver, и спрятать кнопку “Ready For Work”:

Так, снова идем в Developer Tools, ищем, как можно найти кнопку “Ready For Work”, и пишем код, чтобы спрятать эту кнопку. А вот и код:

 setInterval(function(){ 
         if ($("#customfield_10300-val").text()) {
           if (JIRA.Users.LoggedInUser.userName() == $.trim($("#customfield_10300-val").text())) {
            $('#action_id_11').addClass('hidden'); ;
           }
         }
      }, 3000);
  • запускаем цикл. Без него ничего работать не будет. Но нас это не пугает. Мы же уже эксперты.
  • проверяем, что текущий пользователь равен пользователю в поле Approver
  • и если да, то добавляем class hidden кнопке “Ready For Work”

Теперь откроем тикет под пользователем admin:

И видим, что кнопка “Ready For Work” отсутствует.

А теперь зайдем под пользователем manager1, и кнопка появится:

Если Вам интересно, как я нашел метод JIRA.Users.LoggedInUser.userName(), то если Вы ведете JIRA в консоли Developer Tools, то увидите много полезных методов. О том, стоит ли их использовать, поговорим позже.

И еще немного JSP

Итак, мы устанавливаем значение manager1 в поле Approver. Но нам же нужно брать данные из таблицы user_managers в БД. Как это сделать?

Ответ простой. Используем JSP. Создадим страницу getmanagerforuser.jsp вот с таким кодом:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
try {
   String reporter=request.getParameter("reporter");
   response.getWriter().write("{\"reporter\":\""+ reporter+ "\",\"manager\":\"manager1\"}");

}   catch(Exception e)  {
}
%>

Этот код получает значение параметра, который передается при вызове getmanagerforuser.jsp, выдает json, где в поле reporter записывается значение параметра, а в поле manager – manager1.

Давайте вызовем нашу jsp страницу:

Как Вы видите, в качестве параметра reporter мы передали myreporter. И значение этого параметра наша jsp страница увидела и вывела в ответе.

Очень хорошо! Теперь вызовем нашу jsp страницу из JavaScript кода. Мы в нее передадим занчение из поля Reporter и из полученного json возьмем поле manager и запишем в поле Approver.

Вот код:

            $.get( "/jira/getmanagerforuser.jsp?reporter="+ $("optgroup#reporter-group-suggested > option:selected").val(), function( data ) {
                 $("#customfield_10300").val(data.manager);
            });

Теперь откроем экран создание тикета:

И тишина. Никого значения в поле Approver. Что-то пошло не так. Давайте искать проблему в Developer Tools:

Так, jsp страница вызвалась корректно. Значение параметра reporter равно admin. Тоже хорошо. Так, давайте ответ смотреть:

Как видно наш json это только маленькая часть ответа. Нужно его оттуда достать.

Достанем. Вот код:

 $.get( "/jira/getmanagerforuser.jsp?reporter="+ $("optgroup#reporter-group-suggested > option:selected").val(), function( data ) {
                 var startIndex = data.indexOf("<section id=\"content\" role=\"main\">") +34;
                 var endIndex = data.indexOf("</section>", startIndex);
                 var str = data.substring(startIndex, endIndex);
                 $("#customfield_10300").val(JSON.parse(str.trim()).manager);
            });

И теперь, если мы создадим тикет, то поле Approver будет заполено значением manager1:

А вот наш финальный JavaScript код:

<script>
      setInterval(function(){ 
         if ($("optgroup#reporter-group-suggested > option:selected").val()) {
            $.get( "/jira/getmanagerforuser.jsp?reporter="+ $("optgroup#reporter-group-suggested > option:selected").val(), function( data ) {
                 var startIndex = data.indexOf("<section id=\"content\" role=\"main\">") +34;
                 var endIndex = data.indexOf("</section>", startIndex);
                 var str = data.substring(startIndex, endIndex);
                 $("#customfield_10300").val(JSON.parse(str.trim()).manager);
            });

            
            $("#customfield_10300").focus(function() {
                 this.blur();
            });
         }
      }, 3000);
      setInterval(function(){ 
         if ($("#customfield_10300-val").text()) {
           if (JIRA.Users.LoggedInUser.userName() != $.trim($("#customfield_10300-val").text())) {
            $('#action_id_11').addClass('hidden');
           }
         }
      }, 3000);
</script>

Я не буду реализовывать получение данных из таблицы user_managers в getmanagerforuser.jsp. Я думаю, что Вы понимаете, как это реализовать.

Осталось выполнить последнее требование и пирожок с полки у нас в кармане.

DB triggers and functions

Если менеджера деактивировали, то нужно во всех существующих тикетах, посмотреть поле Approver, и, если там деактивированный пользователь, то заменить этого пользователя на пользователя supermanager.

Ничего не может быть проще. Мы напишем тригер в базе данных на таблицу cwd_user. Если значение поля active изменилось с 1 на 0, то нужно проапдейтить таблицу customfieldvalue так, чтобы в строках с customfield = 10300 и stringvalue = менеджеру, которого деактивировали, проставить в поле stringvalue значение supermanager.

Сначала напишем функцию:

CREATE OR REPLACE FUNCTION public.changeusermanager()
	RETURNS trigger
	LANGUAGE plpgsql
AS $function$
	BEGIN
		IF NEW.active <> OLD.active and NEW.active = 0 THEN
		 	update customfieldvalue set stringvalue = 'supermanager' where customfield  = 10300 and stringvalue = NEW.user_name; 
		END IF;

		RETURN NEW;
	END;
$function$
;

А теперь тригер, который дергает эту функцию:

create trigger changemanager before
update
    on
    public.cwd_user for each row execute procedure public.changeusermanager();

Посмотрим, как это работает. Зайдем в управление пользователями в Jira и снимем галочку Active для пользователя manager1:

Нажмем на кнопку Update и посмотрим на какой-нибудь тикет:

Да, значение в поле Approver изменилось на supermanager. Работает!

Итак, мы решили задачу и можем идти к нашему менеджеру. Он будет доволен, так как код он смотреть, конечно же, не будет. И если у нас нет тестировщиков, то мы в шоколаде. Премия у нас в кармане. А вот если есть тестировщики, то боюсь, что выйти в прод у нас не получится. Но это все будет завтра, а сейчас мы счастливы.

Заключение к плохому решению

Для этого решения мы использовали следующие подходы: JSP страницы, JavaScript код в самом плохом его варианте, триггеры и процедуры баз данных.

Если Вы обнаружили такие подходы в Вашей Jira, то нужно задаться вопросом, а хорошего ли качества Ваше решение в Jira?

Кроме того, если Ваш Jira администратор/разработчик смотрит в Developer Tools и на основании увиденного пишет большое количество JavaScript кода, то это сигнал для того, что Вашему администратору/разработчику Jira нужно обучение. Иначе Ваше решение будет очень низкого качества.

А в следующих частях я расскажу, почему эти подходы плохи, и самое главное, как сделать решение лучше.

One thought on “Atlassian Jira. Путь костылье. Часть 1

Leave a Reply

%d bloggers like this: