In part one you created your Forge UI kit app using the Jira dashboard gadget template. You then customised the Configuration screen and displayed the configuration data in the app.
Now, you will modify the app to call the OpenWeather Geolocation API using the fetch client from the @forge/api
package, and display a list of locations for the user to select from on the app configuration view. You will also learn how to store and access environment variables in Forge.
This is part two in this tutorial. Complete Part one before working through this page.
Using the Forge CLI, you will store the API Key for the OpenWeather API as an environment variable.
If you haven't already, sign up for a free Open weather map account and generate an API key. You won't need to sign up for a paid account, as the API calls this app makes are included in the Free plan.
Environment variables are key-value pairs which can be stored based on your environment. As with other Forge CLI commands, the Forge CLI variables commands act on the development environment unless another environment is specified explicitly. Storing your API key as an environment variable means you don't need to store the key in plain text within your app.
1 2forge variables set --encrypt OPENWEATHER_KEY <your-key>
forge variables list
at any time from your app's top-level directory to view your variables.In this section, you will modify your manifest.yml
to add the necessary permissions to call the OpenWeather API.
Open the manifest.yml
file,
Add the following permissions to the end:
1 2permissions: external: fetch: backend: - api.openweathermap.org
For more information on external permissions, see Permissions - External permissions in the Forge docs.
Save the changes you made to manifest.yml
From your app's top-level directory run forge deploy
, the following warning message will appear
1 2We've detected new scopes or egress URLs in your app. Run forge install --upgrade and restart your tunnel to put them into effect.
From your app's top-level directory run forge install --upgrade
and follow the prompts to upgrade your app installation.
The OpenWeather Current weather data API needs the longitude and latitude to be supplied, rather than the country and city. OpenWeather provides a Geocoding API which allows you to specify a city, state and country and get a list of cities that match along with their Longitudes and Latitudes.
In this section, you will create a resolver function that will use fetch
to call the Geocoding API with the city
and country
entered by the user and pass that data back to be displayed in your app.
API Calls to external services (such as the OpenWeather API) must be made via a resolver.
src/resolvers
directory for your app.index.js
file, the contents should be as follows:
1 2import Resolver from '@forge/resolver'; const resolver = new Resolver(); resolver.define('getText', (req) => { console.log(req); return 'Hello, world!'; }); export const handler = resolver.getDefinitions();
fetch
from @forge/api
by adding the following to the top of the file, and save your changes
1 2import { fetch } from '@forge/api'
1 2npm install @forge/api
src/resolvers/index.js
create a new resolver to call the OpenWeather Geocoding API using fetch
:
1 2resolver.define('getLocationCoordinates', async (req) => { if(req.payload.location) { const config = req.payload.location; const url = "https://api.openweathermap.org/geo/1.0/direct?q=" + config.city + "," + config.country + "&limit=5&appid=" + process.env.OPENWEATHER_KEY; const response = await fetch(url) if(!response.ok) { const errmsg = `Error from Open Weather Map Geolocation API: ${response.status} ${await response.text()}`; console.error(errmsg) throw new Error(errmsg) } const locations = await response.json() return locations; } else { return null; } });
In this section, you'll modify the configuration view for your app so that:
You will be using the form
component to make most of these changes. See UI Kit Form Component and UI Kit useForm Hook
src/frontend
directory of your app and open index.jsx
RadioGroup
and ErrorMessage
to the imports from @forge/react
Edit
function to add getValues
, and formState
which will be used to get the field data entered by the user and to catch errors
1 2const { handleSubmit, register, getValues, formState } = useForm();
getOptions
and the following variables to the Edit
function:
1 2const [locationOptions, setLocationOptions] = useState(null); const [showOptions, setShowOptions] = useState(false); const { errors } = formState; const getOptions = () => { const values = getValues(); if(values.city && values.country){ if(currentCC && (currentCC.city == values.city)&&(currentCC.country == values.country)) { // do nothing if the city and country entered by the user hasn't changed } else { // store the curent city and country to compare for changes later currentCC = { city: values.city, country: values.country } // refresh locationOptions by calling the OpenWeather Geolocation API invoke('getLocationCoordinates', {location: values}).then((val) => { setLocationOptions(val); // set showOptions to true - this will be used to display the radio button group, and the submit button setShowOptions(true); }); } } };
locationOption
to the Edit
function. This will be used to create the radio button options.
1 2function locationOption(obj, index, array) { return { name: "location", label: obj.name + ", " + obj.state + ", " + obj.country, value: index } }
configureGadget
function, to return the selected location, rather than the user entered city and state.
1 2const configureGadget = (data) => { view.submit(locationOptions[data.location]) }
return
for the Edit
function as follows
1 2<> <Form onSubmit={handleSubmit(configureGadget)}> <FormSection> <Label>City<RequiredAsterisk /></Label> <Textfield {...register("city", { required: true, onChange: getOptions() })} /> <Label>Country<RequiredAsterisk /></Label> <Textfield {...register("country", { required: true })} /> {showOptions && <Label>Select your location<RequiredAsterisk /></Label>} {showOptions && ( <RadioGroup {...register("location", {required: true})} options={locationOptions.map(locationOption)}/> )} {errors["location"] && <ErrorMessage>Select a location</ErrorMessage>} </FormSection> <FormFooter> {showOptions && <Button appearance="primary" type="submit"> Submit </Button>} </FormFooter> </Form> </>
return
in the View
function to display the latitude and longitude using the location returned from the Geolocation service,
1 2<> <Text>City: {gadgetConfiguration["name"] ? gadgetConfiguration["name"] : "Edit me"}</Text> <Text>Country: {gadgetConfiguration["country"] ? gadgetConfiguration["country"] : "Edit me"}</Text> <Text>Lon: {gadgetConfiguration["lon"] ? gadgetConfiguration["lon"] : "Edit me"}</Text> <Text>Lat: {gadgetConfiguration["lat"] ? gadgetConfiguration["lat"] : "Edit me"}</Text> </>
Your src/resolvers/index.js
should look like this:
1 2import Resolver from '@forge/resolver'; import { fetch } from '@forge/api' const resolver = new Resolver(); resolver.define('getText', (req) => { console.log(req); return 'Hello, world!'; }); resolver.define('getLocationCoordinates', async (req) => { if(req.payload.location) { const config = req.payload.location; const url = "https://api.openweathermap.org/geo/1.0/direct?q=" + config.city + "," + config.country + "&limit=5&appid=" + process.env.OPENWEATHER_KEY; const response = await fetch(url) if(!response.ok) { const errmsg = `Error from Open Weather Map Geolocation API: ${response.status} ${await response.text()}`; console.error(errmsg) throw new Error(errmsg) } const locations = await response.json() return locations; } else { return null; } }); export const handler = resolver.getDefinitions();
Your src/frontend/index.jsx
should look like this:
1 2import React, {useEffect, useState} from "react"; import ForgeReconciler, { Text, useProductContext, Textfield, Form, Button, FormSection, FormFooter, Label, RequiredAsterisk, useForm, RadioGroup, ErrorMessage, } from "@forge/react"; import { invoke, view } from "@forge/bridge"; let currentCC = null; export const Edit = () => { const { handleSubmit, register, getValues, formState } = useForm(); const [locationOptions, setLocationOptions] = useState(null); const [showOptions, setShowOptions] = useState(false); const { errors } = formState; const getOptions = () => { const values = getValues(); if(values.city && values.country){ if(currentCC && (currentCC.city == values.city)&&(currentCC.country == values.country)) { } else { currentCC = { city: values.city, country: values.country } invoke('getLocationCoordinates', {location: values}).then((val) => { setLocationOptions(val); setShowOptions(true); }); } } }; const configureGadget = (data) => { view.submit(locationOptions[data.location]) } function locationOption(obj, index, array) { return { name: "location", label: obj.name + ", " + obj.state + ", " + obj.country, value: index } } return ( <> <Form onSubmit={handleSubmit(configureGadget)}> <FormSection> <Label>City<RequiredAsterisk /></Label> <Textfield {...register("city", { required: true, onChange: getOptions() })} /> <Label>Country<RequiredAsterisk /></Label> <Textfield {...register("country", { required: true })} /> {showOptions && <Label>Select your location<RequiredAsterisk /></Label>} {showOptions && ( <RadioGroup {...register("location", {required: true})} options={locationOptions.map(locationOption)}/> )} {errors["location"] && <ErrorMessage>Select a location</ErrorMessage>} </FormSection> <FormFooter> {showOptions && <Button appearance="primary" type="submit"> Submit </Button>} </FormFooter> </Form> </> ); }; const View = () => { const [data, setData] = useState(null); const context = useProductContext(); useEffect(() => { invoke('getText', { example: 'my-invoke-variable' }).then(setData); }, []); if (!context) { return "Loading..."; } const { extension: { gadgetConfiguration }, } = context; return ( <> <Text>City: {gadgetConfiguration["name"] ? gadgetConfiguration["name"] : "Edit me"}</Text> <Text>Country: {gadgetConfiguration["country"] ? gadgetConfiguration["country"] : "Edit me"}</Text> <Text>Lon: {gadgetConfiguration["lon"] ? gadgetConfiguration["lon"] : "Edit me"}</Text> <Text>Lat: {gadgetConfiguration["lat"] ? gadgetConfiguration["lat"] : "Edit me"}</Text> </> ); }; const App = () => { const context = useProductContext(); if (!context) { return "This is never displayed..."; } return context.extension.entryPoint === "edit" ? <Edit /> : <View />; }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Your manifest.yml
should look like this:
1 2modules: jira:dashboardGadget: - key: weather-gadget-dashboard-gadget-ui-kit-2-hello-world-gadget title: weather-gadget description: A weather dashboard gadget. thumbnail: https://developer.atlassian.com/platform/forge/images/icons/issue-panel-icon.svg resource: main-resource render: native resolver: function: resolver edit: resource: main-resource render: native function: - key: resolver handler: index.handler resources: - key: main-resource path: src/frontend/index.jsx app: runtime: name: nodejs18.x id: [YOUR_ID] permissions: external: fetch: backend: - api.openweathermap.org
In the next step you will call the OpenWeather current weather API, and display the results using UI Kit layout elements.
Rate this page: