Last updatedSep 1, 2019

Lesson 1 - The source awakens

DescriptionA guide to setting up a general page using Atlassian Connect Express (ACE).
LevelBeginner
Estimated time15 minutes
Examplehttps://bitbucket.org/atlassianlabs/confluence_cloud_tutorials/src/source-awakens/

Prerequisites

Ensure you have installed all the tools you need for Confluence Connect app development, and running Confluence by going through the getting started guide.

Making a new project

Before we begin this process, let's double-check that we have all our prerequisites correctly installed. By issuing the following commands, you should see similar results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ node -v
v8.12.0
$ npm -v
6.4.1
$ atlas-connect -h 
 
  Usage: atlas-connect [options] [command]


  Commands:

    new [name]  create a new Atlassian add-on
    help [cmd]  display help for [cmd]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

If any of the above commands are not found, please refer to the Getting started tutorial.

To create your new project:

1
$ atlas-connect new awesome-app

You will be prompted to pick which Atlassian product you are building an app for. Select 'confluence' using your arrow keys, and press enter.

1
2
3
4
5
6
? Select a product
jira
jira-service-desk
❯ confluence
hipchat
bitbucket

Now, let's navigate to our newly created app directory, and see what we have.

1
2
3
4
5
$ cd awesome-app/
$ ls
LICENSE.txt             app.js                  credentials.json.sample routes
Procfile                atlassian-connect.json  package.json            views
README.md               config.json             public

First, lets install the Node.js dependencies:

1
$ npm install

You also need to install the npm packages shown below. SQLite is a small, lightweight database, and ngrok is a tool to tunnel your local development environment to the internet.

1
2
$ npm install sqlite3 --save
$ npm install ngrok@2 --save-dev 

You can change SQLite to PostgreSQL, MySQL, or MSSQL, which are supported by Sequelize because it's used under the hood of Atlassian Connect Express. Make sure to update config.json file as sqlite3 configuration is used by default.

Enabling developer mode on your Confluence Cloud instance

You need to enable development mode on your Confluence Cloud development instance. Development mode allows the installation of app descriptors that are not from the Atlassian Marketplace. You can find how to do that here.

Starting and installing the app

Because your new app is a scaffold generated by ACE, there are some very convenient features that make it easy to install, run, and test your app, from one simple command. First, you'll need a credentials.json file in your app directory with your credentials.

  • site-url: Replace with the site URL of your cloud development site, for example, your-site.atlassian.net.
  • username: Use the email address of your Atlassian account as the value of this field.
  • password: Specify an API token as the value of this field.
1
2
3
4
5
6
7
8
9
{
    "hosts" : {
        "<site-url>": {
            "product" : "confluence",
            "username" : "<email@address.com>",
            "password" : "<api-token>"
        }
    }
}

We now have everything needed to get into our development workflow. Use the command below to start the app on your local machine and initiate the installation to your Confluence Cloud development instance.

1
$ npm start

We will see the following output: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ npm start
//...More stuff above here.
Saved tenant details for <your-client-key> to database
{ key: 'my-app',
  clientKey: '<your-client-key>',
  publicKey: '<your-public-key>',
  sharedSecret: '<your-shared-secret>',
  serverVersion: '6452',
  pluginsVersion: '1.268.0',
  baseUrl: '<site-url>/wiki',
  productType: 'confluence',
  description: 'Atlassian Confluence at null ',
  eventType: 'installed' }
POST /installed 204 20ms
//...More stuff below here

Hello World

Nearly done! Navigate to your Confluence instance, and click on 'Hello World' in the top left. You should be greeted with this:

hello world

The app descriptor

Yes! You're all ready to go - this is an example of a general page built using Connect. How does this generalPage actually come about in our app? The secrets are all hidden in our app descriptor...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
    "key": "my-app",
    "name": "My app",
    "description": "My very first app",
    "vendor": {
        "name": "Angry Nerds",
        "url": "https://www.atlassian.com/angrynerds"
    },
    "baseUrl": "{{localBaseUrl}}",
    "links": {
        "self": "{{localBaseUrl}}/atlassian-connect.json",
        "homepage": "{{localBaseUrl}}/atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        // atlassian-connect-express expects this route to be configured to manage the installation handshake
        "installed": "/installed"
    },
    "scopes": [
        "READ"
    ],
    "modules": {
        "generalPages": [
            // Jira - Add a Hello World menu item to the navigation bar
            {
                "key": "hello-world-page-jira",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "Hello World"
                },
                "url": "/hello-world",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            },
            // Confluence - Add a Hello World menu item to the navigation bar
            {
                "key": "hello-world-page-confluence",
                "location": "system.header/left",
                "name": {
                    "value": "Hello World"
                },
                "url": "/hello-world",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            }
        ]
    },
    "apiMigrations": {
        "gdpr": true
    }
}

Of interest to us are the modules configured under the "modules" key of our app descriptor. 

