13/04/2024
query example {
tenantContexts(hostNames:["your-domain.atlassian.net"]) {
cloudId
}
}
X-ExperimentalApi: DevOpsBeta
use this field you must set a X-ExperimentalApi: confluence-agg-beta
modules:
jira:uiModifications:
- key: ui-modifications-app
title: Example UI modifications app
resource: uiModificationsApp
resources:
- key: uiModificationsApp
path: static/ui-modifications/dist
//static/hello-world/index.js
import { uiModificationsApi } from '/jira-bridge';
uiModificationsApi.onInit(
({ api }) => {
const { getFieldById } = api;
// Hiding the priority field
const priority = getFieldById('priority');
priority?.setVisible(false);
// Changing the summary field label
const summary = getFieldById('summary');
summary?.setName('Modified summary label');
// Changing the assignee field description
const assignee = getFieldById('assignee');
assignee?.setDescription('Description added by UI modifications');
// Get value of labels field
const labels = getFieldById('labels');
const labelsData = labels?.getValue() || [];
labels?.setDescription(
`${labelsData.length} label(s) are currently selected`
);
},
() => ['priority', 'summary', 'assignee', 'labels']
);
import { uiModificationsApi } from '/jira-bridge';
// Below: imaginary import
import { shouldPriorityBeHidden } from '../my-services';
uiModificationsApi.onInit(
({ api }) => {
const { getFieldById } = api;
// Hiding the priority field
const priority = getFieldById('priority');
// We store the update Promise
const priorityUpdate = shouldPriorityBeHidden().then((result) => {
if (result === true) {
priority?.setVisible(false);
}
});
// Changing the assignee field description
const assignee = getFieldById('assignee');
assignee?.setDescription('Description added by UI modifications');
// We return the promise. In this case even the assignee field description
// will be updated only after the priorityUpdate promise resolves.
return priorityUpdate;
},
() => ['priority', 'assignee']
);
import api, { route } from '/api';
const createUiModification = async (projectId, issueTypeId) => {
const result = await api.asApp().requestJira(route`/rest/api/3/uiModifications`, {
method: "POST",
body: JSON.stringify({
name: 'demo-ui-modification',
data: '["custom data", "for your app"]',
contexts: [
{ projectId, issueTypeId, viewType: 'GIC' },
{ projectId, issueTypeId, viewType: 'IssueView' }
],
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
console.log(`Created UI modification with status ${result.status}`);
return result.status;
}m
uiModifications: Array
resources: # list below the static resources entries for your app
- key: my-resource-1
path: relative/path/to/resource/one/directory
- key: my-resource-2
path: relative/path/to/resource/two/directory
modules:
jira:issuePanel:
- key: hello-world-panel
title: Custom UI App
description: A Forge app with resources
resource: my-resource-1 # link to the resources listed below
app:
id: ""
resources: # list below the resource entries for your app
- key: my-resource-1
path: relative/path/to/resource/one/directory
forge create
cd hello-world-custom-ui
hello-world-custom-ui
|-- src
| `-- index.js
|-- static
| `-- hello-world
| `-- src
| `-- index.js
| `-- App.js
| `-- public
| `-- index.html
| `-- package.json
| `-- package-lock.json
|-- manifest.yml
|-- package.json
|-- package-lock.json
`-- README.md
modules:
jira:issuePanel:
- key: hello-world-panel
resource: main
resolver:
function: resolver
viewportSize: medium
title: Hello World from Emma Richards
icon: https://developer.atlassian.com/platform/forge/images/issue-panel-icon.svg
function:
- key: resolver
handler: index.handler
resources:
- key: main
path: static/hello-world/build
app:
id: ''
tatic/hello-world directory.
Install the needed dependencies:
1
npm install
Build the assets:
1
npm run build
forge deploy
Install your app by running:
1
forge install
Select your Atlassian product using the arrow keys and press the enter key.
npx create-react-app my-app
cd my-app
npm start
npx create-react-app my-app
(npx is a package runner tool that comes with npm 5.2+ and higher, see instructions for older npm versions)
npm
npm init react-app my-app
npm init is available in npm 6+
Yarn
yarn create react-app my-app
yarn create is available in Yarn 0.25+
It will create a directory called my-app inside the current folder.
Inside that directory, it will generate the initial project structure and install the transitive dependencies:
my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
└── setupTests.js
No configuration or complicated folder structures, only the files you need to build your app.
Once the installation is done, you can open your project folder:
cd my-app
Inside
import React, { useEffect, useState } from 'react';
import { invoke } from '/bridge';
function App() {
const [data, setData] = useState(null);
useEffect(() => {
invoke('getText', { example: 'my-invoke-variable' }).then(setData);
}, []);
return (
{data ? data : 'Loading...'}
);
}
export default App;
forge tunnel
forge tunnel
forge tunnel
import Resolver from '/resolver';
const resolver = new Resolver();
resolver.define('getText', (req) => {
console.log(req);
return 'Hello, world!';
});
export const handler = resolver.getDefinitions();
forge tunnel
version: 2
log_level: debug
region: us
authtoken:
$ ngrok config add-authtoken 2cFVRxxBHSpL7bOBStTSXOV0tcD_p3JYuv7MW8rneDQXZxKv
file.
# in ngrok.yml
authtoken: 2cFVRxxBHSpL7bOBStTSXOV0tcD_p3JYuv7MW8rneDQXZxKv
brew install ngrok/ngrok/ngrok
ngrok config add-authtoken 2cFVRxxBHSpL7bOBStTSXOV0tcD_p3JYuv7MW8rneDQXZxKv
ngrok http http://localhost:8080
ngrok http --domain=bursting-turkey-really.ngrok-free.app 80
Endpoint Id:
ep_2ewBsLJsg9WQxn7ktfXwvzCFi2V
bursting-turkey-really.ngrok-free.app
rd_2coZL6TjXyPJWuIBdmXP4BVP4Me
import ngrok
listener = ngrok.forward("localhost:8080", authtoken_from_env=True,
domain="example.ngrok.app")
print(f"Ingress established at: {listener.url()}");
import ngrok
listener = ngrok.forward("localhost:8080", authtoken_from_env=True,
basic_auth=["username1:password1", "username2:password2"])
print(f"Ingress established at: {listener.url()}");
import ngrok
listener = ngrok.forward("localhost:8080", authtoken_from_env=True,
circuit_breaker=0.5)
print(f"Ingress established at: {listener.url()}");
export NGROK_AUTHTOKEN=""
go mod init example.com/ngrok-circuit-breaker
go get golang.ngrok.com/ngrok
go run example.go
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
)
func main() {
l, err := ngrok.Listen(context.Background(),
config.HTTPEndpoint(
config.WithCircuitBreaker(0.1),
),
ngrok.WithAuthtokenFromEnv(),
)
if err != nil {
log.Fatal(err)
}
fmt.Println("Running at", l.URL())
go makeRequests(l.URL())
http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/500" {
w.WriteHeader(500)
fmt.Fprintln(w, "Hello error!")
} else {
w.WriteHeader(200)
fmt.Fprintln(w, "Hello world!")
}
}))
}
func makeRequests(appURL string) {
// make sure we always dial the same IP addresss for testing purposes because
// circuit breaker state is applied on each ngrok edge server individually
u, _ := url.Parse(appURL)
addrs, err := net.LookupHost(u.Host)
if err != nil {
log.Fatal(err)
}
httpClient := http.Client{Transport: &http.Transport{
Dial: func(network, _ string) (net.Conn, error) {
return net.Dial(network, addrs[0]+":443")
},
}}
// make requests that return a 500 until the circuit opens
for {
resp, err := httpClient.Get(appURL + "/500")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status Code %d\n", resp.StatusCode)
if resp.StatusCode == 503 {
fmt.Println("Circuit opened")
break
}
}
// make requests that will eventually return a 200 which will close the circuit
for {
resp, err := httpClient.Get(appURL)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status Code %d\n", resp.StatusCode)
if resp.StatusCode != 503 {
fmt.Println("Circuit closed")
os.Exit(0)
}
}
}
import ngrok
listener = ngrok.forward("localhost:8080", authtoken_from_env=True,
compression=True)
print(f"Ingress established at: {listener.url()}");
mkdir test-dir
cd test-dir
echo "hello world" > t.txt
ngrok http file://`pwd` --domain your-domain.ngrok.app
curl --compressed -D - https://your-domain.ngrok.app/
HTTP/2 200
content-type: text/html; charset=utf-8
date: Tue, 18 Jul 2023 09:52:49 GMT
last-modified: Tue, 18 Jul 2023 09:52:34 GMT
ngrok-agent-ips: 71.227.75.230
ngrok-trace-id: 24e925dd0f348c1040d7ff62b06606cd
content-length: 39
t.txt
ngrok http file://`pwd` --domain your-domain.ngrok.app --compression
curl --compressed -D - https://your-domain.ngrok.app/
HTTP/2 200
content-encoding: deflate
content-type: text/html; charset=utf-8
date: Tue, 18 Jul 2023 10:03:22 GMT
last-modified: Tue, 18 Jul 2023 09:52:34 GMT
ngrok-agent-ips: 71.227.75.230
ngrok-trace-id: b6b6cdce029e94123188ce53c0febee4
vary: Accept-Encoding
t.txt
# Configure the ngrok provider
provider "ngrok" {
api_key = "{24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}"
}
# Provision an ngrok domain
resource "ngrok_reserved_domain" "my_domain" {
name = "my-domain.example.com"
region = "us"
certificate_management_policy {
authority = "letsencrypt"
private_key_type = "ecdsa"
}
}
curl \
-X POST \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{"metadata":"{\"incident_id\":1233122}","urls"😞"http://legit-facebook-login.ngrok.io/login"]}' \
https://api.ngrok.com/abuse_reports
curl \
-X GET \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Ngrok-Version: 2" \
https://api.ngrok.com/abuse_reports/abrp_2cSjyxWkBf0QvB1vG6tgZEi2W8U
curl \
-X POST \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V + 2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{"description":"acme devices","domain":"connect.acme.com"}' \
https://api.ngrok.com/agent_ingresses
curl \
-X GET \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Ngrok-Version: 2" \
https://api.ngrok.com/agent_ingresses/agin_2cSjz4KiqBvr9dEDroJe1IFLyDb
curl \
-X GET \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Ngrok-Version: 2" \
https://api.ngrok.com/agent_ingresses
curl \
-X GET \
-H "Authorization: Bearer {24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V +
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179}" \
-H "Ngrok-Version: 2" \
https://api.ngrok.com/tunnels/tn_2cSjxbTveHCT9NR4ascMRVlFS5j
ngrok config check
api_key: 24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
+
2f4BN4c0RgHDP7ZR6WF2UDZGpIf_3LYhHdKE1rtsm1CB7Mg3V
+
2f4BtbvIwNt5sikocIhoak3XeGs_63LwML6ETjYJHSqsKe179
forge tunnel
Tunnel redirects requests you make to your local machine. This occurs for any
Atlassian site where your app is installed in the specific development
environment. You will not see requests from other users.
Press Ctrl+C to cancel.
Checking Docker image... 100%
Your Docker image is up to date.
Reloading code...
=== Running forge lint...
No issues found.
=== Bundling code...
App code bundled.
=== Snapshotting functions...
No log output.
App code reloaded.
Listening for requests...
INFO 17:34:04.955 Count of objects in test array: 0
our manifest.yml file, under the resource your server is hosting:
1
2
tunnel:
port:
For example, a resources definition might look like this:
1
2
3
4
5
resources:
- key: main
path: static/hello-world/build
tunnel:
port: 3000
ngrok config check
authtoken: 4nq9771bPxe8ctg7LKr_2ClH7Y15Zqe4bWLWF9p
api_key: 24yRd5U3DestCQapJrrVHLOqiAC_7RviwRqpd3wc9dKLujQZN
connect_timeout: 30s
console_ui: true
console_ui_color: transparent
dns_resolver_ips:
- 1.1.1.1
- 8.8.8.8
heartbeat_interval: 1m
heartbeat_tolerance: 5s
inspect_db_size: 104857600 # 100mb
inspect_db_size: 50000000
log_level: info
log_format: json
log: /var/log/ngrok.log
metadata: '{"serial": "00012xa-33rUtz9", "comment": "For customer [email protected]"}'
proxy_url: socks5://localhost:9150
remote_management: false
root_cas: trusted
update_channel: stable
update_check: false
version: 2
web_addr: localhost:4040
tunnels:
website:
addr: 8888
basic_auth:
- "bob:bobpassword"
schemes:
- https
host_header: "myapp.ngrok.dev"
inspect: false
proto: http
domain: myapp.ngrok.dev
e2etls:
addr: 9000
proto: tls
domain: myapp.example.com
crt: example.crt
key: example.key
policyenforced:
policy:
inbound:
- name: LimitIPs
expressions:
- "conn.ClientIP != '1.1.1.1'"
actions:
- type: deny
addr: 8000
proto: tcp
ssh-access:
addr: 22
proto: tcp
remote_addr: 1.tcp.ngrok.io:12345
my-load-balanced-website:
labels:
- env=prod
- team=infra
addr: 8000
tunnels:
httpbin:
proto: http
addr: 8000
domain: alan-httpbin.ngrok.dev
demo:
proto: http
addr: 9090
domain: demo.inconshreveable.com
inspect: false
ngrok start httpbin
authtoken: 4nq9771bPxe8ctg7LKr_2ClH7Y15Zqe4bWLWF9p
import ngrok
def load_file(name):
with open(name, "r") as crt:
return bytearray(crt.read().encode())
listener = ngrok.forward("localhost:8080", authtoken_from_env=True,
proto="tls",
domain="app.example.com",
crt=load_file("/path/to/app-example-com-crt.pem"),
key=load_file("/path/to/app-example-com-key.pem"))
print(f"Ingress established at: {listener.url()}");
Grateful's Actual Bot user:
ak_2f4BN4c0RgHDP7ZR6WF2UDZGpIf
ak_2f4BN4c0RgHDP7ZR6WF2UDZGpIf
bot_2bOqWXrxi75KWEKeNbXsIn2wpD7
https://dashboard.ngrok.com/api/new
ngrok is the fastest way to put anything on the internet with a single command.