React y Atlaskit en el servidor Atlassian y los complementos del centro de datos

¡Hola a todos!

En este artículo, me gustaría hablar sobre cómo usar React y Atlaskit en los complementos de Atlassian en entornos de servidores y centros de datos.

Introducción


Actualmente, si está desarrollando complementos para productos Atlassian para Servidor y Centro de datos, entonces vm , soy , requirejs , jquery , backbone están disponibles para que pueda desarrollar la interfaz de usuario desde la caja . Aquí aquí se puede leer mi artículo sobre cómo utilizar out disponible de las bibliotecas de la caja.

Esta pila de tecnología está desactualizada y me gustaría usar algo más nuevo. Elegí el mecanografiado y reaccioné como la pila . Además, para facilitar la transferencia de complementos de servidor a la nube, elegí la biblioteca de elementos de interfaz de usuario atlaskit .

En este artículo hablaré sobre Atlassian Jira, pero se puede usar el mismo enfoque para otros servidores Atlassian y productos de centros de datos.

Para reproducir los ejemplos de este artículo, debe instalar git y Atlassian SDK .

¡Vamos a empezar!

Instale el arquetipo de Maven y cree un nuevo proyecto.


Creé el arquetipo de Maven para que sea más fácil crear un nuevo proyecto que ya contenga todas las configuraciones necesarias para crear un complemento con React y Atlaskit.

Si no desea utilizar el arquetipo, puede tomar el complemento ya creado de este arquetipo e ir a la parte que ensamblamos y lanzar el proyecto.

Clonar el arquetipo de mi repositorio Bitbucket:

git clone https://alex1mmm@bitbucket.org/alex1mmm/jira-react-atlaskit-archetype.git --branch v1 --single-branch

Vaya a la carpeta de la carpeta jira-react-atlaskit-archetype e instale este arquetipo en su repositorio local de Maven:

cd jira-react-atlaskit-archetype
atlas-mvn install

Después de eso, vaya a la carpeta un nivel más alto y cree un nuevo proyecto basado en este arquetipo:

cd ..
atlas-mvn archetype:generate -DarchetypeCatalog=local

Se le hará una pregunta para seleccionar el arquetipo necesario. Aquí están mis arquetipos en el repositorio local:

1: local -> com.cprime.jira.sil.extension:sil-extension-archetype (This is the com.cprime.jira.sil.extension:sil-extension plugin for Atlassian JIRA.)
2: local -> ru.matveev.alexey.sil.extension:sil-extension-archetype (This is the ru.matveev.alexey.sil.extension:sil-extension plugin for Atlassian JIRA.)
3: local -> ru.matveev.alexey.atlas.jira:jira-react-atlaskit-archetype-archetype (jira-react-atlaskit-archetype-archetype)

Es necesario elegir ru.matveev.alexey.atlas.jira: jira-react-atlaskit-archetype-archetype, por lo que indiqué el número 3 como respuesta.

Luego debe especificar groupid y artifactid para el nuevo proyecto:

Define value for property 'groupId': react.atlaskit.tutorial
Define value for property 'artifactId': my-tutorial
Define value for property 'version' 1.0-SNAPSHOT: : 
Define value for property 'package' react.atlaskit.tutorial: : 
Confirm properties configuration:
groupId: react.atlaskit.tutorial
artifactId: my-tutorial
version: 1.0-SNAPSHOT
package: react.atlaskit.tutorial
 Y: : Y

Montamos e instalamos el proyecto


En mi caso, el nuevo proyecto está en la carpeta my-tutorial. Vayamos a esta carpeta y compilemos el proyecto:

cd my-tutorial
atlas-mvn package

Después de ensamblar el proyecto, vaya a la carpeta del backend y ejecute Atlassian Jira:

cd backend
atlas-run

Complemento de prueba


Después de que el Atlassian Jira haya comenzado, vaya al navegador en la siguiente dirección:

http://localhost:2990/jira

Debe iniciar sesión como administrador: admin e ir a Gear -> Administrar aplicaciones.



Verá un menú de nuestro complemento. Pero antes de comenzar nuestros elementos Atlaskit, vaya a Sistema -> Registro y creación de perfiles y establezca el nivel de registro INFO para el paquete react.atlaskit.tutorial.servlet.



Ahora regrese a Administrar aplicaciones y haga clic en el menú Formulario. Veremos el formulario de ingreso de datos que se mostró usando el elemento Formulario Atlaskit :