1
2
3
4
5
6
7
8
9
10
11
12
 // Confluence - Add a Hello World menu item to the navigation bar
 {
    "key": "hello-world-page-confluence",
    "location": "system.header/left",
    "name": {
        "value": "Hello World"
    },
    "url": "/hello-world",
    "conditions": [{
        "condition": "user_is_logged_in"
    ]
}

Before we take a deeper look at this, let's see our dev-loop in action. Try commenting out and/or deleting the Jira module from your descriptor (since we are operating in Confluence, it does not make sense to keep it with us). Once you have deleted it, and saved your descriptor, you will notice the following in your terminal: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...More stuff above here.
Registered with host at <site-url>/wiki
Re-registering due to atlassian-connect.json change
Saved tenant details for <your-client-key> to database
{ key: 'my-app',
  clientKey: '<your-client-key>',
  publicKey: '<your-public-key>',
  sharedSecret: '<your-shared-secret>',
  serverVersion: '6452',
  pluginsVersion: '1.268.0',
  baseUrl: '<site-url>/wiki',
  productType: 'confluence',
  description: 'Atlassian Confluence at null ',
  eventType: 'installed' }
POST /installed 204 20ms
Registered with host at <site-url>/wiki
// ...More stuff below here

Let's break down exactly what's going on in the generalPages module in the descriptor. 

  • key: This value determines the URL which is loaded when a person attempts to access this general page. In this instance, it would be: <site-url>/wiki/plugins/servlet/ac/my-app/hello-world-page-confluence.
  • location: Tells our host product where to situate a link to this general page. In this case, we are targeting the system navigation area, in the top left. For a list of valid locations, take a look here!
  • name: Specifies the text we see in a link to this page.
  • url: Tells our app which route to send a HTTP request to when a user accesses our general page. The magic happens here:

    routes/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // This is an example route that's used by the default "generalPage" module.
    // Verify that the incoming request is authenticated with Atlassian Connect
    app.get('/hello-world', addon.authenticate(), function (req, res) {
            // Rendering a template is easy; the `render()` method takes two params: name of template
            // and a json object to pass the context in
            res.render('hello-world', {
                title: 'Atlassian Connect'
            });
        }
    );

    Which calls on the following template:

    views/hello-world.hbs 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    {{!< layout}}
    <header class="aui-page-header">
      <div class="aui-page-header-inner">
        <div class="aui-page-header-main intro-header">
          <h1>Hello World!</h1>
    
          <p class="subtitle">Welcome to {{title}}</p>
        </div>
      </div>
    </header>
    
    <div class="aui-page-panel main-panel">
      <div class="aui-page-panel-inner">
        <section class="aui-page-panel-item">
          <div class="aui-group">
            <div class="aui-item">
              <p>
                Congratulations. You've successfully created an Atlassian Connect app using the
                <a href="https://bitbucket.org/atlassian/atlassian-connect-express/src/master/README.md#markdown-header-atlassian-connect-express-nodejs-package-for-express-based-atlassian-add-ons" target="_parent">atlassian-connect-express</a>
                client library.
              </p>
              <p>
                <a class="aui-button aui-button-primary" href="https://bitbucket.org/atlassian/atlassian-connect-express/src/master/README.md#markdown-header-atlassian-connect-express-nodejs-package-for-express-based-atlassian-add-ons" target="_parent">
                  Get Started
                  <span class="aui-icon aui-icon-small aui-iconfont-devtools-arrow-right">Arrow right</span>
                </a>
              </p>
            </div>
          </div>
        </section>
      </div>
    </div> 

    Pretty cool, right?

  • conditions: Specifies the conditions which must be reached before we a user is allowed to access this page. To understand the conditions system better, have a read here.

The AUI Sandbox

Next up, try and edit your app, so that it looks a little different - maybe like this!

connect is awesome

Does this look long and tiring? Truth be told, it took less than 5 minutes using the AUI Sandbox. Lastly, here's what our source looks like!

views/hello-world.hbs 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
{{!< layout}}
<header class="aui-page-header">

  <div class="aui-page-header-inner">
    <div class="aui-page-header-main intro-header">
      <h1>Connect is awesome!</h1>

      <p class="subtitle">Welcome to {{title}}</p>
    </div>
  </div>
</header>

<nav class="aui-navgroup aui-navgroup-horizontal">
    <div class="aui-navgroup-inner">
        <div class="aui-navgroup-primary">
            <ul class="aui-nav">
                <li><a href="#">Nav item</a></li>
                <li class="aui-nav-selected"><a href="#">Nav item</a></li>
                <li><a href="#">Nav item</a></li>
                <li><a href="#">Nav item <span class="aui-badge">12</span></a></li>
                <li><a href="#">Nav item</a></li>
            </ul>
        </div><!-- .aui-navgroup-primary -->
        <div class="aui-navgroup-secondary">
            <ul class="aui-nav">
                <li><a href="#hnavsettingsDropdown" class="aui-dropdown2-trigger" aria-owns="hnavsettings-dropdown" aria-haspopup="true"><span class="aui-icon aui-icon-small aui-iconfont-configure">Configure</span> <span class="aui-icon-dropdown"></span></a></li>
            </ul>
        </div><!-- .aui-navgroup-secondary -->
    </div><!-- .aui-navgroup-inner -->
</nav>

<div class="aui-page-panel">
    <div class="aui-page-panel-inner">
        <section class="aui-page-panel-content">
            <h2>This is a heading.</h2>
            <div class="aui-item">
                <p>
                    Here is a cool table.
                </p>
                <div class="aui-tabs horizontal-tabs" id="tabs-example1">
                    <ul class="tabs-menu">
                        <li class="menu-item active-tab">
                            <a href="#tabs-example-first"><strong>Designers</strong></a>
                        </li>
                        <li class="menu-item">
                            <a href="#tabs-example-second"><strong>Developers</strong></a>
                        </li>
                        <li class="menu-item">
                            <a href="#tabs-example-third"><strong>PMs</strong></a>
                        </li>
                    </ul>
                    <div class="tabs-pane active-pane" id="tabs-example-first">
                        <h3>Designers</h3>
                        <table class="aui">
                            <thead>
                            <tr>
                                <th id="basic-number">#</th>
                                <th id="basic-fname">First name</th>
                                <th id="basic-lname">Last name</th>
                                <th id="basic-username">Username</th>
                            </tr>
                            </thead>
                            <tbody>
                            <tr>
                                <td headers="basic-number">1</td>
                                <td headers="basic-fname">Matt</td>
                                <td headers="basic-lname">Bond</td>
                                <td headers="basic-username">mbond</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">2</td>
                                <td headers="basic-fname">Ross</td>
                                <td headers="basic-lname">Chaldecott</td>
                                <td headers="basic-username">rchaldecott</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">3</td>
                                <td headers="basic-fname">Henry</td>
                                <td headers="basic-lname">Tapia</td>
                                <td headers="basic-username">htapia</td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                    <div class="tabs-pane" id="tabs-example-second">
                        <h3>Developers</h3>
                        <table class="aui">
                            <thead>
                            <tr>
                                <th id="basic-number">#</th>
                                <th id="basic-fname">First name</th>
                                <th id="basic-lname">Last name</th>
                                <th id="basic-username">Username</th>
                            </tr>
                            </thead>
                            <tbody>
                            <tr>
                                <td headers="basic-number">4</td>
                                <td headers="basic-fname">Seb</td>
                                <td headers="basic-lname">Ruiz</td>
                                <td headers="basic-username">sruiz</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">7</td>
                                <td headers="basic-fname">Sean</td>
                                <td headers="basic-lname">Curtis</td>
                                <td headers="basic-username">scurtis</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">8</td>
                                <td headers="basic-fname">Matthew</td>
                                <td headers="basic-lname">Watson</td>
                                <td headers="basic-username">mwatson</td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                    <div class="tabs-pane" id="tabs-example-third">
                        <h3>Product management</h3>
                        <table class="aui">
                            <thead>
                            <tr>
                                <th id="basic-number">#</th>
                                <th id="basic-fname">First name</th>
                                <th id="basic-lname">Last name</th>
                                <th id="basic-username">Username</th>
                            </tr>
                            </thead>
                            <tbody>
                            <tr>
                                <td headers="basic-number">5</td>
                                <td headers="basic-fname">Jens</td>
                                <td headers="basic-lname">Schumacher</td>
                                <td headers="basic-username">jschumacher</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">6</td>
                                <td headers="basic-fname">Sten</td>
                                <td headers="basic-lname">Pittet</td>
                                <td headers="basic-username">spittet</td>
                            </tr>
                            <tr>
                                <td headers="basic-number">9</td>
                                <td headers="basic-fname">James</td>
                                <td headers="basic-lname">Dumay</td>
                                <td headers="basic-username">jdumay</td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                </div><!-- .aui-tabs -->

            </div>
        </section>
    </div>
</div>


<div class="aui-dropdown2 aui-style-default" id="hnavsettings-dropdown" data-dropdown2-alignment="right">
    <ul>
        <li><a href="#" class="">Nav dropdown item</a></li>
        <li><a href="#" class="active">Nav dropdown item</a></li>
        <li><a href="#">Nav dropdown item</a></li>
    </ul>
</div>

Conclusion

As you can see, general pages provide a powerful mechanism to display customized content on a page in Confluence. For more information on the many other different kinds of modules and entities you can specify in your app descriptor, check out the Confluence Cloud Connect module documentation

Now, we're ready to move onto something even more exciting. Check out how to add macros to the Confluence editor in Lesson 2 - Rise of the Macros