Reagieren und Atlaskit in Atlassian Server- und Rechenzentrums-Plugins

Hallo alle zusammen!

In diesem Artikel möchte ich über die Verwendung von React und Atlaskit in Atlassian-Plugins in Server- und Rechenzentrumsumgebungen sprechen.

Einführung


Wenn Sie derzeit Plugins für Atlassian-Produkte für Server und Rechenzentrum entwickeln, stehen Ihnen VM , Soja , Requirejs , JQuery und Backbone zur Verfügung, um die Benutzeroberfläche aus der Box heraus zu entwickeln . Hier hier können Sie meinen Artikel über die Verwendung der sofort verfügbaren Bibliotheken lesen.

Dieser Technologie-Stack ist veraltet und möchte etwas Neueres verwenden. Ich habe Typoskript gewählt und als Stapel reagiert . Um die Übertragung von Server-Plug-Ins in die Cloud zu vereinfachen, habe ich die Bibliothek mit Elementen der Benutzeroberfläche atlaskit ausgewählt .

In diesem Artikel werde ich über Atlassian Jira sprechen, aber der gleiche Ansatz kann für andere Atlassian Server- und Rechenzentrumsprodukte verwendet werden.

Um die Beispiele aus diesem Artikel zu reproduzieren, müssen Sie git und Atlassian SDK installieren .

Also fangen wir an!

Installieren Sie den Maven-Archetyp und erstellen Sie ein neues Projekt.


Ich habe den Maven-Archetyp erstellt, um das Erstellen eines neuen Projekts zu vereinfachen, das bereits alle erforderlichen Einstellungen zum Erstellen eines Plugins mit React und Atlaskit enthält.

Wenn Sie den Archetyp nicht verwenden möchten, können Sie das bereits erstellte Plug-In von diesem Archetyp nehmen und zu dem Teil gehen, den wir zusammenstellen und das Projekt starten.

Klonen Sie den Archetyp aus meinem Bitbucket-Repository:

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

Wechseln Sie in den Ordner jira-react-atlaskit-archetype und installieren Sie diesen Archetyp in Ihrem lokalen Maven-Repository:

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

Gehen Sie danach in den Ordner eine Ebene höher und erstellen Sie ein neues Projekt basierend auf diesem Archetyp:

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

Es wird eine Frage gestellt, um den erforderlichen Archetyp auszuwählen. Hier sind meine Archetypen im lokalen Repository:

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)

Ich muss ru.matveev.alexey.atlas.jira wählen: jira-react-atlaskit-archetype-archetype, also habe ich die Nummer 3 als Antwort angegeben.

Dann müssen Sie die Gruppen-ID und die Artefakt-ID für das neue Projekt angeben:

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

Wir montieren und installieren das Projekt


In meinem Fall befindet sich das neue Projekt im Ordner my-tutorial. Gehen wir zu diesem Ordner und erstellen das Projekt:

cd my-tutorial
atlas-mvn package

Wechseln Sie nach dem Zusammenstellen des Projekts zum Backend-Ordner und führen Sie Atlassian Jira aus:

cd backend
atlas-run

Plugin testen


Gehen Sie nach dem Start des Atlassian Jira unter der folgenden Adresse zum Browser:

http://localhost:2990/jira

Sie müssen sich als admin anmelden: admin und loslegen -> Apps verwalten.



Sie sehen ein Menü aus unserem Plugin. Bevor Sie jedoch unsere Atlaskit-Elemente starten, gehen Sie zu System -> Protokollierung und Profilerstellung und legen Sie die INFO-Protokollierungsstufe für das Paket react.atlaskit.tutorial.servlet fest.



Gehen Sie nun zurück zu Apps verwalten und klicken Sie auf das Menü Formular. Wir sehen das Dateneingabeformular, das mit dem Atlaskit- Formularelement angezeigt wurde :



Füllen Sie alle Textfelder aus und klicken Sie auf die Schaltfläche



Senden : Wenn Sie nun die Datei atlassian-jira.log öffnen, wird Folgendes angezeigt:

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

Dies bedeutet, dass durch Klicken auf die Schaltfläche Senden unsere Daten erfolgreich an das Servlet übertragen wurden, das dieses Formular bereitstellt, und dieses Servlet die eingegebenen Daten in einer Protokolldatei anzeigt.
Wählen wir nun das Menü Dynamische Tabelle. Sie sehen das Atlaskit Dynamic Table- Element :



Das ist alles, was unser Plugin tut. Nun wollen wir sehen, wie alles funktioniert!

Innerhalb des Plugins


Hier ist die Struktur unseres Plugins: Das



Backend enthält das Atlassian Jira Plugin, das mit dem Atlassian SDK erstellt wurde.
Das Frontend enthält die UI-Elemente, die von unserem Plugin verwendet werden.
pom.xml pom-Datei, in der zwei Module definiert sind:

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

Schauen wir uns nun den vorderen Ordner an.

Vorderes Ende


Der Frontend-Ordner enthält die folgenden Dateien: Ich werde



die Hauptdateien beschreiben.

package.json ist eine Konfigurationsdatei für npm , die die folgenden Informationen enthält:

  • Liste der Pakete, von denen unser Projekt abhängt.
  • Versionen der von uns verwendeten Pakete.

Wir werden Pakete wie Typoskript, Atlaskit, Babel und andere verwenden.

.babel.rc - Konfigurationsdatei für Babel . Babel wird verwendet, um ECMAScript 2015+ Code in JavaScript-Code zu übersetzen. Wir werden unseren Typescript-Code schreiben, daher müssen wir ihn in JavaScript-Code übersetzen, damit das Jira-Plugin damit arbeiten kann.

webpack.config.js - Konfigurationsdatei für das Webpack . Webpack verarbeitet unsere Anwendung, erstellt ein Abhängigkeitsdiagramm und generiert ein Bundle, das das gesamte erforderliche JavaScript enthält, damit unsere Anwendung funktioniert. Damit das Jira-Plugin mit unserem JavaScript funktioniert, benötigen wir für jeden Einstiegspunkt eine Javascript-Datei. In unserem Fall sind die Einstiegspunkte die Menüpunkte Formular und Dynamische Tabelle.

Hier ist der Inhalt der Datei 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")
    }
};

Wie Sie sehen können, verwenden wir das atlassian-webresource-webpack-Plugin .

Es wird benötigt, damit Webpack nach dem Erstellen einer JavaScript-Datei automatisch als Webressource hinzugefügt wird:

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')
        }),
    ],

Als Ergebnis dieser Konfiguration wird nach dem Zusammenstellen des Frontend-Moduls eine wr-defs.xml-Datei im Ordner backend / src / resources / META-INF / plugin-descriptors erstellt.

Mit dem Parameter locationPrefix können wir den Speicherort der JavaScript-Dateien im Jira-Plugin angeben. In unserem Fall geben wir an, dass sich die Dateien im Ordner backend / src / resources / frontend befinden. Wir werden die JavaScript-Dateien später im Backend-Modul in diesem Ordner ablegen, aber jetzt können wir mit diesem Parameter eine solche Zeile in der Datei wr-defs.xml <resource type = "download" name = "bundled.dynamictable.js" location = "frontend / " abrufen bundled.dynamictable.js » />.

Hier ist der Inhalt der Datei wr-defs.xml, die während des Erstellungsprozesses des Projekts erstellt wurde:

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

Wie Sie sehen können, haben wir zusätzliche Abschnitte von Webressourcen, in denen von webpack erstellte JavaScript-Dateien definiert sind. Wir müssen Jira nur mitteilen, dass wir bei der Installation unseres Plugins auch Webressourcen aus dem Ordner backend / src / resources / META-INF / plugin-deskriptor verwenden. Zu diesem Zweck haben wir die folgenden Änderungen an der Datei backend / pom.xml vorgenommen:

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

Wir haben <Atlassian-Scan-Folders> META-INF / Plugin-Deskriptoren </ Atlassian-Scan-Folders> hinzugefügt. Dieser Parameter teilt Jira mit, dass im Ordner META-INF / Plugin-Descriptors nach zusätzlichen Webressourcen gesucht werden muss.

Wir haben auch <compressResources> false </ compressResources> hinzugefügt, um die Minimierung unserer JavaScript-Dateien zu deaktivieren. Sie wurden bereits minimiert.
Wir haben außerdem zwei Einstiegspunkte für unsere Anwendung in der Datei webpack.config.js definiert:

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

Dies bedeutet, dass das Webpack die Dateien ./src/form.js und ./src/dynamictable.js scannt und zwei JavaScript-Dateien erstellt, von denen jede eine Datei für einen der Einstiegspunkte ist. Diese Dateien werden im Frontend / Dist-Ordner erstellt.

./src/form.js und ./src/dynamictable.js enthalten nichts Besonderes. Ich habe den größten Teil des Codes aus Beispielen in Atlaskit entnommen.

Hier ist der Inhalt der Datei form.js:

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

Hier ist nur eine Zeile, die die Klasse aus der Datei ./js/components/Form.js importiert.

Hier ist der Inhalt der Datei ./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;
});

Hier zeige ich die MyForm-Komponente in einem div-Container. Dieser Container wird in der Soja-Jira-Plugin-Vorlage definiert.

Beachten Sie auch diese Zeile:

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

document.getElementById ("contextPath"). value ruft den Wert des Feldes mit der ID contextPath ab. Ich definiere dieses Feld in der Soja-Vorlage im Jira-Plugin. Der Wert in diesem Feld stammt von dem Servlet, an das der Menüpunkt Formular gebunden ist. In meinem Fall enthält contextPath den Wert / jira, da beim Starten von Jira über das Atlassian SDK dieser Kontextpfad festgelegt wird.

Und alles dreht sich um Frontend. Durch das Zusammenstellen des Frontend-Moduls erhalten wir zwei JavaScript-Dateien im Ordner frontend / dist und xml mit zusätzlichen Webressourcen im Ordner backend / src / resources / META-INF / plugin-descriptors.

Kommen wir nun zum Backend.

Backend


Ich habe diese Plugins zur Datei backend / pom.xml hinzugefügt:

<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 erstellt eine maven.properties-Datei, die alle Maven-Eigenschaften enthält. Ich benötige die Eigenschaft atlassian.plugin.key, um Webressourcen von Servlets aufzurufen, die an die Menüelemente unseres Plugins gebunden sind.

Das maven-resources-plugin nimmt JavaScript-Dateien aus dem Ordner frontend / dist auf und kopiert sie in den Ordner backend / resources / frontend.

Dann habe ich Menüelemente erstellt und Servlets aus diesen Menüelementen aufgerufen.

Hier sind die Zeilen aus der Datei 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>

Wir haben also Menüs und Servlets, die über diese Menüpunkte aufgerufen werden.
Schauen wir uns nun die Servlets an:

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();
    }}

Ich definiere zwei Variablen resourceService und sojaTemplateRenderer und initialisiere diese Variablen im Klassenkonstruktor. resourceService - eine Bean, die Eigenschaften aus der Datei maven.properties liest. sojaTemplateRenderer - Jira-Bean, die Soja-Vorlagen aufrufen kann.
In der doGet-Methode erhalte ich den Wert der Eigenschaft atlassian.plugin.key und den Kontextpfad. Dann übergebe ich den Kontextpfad als Parameter an die Soja-Vorlage und rufe die Soja-Vorlage unter dem Namen servlet.ui.form auf.

Hier ist der Inhalt der Soja-Datei:

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

Der Code für die Vorlagen ist recht einfach. Ich rufe die Webressource für den Menüpunkt auf und erstelle ein Container-Div, das von React verwendet wird.

Ich habe die Sojadatei selbst in atlassian-plugin.xml registriert:

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

Das ist alles, was Sie tun müssen, um React und Atlaskit in Atlassian Server- und Rechenzentrums-Plugins zu verwenden.

All Articles