React et Atlaskit dans le serveur Atlassian et les plugins du centre de données

Bonjour à tous!

Dans cet article, je voudrais parler de la façon d'utiliser React et Atlaskit dans les plugins Atlassian dans les environnements de serveur et de centre de données.

introduction


Actuellement, si vous développez des plugins pour les produits Atlassian pour serveur et centre de données, vm , soy , requirejs , jquery , backbone sont disponibles pour que vous puissiez développer l'interface utilisateur à partir de la boîte . Ici , vous pouvez lire mon article sur l'utilisation des bibliothèques prêtes à l'emploi.

Cette pile technologique est obsolète et souhaite utiliser quelque chose de plus récent. J'ai choisi dactylographié et réagis comme la pile . De plus, afin de faciliter le transfert des plug-ins de serveur vers le cloud, j'ai choisi la bibliothèque d'éléments d'interface utilisateur atlaskit .

Dans cet article, je parlerai d'Atlassian Jira, mais la même approche peut être utilisée pour d'autres serveurs Atlassian et produits de centre de données.

Pour reproduire les exemples de cet article, vous devez installer git et Atlassian SDK .

Commençons donc!

Installez l'archétype Maven et créez un nouveau projet.


J'ai créé l'archétype Maven pour faciliter la création d'un nouveau projet qui contiendra déjà tous les paramètres nécessaires pour créer un plugin avec React et Atlaskit.

Si vous ne souhaitez pas utiliser l'archétype, vous pouvez prendre le plug-in déjà créé à partir de cet archétype et aller à la partie Nous assemblons et lançons le projet.

Clonez l'archétype de mon référentiel Bitbucket:

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

Accédez au dossier du dossier jira-react-atlaskit-archetype et installez cet archétype dans votre référentiel Maven local:

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

Après cela, allez dans le dossier un niveau plus haut et créez un nouveau projet basé sur cet archétype:

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

Une question sera posée pour sélectionner l'archétype nécessaire. Voici mes archétypes dans le référentiel 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)

Il est nécessaire de choisir ru.matveev.alexey.atlas.jira: jira-react-atlaskit-archetype-archetype, j'ai donc indiqué le chiffre 3 comme réponse.

Ensuite, vous devez spécifier groupid et artifactid pour le nouveau projet:

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

Nous assemblons et installons le projet


Dans mon cas, le nouveau projet se trouve dans le dossier my-tutorial. Allons dans ce dossier et construisons le projet:

cd my-tutorial
atlas-mvn package

Une fois le projet assemblé, accédez au dossier principal et exécutez Atlassian Jira:

cd backend
atlas-run

Plugin de test


Après le démarrage d'Atlassian Jira, accédez au navigateur à l'adresse suivante:

http://localhost:2990/jira

Vous devez vous connecter en tant qu'administrateur: admin et aller engrenage -> Gérer les applications.



Vous verrez un menu de notre plugin. Mais avant de démarrer nos éléments Atlaskit, accédez à Système -> Journalisation et profilage et définissez le niveau de journalisation INFO pour le package react.atlaskit.tutorial.servlet.



Revenez maintenant à Gérer les applications et cliquez sur le menu Formulaire. Nous verrons le formulaire de saisie de données qui était affiché à l'aide de l'élément Formulaire Atlaskit :



Remplissez tous les champs de texte et cliquez sur le bouton Soumettre:



Maintenant, si vous ouvrez le fichier atlassian-jira.log, vous verrez quelque chose comme ceci:

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"}

Cela signifie qu'en cliquant sur le bouton Soumettre, nos données ont été transférées avec succès vers le servlet qui sert ce formulaire, et ce servlet a affiché les données saisies dans un fichier journal.
Maintenant, sélectionnons le menu Dynamic Table. Vous verrez l'élément Atlaskit Dynamic Table :



c'est tout ce que notre plugin fait. Voyons maintenant comment tout cela fonctionne!

Plugin intérieur


Voici la structure de notre plugin: le



backend contient le plugin Atlassian Jira qui a été créé à l'aide du SDK Atlassian.
frontend contient les éléments d'interface utilisateur qui seront utilisés par notre plugin.
pom.xml fichier pom dans lequel deux modules sont définis:

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

Examinons maintenant le dossier frontal.

L'extrémité avant


Le dossier frontend contient les fichiers suivants: Je



décrirai les fichiers principaux.

package.json est un fichier de configuration pour npm qui contient les informations suivantes:

  • liste des packages dont dépend notre projet.
  • versions des packages que nous utilisons.

Nous utiliserons des packages tels que tapuscrit, atlaskit, babel et autres.

.babel.rc - fichier de configuration pour Babel . Babel est utilisé pour traduire le code ECMAScript 2015+ en code JavaScript. Nous allons écrire notre code Typescript, nous devons donc le traduire en code JavaScript afin que le plugin Jira puisse fonctionner avec.

webpack.config.js - fichier de configuration pour webpack . Webpack traite notre application, construit un graphe de dépendances et génère un bundle qui contient tout le JavaScript nécessaire au fonctionnement de notre application. Pour que le plugin Jira fonctionne avec notre JavaScript, nous avons besoin d'un fichier Javascript pour chaque point d'entrée. Dans notre cas, les points d'entrée sont les éléments de menu Formulaire et Table dynamique.

Voici le contenu du fichier 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")
    }
};

Comme vous pouvez le voir, nous utilisons le plugin atlassian-webresource-webpack-plugin .

Il est nécessaire pour qu'après que webpack ait créé un fichier JavaScript, ce fichier soit automatiquement ajouté en tant que ressource 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')
        }),
    ],

À la suite de cette configuration, après la construction du module frontal, le fichier wr-defs.xml sera créé dans le dossier backend / src / resources / META-INF / plugin-descriptors.

Le paramètre locationPrefix nous permet de spécifier l'emplacement des fichiers JavaScript dans le plugin Jira. Dans notre cas, nous indiquons que les fichiers seront situés dans le dossier backend / src / resources / frontend. Nous mettrons les fichiers JavaScript dans ce dossier plus tard dans le module backend, mais maintenant ce paramètre nous permet d'obtenir la ligne suivante dans le wr-defs.xml <resource type = "download" name = "bundled.dynamictable.js" location = "frontend / bundled.dynamictable.js » />.

Voici le contenu du fichier wr-defs.xml qui a été créé pendant le processus de construction du projet:

<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>

Comme vous pouvez le voir, nous avons des sections supplémentaires de ressources Web dans lesquelles les fichiers JavaScript créés par webpack sont définis. Il ne nous reste plus qu'à en informer Jira, afin que lors de l'installation de notre plugin, nous utilisions également les ressources web du dossier backend / src / resources / META-INF / plugin-descriptor. Pour ce faire, nous avons apporté les modifications suivantes au fichier 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>

Nous avons ajouté <Atlassian-Scan-Folders> META-INF / plugin-descriptors </Atlassian-Scan-Folders>. Ce paramètre indiquera à Jira qu'il est nécessaire de rechercher des ressources Web supplémentaires dans le dossier META-INF / plugin-descriptors.

Nous avons également ajouté <compressResources> false </compressResources> afin de désactiver la minification de nos fichiers JavaScript. Ils ont déjà été minifiés.
Nous avons également défini deux points d'entrée pour notre application dans le fichier webpack.config.js:

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

Cela signifie que webpack analysera les fichiers ./src/form.js et ./src/dynamictable.js et créera deux fichiers JavaScript, chacun étant un fichier pour l'un des points d'entrée. Ces fichiers seront créés dans le dossier frontend / dist.

./src/form.js et ./src/dynamictable.js ne contiennent rien de spécial. J'ai pris la plupart du code d'exemples dans Atlaskit.

Voici le contenu du fichier form.js:

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

Voici une seule ligne qui importe la classe à partir du fichier ./js/components/Form.js.

Voici le contenu du fichier ./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;
});

Ici, je montre le composant MyForm dans un conteneur div. Ce conteneur sera défini dans le modèle de plugin soy Jira.

Faites également attention à cette ligne:

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

document.getElementById ("contextPath"). value obtient la valeur du champ avec l'id contextPath. Je définis ce champ dans le modèle de soja dans le plugin Jira. La valeur de ce champ provient du servlet auquel l'élément de menu Formulaire est lié. Dans mon cas, contextPath contient la valeur / jira, car lors du démarrage de Jira à partir du SDK Atlassian, ce chemin de contexte est défini.

Et c'est une question de frontend. À la suite de l'assemblage du module frontal, nous obtenons deux fichiers JavaScript dans le dossier frontal / dist et xml avec des ressources Web supplémentaires dans le dossier principal / src / resources / META-INF / plugin-descriptors.

Passons maintenant au backend.

Backend


J'ai ajouté ces plugins au fichier 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 crée un fichier maven.properties qui contient toutes les propriétés Maven. J'ai besoin de la propriété atlassian.plugin.key pour appeler des ressources Web à partir de servlets liés aux éléments de menu de notre plugin.

maven-resources-plugin récupère les fichiers JavaScript du dossier frontend / dist et les copie dans le dossier backend / resources / frontend.

J'ai ensuite créé des éléments de menu et appelé des servlets à partir de ces éléments de menu.

Voici les lignes du fichier 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>

Nous avons donc des menus et des servlets qui sont appelés à partir de ces éléments de menu.
Voyons maintenant les 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();
    }}

Je définis deux variables resourceService et soyTemplateRenderer et initialise ces variables dans le constructeur de classe. resourceService - un bean qui lit les propriétés du fichier maven.properties. soyTemplateRenderer - Haricot Jira qui peut appeler des modèles de soja.
Dans la méthode doGet, j'obtiens la valeur de la propriété atlassian.plugin.key et le chemin du contexte. Ensuite, je passe le chemin de contexte en tant que paramètre au modèle de soja et appelle le modèle de soja sous le nom servlet.ui.form.

Voici le contenu du dossier 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}

Le code des modèles est assez simple. J'appelle la ressource Web pour l'élément de menu et crée une div de conteneur qui sera utilisée par React.

J'ai enregistré le fichier soja lui-même dans 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> 

C'est tout ce que vous devez faire pour utiliser React et Atlaskit dans le serveur Atlassian et les plugins du centre de données.

All Articles