Complete todos los campos de texto y haga clic en el botón Enviar:



ahora, si abre el archivo atlassian-jira.log, verá algo como esto:

2020-05-10 08:44:29,701+0300 http-nio-2990-exec-4 INFO admin 524x12509x1 15awhw2 0:0:0:0:0:0:0:1 /plugins/servlet/form [r.a.tutorial.servlet.FormServlet] Post Data: {"firstname":"Alexey","lastname":"Matveev","description":"No description","comments":"and no comments"}

Esto significa que al hacer clic en el botón Enviar, nuestros datos se transfirieron con éxito al servlet que sirve este formulario, y este servlet mostró los datos ingresados ​​en un archivo de registro.
Ahora seleccionemos el menú Tabla dinámica. Verá el elemento Atlaskit Dynamic Table :



eso es todo lo que hace nuestro complemento. ¡Ahora veamos cómo funciona todo!

Plugin interno


Aquí está la estructura de nuestro complemento: el



backend contiene el complemento Atlassian Jira que se creó utilizando el SDK de Atlassian.
La interfaz contiene los elementos de la interfaz de usuario que utilizará nuestro complemento.
pom.xml archivo pom en el que se definen dos módulos:

    <modules>  
        <module>frontend</module>
        <module>backend</module>
    </modules>

Ahora echemos un vistazo a la carpeta con frente.

Interfaz


La carpeta frontend contiene los siguientes archivos:



Describiré los archivos principales.

package.json es un archivo de configuración para npm que contiene la siguiente información:

  • lista de paquetes de los que depende nuestro proyecto.
  • versiones de los paquetes que usamos.

Utilizaremos paquetes como mecanografiado, atlaskit, babel y otros.

.babel.rc: archivo de configuración para Babel . Babel se utiliza para traducir el código ECMAScript 2015+ en código JavaScript. Escribiremos nuestro código de mecanografía, por lo que debemos traducirlo al código JavaScript para que el complemento Jira pueda funcionar con él.

webpack.config.js: archivo de configuración para webpack . Webpack procesa nuestra aplicación, crea un gráfico de dependencia y genera un paquete que contiene todo el JavaScript necesario para que nuestra aplicación funcione. Para que el complemento Jira funcione con nuestro JavaScript, necesitamos un archivo Javascript para cada punto de entrada. En nuestro caso, los puntos de entrada son los elementos del menú Forma y Tabla dinámica.

Aquí está el contenido del archivo webpack.config.js:

const WrmPlugin = require('atlassian-webresource-webpack-plugin');
var path = require('path');module.exports = {
    module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: {
                loader: "babel-loader"
            }
          }
        ]
    },
    entry: {
            'form': './src/form.js',
            'dynamictable': './src/dynamictable.js'
    },

    plugins: [
        new WrmPlugin({
            pluginKey: 'ru.matveev.alexey.atlas.jira.jira-react-atlaskit',
            locationPrefix: 'frontend/',
            xmlDescriptors: path.resolve('../backend/src/main/resources', 'META-INF', 'plugin-descriptors', 'wr-defs.xml')
        }),
    ],
    output: {
        filename: 'bundled.[name].js',
        path: path.resolve("./dist")
    }
};

Como puede ver, usamos atlassian-webresource-webpack-plugin .

Es necesario para que después de que webpack haya creado un archivo JavaScript, este archivo se agregue automáticamente como un recurso web:

plugins: [
        new WrmPlugin({
            pluginKey: 'ru.matveev.alexey.atlas.jira.jira-react-atlaskit',
            locationPrefix: 'frontend/',
            xmlDescriptors: path.resolve('../backend/src/main/resources', 'META-INF', 'plugin-descriptors', 'wr-defs.xml')
        }),
    ],

Como resultado de esta configuración, después de compilar el módulo frontend, el archivo wr-defs.xml se creará en la carpeta backend / src / resources / META-INF / plugin-descriptors.

El parámetro locationPrefix nos permite especificar la ubicación de los archivos JavaScript en el complemento Jira. En nuestro caso, indicamos que los archivos se ubicarán en la carpeta backend / src / resources / frontend. Pondremos los archivos JavaScript en esta carpeta más adelante en el módulo de back-end, pero ahora este parámetro nos permite obtener dicha línea en el archivo wr-defs.xml <resource type = "download" name = "bundled.dynamictable.js" location = "frontend / bundled.dynamictable.js » />.

Aquí está el contenido del archivo wr-defs.xml que se creó durante el proceso de compilación del proyecto:

<bundles>
  <web-resource key="entrypoint-form">
    <transformation extension="js">
      <transformer key="jsI18n"/>
    </transformation>
    <context>form</context>
    <dependency>com.atlassian.plugins.atlassian-plugins-webresource-plugin:context-path</dependency>
    <resource type="download" name="bundled.form.js" location="frontend/bundled.form.js"/>
  </web-resource>
  <web-resource key="entrypoint-dynamictable">
    <transformation extension="js">
      <transformer key="jsI18n"/>
    </transformation>
    <context>dynamictable</context>
    <dependency>com.atlassian.plugins.atlassian-plugins-webresource-plugin:context-path</dependency>
    <resource type="download" name="bundled.dynamictable.js" location="frontend/bundled.dynamictable.js"/>
  </web-resource>
  <web-resource key="assets-632cdd38-e80f-4a5a-ba4c-07ba7cb36e60">
    <transformation extension="js">
      <transformer key="jsI18n"/>
    </transformation>
    <transformation extension="soy">
      <transformer key="soyTransformer"/>
      <transformer key="jsI18n"/>
    </transformation>
    <transformation extension="less">
      <transformer key="lessTransformer"/>
    </transformation>
  </web-resource>
</bundles>

Como puede ver, tenemos secciones adicionales de recursos web en las que se definen los archivos JavaScript creados por webpack. Todo lo que nos queda es decirle a Jira, para que al instalar nuestro complemento, también usemos recursos web de la carpeta backend / src / resources / META-INF / plugin-descriptor. Para hacer esto, realizamos los siguientes cambios en el archivo backend / pom.xml:

<plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>jira-maven-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <productVersion>${jira.version}</productVersion>
                    <productDataVersion>${jira.version}</productDataVersion>
                    <compressResources>false</compressResources>
                    <enableQuickReload>true</enableQuickReload>
                    <instructions>
                        <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>
                        <Export-Package></Export-Package>
                        <Import-Package>org.springframework.osgi.*;resolution:="optional", org.eclipse.gemini.blueprint.*;resolution:="optional", *</Import-Package>
                        <!-- Ensure plugin is spring powered -->
                        <Spring-Context>*</Spring-Context>
                        <Atlassian-Scan-Folders>META-INF/plugin-descriptors</Atlassian-Scan-Folders>
                    </instructions>
                </configuration>
            </plugin>

Hemos agregado <Atlassian-Scan-Folders> META-INF / plugin-descriptors </Atlassian-Scan-Folders>. Este parámetro le dirá a Jira que es necesario buscar recursos web adicionales en la carpeta META-INF / plugin-descriptors.

También agregamos <compressResources> false </compressResources> para deshabilitar la minificación de nuestros archivos JavaScript. Ya han sido minificados.
También definimos dos puntos de entrada a nuestra aplicación en el archivo webpack.config.js:

 entry: {
            'form': './src/form.js',
            'dynamictable': './src/dynamictable.js'
    },

Esto significa que webpack escaneará los archivos ./src/form.js y ./src/dynamictable.js y creará dos archivos JavaScript, cada uno de los cuales es un archivo para uno de los puntos de entrada. Estos archivos se crearán en la carpeta frontend / dist.

./src/form.js y ./src/dynamictable.js no contienen nada especial. Tomé la mayor parte del código de ejemplos en Atlaskit.

Aquí está el contenido del archivo form.js:

import Form from "./js/components/Form";

Aquí hay solo una línea que importa la clase del archivo ./js/components/Form.js.

Aquí está el contenido del archivo ./js/components/Form.js:

import React, { Component } from 'react';
import ReactDOM from "react-dom";
import Button from '@atlaskit/button';
import TextArea from '@atlaskit/textarea';
import TextField from '@atlaskit/textfield';
import axios from 'axios';

import Form, { Field, FormFooter } from '@atlaskit/form';

export default class MyForm extends Component {
  render() {
  return (
  <div
    style={{
      display: 'flex',
      width: '400px',
      margin: '0 auto',
      flexDirection: 'column',
    }}
  >
    <Form onSubmit={data => axios.post(document.getElementById("contextPath").value + "/plugins/servlet/form", data)}>
      {({ formProps }) => (
        <form {...formProps} name="text-fields">
          <Field name="firstname" defaultValue="" label="First name" isRequired>
            {({ fieldProps }) => <TextField {...fieldProps} />}
          </Field>

          <Field name="lastname" defaultValue="" label="Last name" isRequired>
            {({ fieldProps: { isRequired, isDisabled, ...others } }) => (
              <TextField
                disabled={isDisabled}
                required={isRequired}
                {...others}
              />
            )}
          </Field>
          <Field
            name="description"
            defaultValue=""
            label="Description"
          >
            {({ fieldProps }) => <TextArea {...fieldProps} />}
          </Field>

          <Field
            name="comments"
            defaultValue=""
            label="Additional comments"
          >
            {({ fieldProps }) => <TextArea {...fieldProps} />}
          </Field>
          <FormFooter>
            <Button type="submit" appearance="primary">
              Submit
            </Button>
          </FormFooter>
        </form>
      )}
    </Form>
  </div>
);
}
}
window.addEventListener('load', function() {
    const wrapper = document.getElementById("container");
    wrapper ? ReactDOM.render(<MyForm />, wrapper) : false;
});</code></pre>
<!-- /wp:code -->       :<!-- wp:code -->
<pre class="wp-block-code"><code>window.addEventListener('load', function() {
    const wrapper = document.getElementById("container");
    wrapper ? ReactDOM.render(<MyForm />, wrapper) : false;
});

Aquí muestro el componente MyForm en un contenedor div. Este contenedor se definirá en la plantilla de complemento de Jira de soja.

También preste atención a esta línea:

onSubmit={data => axios.post(document.getElementById("contextPath").value + "/plugins/servlet/form", data)}

document.getElementById ("contextPath"). value obtiene el valor del campo con id contextPath. Defino este campo en la plantilla de soja en el complemento Jira. El valor en este campo proviene del servlet al que está vinculado el elemento de menú Formulario. En mi caso, contextPath contiene el valor / jira, ya que al iniciar Jira desde el SDK de Atlassian, se establece esta ruta de contexto.

Y se trata de frontend. Como resultado del ensamblaje del módulo frontend, obtenemos dos archivos JavaScript en la carpeta frontend / dist y xml con recursos web adicionales en la carpeta backend / src / resources / META-INF / plugin-descriptors.

Ahora pasemos al backend.

Backend


Agregué estos complementos al archivo backend / pom.xml:

<plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>properties-maven-plugin</artifactId>
                <version>1.0.0</version>
                <executions>
                    <execution>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>write-project-properties</goal>
                        </goals>
                        <configuration>
                            <outputFile>${project.build.outputDirectory}/maven.properties</outputFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>copy frontend files to resources</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>src/main/resources/frontend</outputDirectory>
                            <overwrite>true</overwrite>
                            <resources>
                                <resource>
                                    <directory>../frontend/dist</directory>
                                    <includes>
                                        <include>*.*</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

properties-maven-plugin crea un archivo maven.properties que contiene todas las propiedades de Maven. Necesito la propiedad atlassian.plugin.key para llamar a recursos web desde servlets que están vinculados a los elementos de menú de nuestro complemento.

maven-resources-plugin recoge archivos JavaScript de la carpeta frontend / dist y los copia en la carpeta backend / resources / frontend.

Luego creé elementos de menú e hice una llamada a servlets desde estos elementos de menú.

Aquí están las líneas del archivo atlassian-plugin.xml:

<servlet name="Form Servlet" i18n-name-key="form-servlet.name" key="form-servlet" class="react.atlaskit.tutorial.servlet.FormServlet"> 
    <description key="form-servlet.description">The Form Servlet Plugin</description>  
    <url-pattern>/form</url-pattern>
  </servlet>  
  <servlet name="Dynamic Table Servlet" i18n-name-key="dynamic-table-servlet.name" key="dynamic-table-servlet" class="react.atlaskit.tutorial.servlet.DynamicTableServlet"> 
    <description key="dynamic-table-servlet.description">The Dynamic Table Servlet Plugin</description>  
    <url-pattern>/dynamictable</url-pattern>
  </servlet>
  <web-section name="React Plugin" i18n-name-key="react.name" key="react" location="admin_plugins_menu" weight="1000">
    <description key="react.description">React Plugin</description>
    <label key="react.label"/>
  </web-section>
  <web-item name="from web item" i18n-name-key="form.name" key="form" section="admin_plugins_menu/react" weight="1000">
    <description key="form.description">Form</description>
    <label key="form.label"/>
    <link linkId="configuration-link">/plugins/servlet/form</link>
  </web-item>
  <web-item name="dynamic table web item" i18n-name-key="dynamictable.name" key="dynamictable" section="admin_plugins_menu/react" weight="1000">
    <description key="dynamictable.description">Dynamic Table</description>
    <label key="dynamictable.label"/>
    <link linkId="configuration-link">/plugins/servlet/dynamictable</link>
  </web-item>

Entonces, tenemos menús y servlets que se llaman desde estos elementos de menú.
Ahora veamos los servlets:

FormServlet.java:

public class FormServlet extends HttpServlet{
    private static final Logger log = LoggerFactory.getLogger(FormServlet.class);
    private final ResourceService resourceService;
    private final SoyTemplateRenderer soyTemplateRenderer;

    public FormServlet(@ComponentImport  SoyTemplateRenderer soyTemplateRenderer, ResourceService resourceService) {
        this.resourceService = resourceService;
        this.soyTemplateRenderer = soyTemplateRenderer;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
        String pluginKey = this.resourceService.getProperty("atlassian.plugin.key");
        Map<String, Object> map = new HashMap<>();
        map.put("contextPath", req.getContextPath());

        String html = soyTemplateRenderer.render(pluginKey + ":jira-react-atlaskit-resources", "servlet.ui.form", map);

        resp.setContentType("text/html");
        resp.getWriter().write(html);
        resp.getWriter().close();    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        StringBuffer jb = new StringBuffer();
        String line = null;
        try {
            BufferedReader reader = req.getReader();
            while ((line = reader.readLine()) != null)
                jb.append(line);
        } catch (Exception e) { /*report an error*/ }
        log.info(String.format("Post Data: %s", jb.toString()));

        String pluginKey = this.resourceService.getProperty("atlassian.plugin.key");
        Map<String, Object> map = new HashMap<>();
        map.put("contextPath", req.getContextPath());

        String html = soyTemplateRenderer.render(pluginKey + ":jira-react-atlaskit-resources", "servlet.ui.form", map);

        resp.setContentType("text/html");
        resp.getWriter().write(html);
        resp.getWriter().close();
    }}

Defino dos variables resourceService y soyTemplateRenderer e inicializo estas variables en el constructor de la clase. resourceService: un bean que lee las propiedades del archivo maven.properties. soyTemplateRenderer - Jira bean que puede llamar plantillas de soja.
En el método doGet, obtengo el valor de la propiedad atlassian.plugin.key y la ruta de contexto. Luego paso la ruta de contexto como parámetro a la plantilla de soja y llamo a la plantilla de soja bajo el nombre servlet.ui.form.

Aquí están los contenidos del archivo de soja:

{namespace servlet.ui}
/**
 * This template is needed to draw the form page.
 */
{template .form}
    {@param contextPath: string}
    {webResourceManager_requireResourcesForContext('form')}
    <html>
    <head>
        <meta charset="utf-8"/>
        <meta name="decorator" content="atl.admin">
        <meta name="admin.active.section" content="admin_plugins_menu/react">
        <title>Form Page</title>
    </head>
    <body>
    <div class="field-group hidden">
        <input class="text" type="text" id="contextPath" name="contextPath" value="{$contextPath}">
    </div>
    <div id="maincontainer">
        <div id="container">
        </div>
    </div>
    </body>
    </html>
{/template}
/**
 * This template is needed to draw the dynamic table page.
 */
{template .dynamictable}
    {webResourceManager_requireResourcesForContext('dynamictable')}
    <html>
    <head>
        <meta charset="utf-8"/>
        <meta name="decorator" content="atl.admin">
        <meta name="admin.active.section" content="admin_plugins_menu/react">
        <title>Dynamic Table Page</title>
    </head>
    <body>
    <div id="maincontainer">
        <div id="container">
        </div>
    </div>
    </body>
    </html>
{/template}

El código para las plantillas es bastante simple. Llamo al recurso web para el elemento del menú y creo un contenedor div que será utilizado por React.

Registré el archivo de soya en atlassian-plugin.xml:

<web-resource key="jira-react-atlaskit-resources" name="jira-react-atlaskit Web Resources"> 
    ...
    <resource type="soy" name="soyui" location="/templates/servlets.soy"/>
    ...
    <context>jira-react-atlaskit</context> 
  </web-resource> 

Eso es todo lo que necesita hacer para usar React y Atlaskit en el servidor Atlassian y los complementos del centro de datos.

All Articles