In part I and II you created your Forge UI kit app using the Jira dashboard gadget template, and modified the app to:
@forge/api
package,In this section, you will call the OpenWeather Current weather API and display the current weather conditions for the users nominated location.
In this section, you will create a resolver function that will use fetch
to call the Current weather data API with the lon
and lat
you got in the previous part of the tutorial.
Navigate to the src/resolvers
directory, and open index.js
.
Create a new resolver called getCurrentWeather
that will call the Current weather data API https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&units=metric&appid={API key}
and return the weather.
If you're stuck, use the getLocationCoordinates
resolver as a template for your new resolver, and remember you can access the lon and lat via the gadgetConfiguration within the context information ie req.context.extension.gadgetConfiguration.lon
and req.context.extension.gadgetConfiguration.lat
You'll find an example solution at the bottom of the page if you're stuck.
Save your changes
In this section, you will invoke the getCurrentWeather
resolver, store the result and display the result via a console.log so it can be viewed in the Browser Console.
From your app's top-level directory run forge tunnel
to start the tunnel so you can test your changes as you go
Navigate to the src/frontend
directory, and open index.jsx
In the View
function, add a new useState
hook to store and set the Weather data:
1 2const [weather, setWeather] = useState(null)
In the View
function, use a useEffect
hook to invoke the getCurrentWeather
1 2useEffect(() => { invoke('getCurrentWeather').then(setWeather); }, []);
In the View
return statement, add {console.log(weather)}
so you can view the weather object in the browser console
Save your changes, and wait for the tunnel to complete the reload then refresh your app and test your changes
Don't forget to check your Browser console to make sure the weather is being received in the frontend so you can display it in the next step.
If something is not working, check your code against the solutions below:
The View
function of your src/frontend/index.jsx
should look like this:
1 2... const View = () => { const [weather, setWeather] = useState(null); const context = useProductContext(); useEffect(() => { invoke('getCurrentWeather').then(setWeather); }, []); if (!context) { return "Loading..."; } const { extension: { gadgetConfiguration }, } = context; return ( <> {console.log(weather)} <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> </> ); }; ...
The getCurrentWeather
resolver in src/resolvers/index.js
should look like this:
1 2... resolver.define('getCurrentWeather', async (req) => { console.log(req.context.extension.gadgetConfiguration) if(req.context.extension.gadgetConfiguration) { const coord = req.context.extension.gadgetConfiguration; const url = "https://api.openweathermap.org/data/2.5/weather?lat=" + coord.lat + "&lon=" + coord.lon +"&units=metric&appid=" + process.env.OPENWEATHER_KEY; const response = await fetch(url) if(!response.ok) { const errmsg = `Error from Open Weather Map Current Weather API: ${response.status} ${await response.text()}`; console.error(errmsg) throw new Error(errmsg) } const weather = await response.json() return weather; } else { return null; } }); ...
Before displaying images from external sites, you need to add the permissions in the app manifest.
In this section, you will modify your manifest.yml
to add the necessary permissions to display images from openweather.org.
Open the manifest.yml
file,
Modify the external permissions as follows:
1 2permissions: external: images: - 'https://openweathermap.org' 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.
In this section, you will use the following UI Kit elements to display the current weather conditions:
Feel free to play with the xcss, box and Inline elements to customise the look of your app.
From your app's top-level directory run forge tunnel
to start the tunnel so you can test your changes as you go
Navigate to the src/frontend
directory, and open index.jsx
Modify the import ForgeReconciler, {...} from "@forge/react"
statement at the top of the file
1 2import ForgeReconciler, { Text, useProductContext, Textfield, Form, Button, FormSection, FormFooter, Label, RequiredAsterisk, useForm, RadioGroup, ErrorMessage, Box, Inline, xcss, Heading, Strong, Image } from "@forge/react";
Replace the View
function return to display the weather information instead of the location data
1 2<Heading as="h2">{weather ? weather.name : 'Loading...'} Weather</Heading> <Image src={weather ? (`https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`) : "https://openweathermap.org/img/wn/01d@2x.png"} alt={weather ? weather.weather[0].description : "Loading"} /> <Text><Strong>Current Temperature</Strong> {weather ? weather.main.temp : '[ ]'} °C</Text> <Text><Strong>Feels like:</Strong> {weather ? weather.main.feels_like : '[ ]'} °C</Text> <Text><Strong>Humidity:</Strong> {weather ? weather.main.humidity : '[ ]'}%</Text>
Next, lets modify the the app to display the Heading, and then the Image on the left with the Text on the right,
1 2<Heading as="h2">{weather ? weather.name : 'Loading...'} Weather</Heading> <Inline> <Image src={weather ? (`https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`) : "https://openweathermap.org/img/wn/01d@2x.png"} alt={weather ? weather.weather[0].description : "Loading"} /> <Box> <Text><Strong>Current Temperature</Strong> {weather ? weather.main.temp : '[ ]'} °C</Text> <Text><Strong>Feels like:</Strong> {weather ? weather.main.feels_like : '[ ]'} °C</Text> <Text><Strong>Humidity:</Strong> {weather ? weather.main.humidity : '[ ]'}%</Text> </Box> </Inline>
We've used an <Inline>
element which will make each of the elements inside it appear horizontally.
But, we don't want the three Text elements to remain stacked vertically, so we've put them inside a <Box>
so they're treated as one by <Inline>
There's a similar <Stack>
element, which allows you to specify elements appear vertically.
Now, there should be a little more space between the heading, and the Image and Text appearing below it, so wrap the Inline in a box to apply styling,
1 2<Heading as="h2">{weather ? weather.name : 'Loading...'} Weather</Heading> <Box xcss={containerStyle}> <Inline> <Image src={weather ? (`https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`) : "https://openweathermap.org/img/wn/01d@2x.png"} alt={weather ? weather.weather[0].description : "Loading"} /> <Box> <Text><Strong>Current Temperature</Strong> {weather ? weather.main.temp : '[ ]'} °C</Text> <Text><Strong>Feels like:</Strong> {weather ? weather.main.feels_like : '[ ]'} °C</Text> <Text><Strong>Humidity:</Strong> {weather ? weather.main.humidity : '[ ]'}%</Text> </Box> </Inline> </Box>
Finally, add a containerStyle
to your app - this will allow you to specify the styling changes,
1 2const containerStyle = xcss({ padding: 'space.200' });
Save index.jsx
, and wait for the tunnel to reload before testing your changes in your app.
Once you're happy with how your app looks, exit the tunnel and deploy your changes.
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, Box, Inline, xcss, Heading, Strong, Image } 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 [weather, setWeather] = useState(null); const context = useProductContext(); useEffect(() => { invoke('getCurrentWeather').then(setWeather); }, []); const containerStyle = xcss({ padding: 'space.200' }); return ( <> {console.log(weather)} <Heading as="h2">{weather ? weather.name : 'Loading...'} Weather</Heading> <Box xcss={containerStyle}> <Inline> <Image src={weather ? (`https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`) : "https://openweathermap.org/img/wn/01d@2x.png"} alt={weather ? weather.weather[0].description : "Loading"} /> <Box> <Text><Strong>Current Temperature</Strong> {weather ? weather.main.temp : '[ ]'} °C</Text> <Text><Strong>Feels like:</Strong> {weather ? weather.main.feels_like : '[ ]'} °C</Text> <Text><Strong>Humidity:</Strong> {weather ? weather.main.humidity : '[ ]'}%</Text> </Box> </Inline> </Box> </> ); }; 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> );
Now that you've successfully built your Jira Weather Gadget, why not try improving on it,
Alternatively, why not start building your own app?
We'd love to hear from you! Provide feedback on Forge Quest
Rate this page: