Last updated Jun 1, 2024

Your next quest - Part III

In part I and II you created your Forge UI kit app using the Jira dashboard gadget template, and modified the app to:

In this section, you will call the OpenWeather Current weather API and display the current weather conditions for the users nominated location.

This is part three of the tutorial. Complete Part one and Part two before working through this page.

Create a new resolver function to get the Current weather

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.

  1. Navigate to the src/resolvers directory, and open index.js.

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

  3. Save your changes

Call the getCurrentWeather resolver from the frontend

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.

  1. From your app's top-level directory run forge tunnel to start the tunnel so you can test your changes as you go

  2. Navigate to the src/frontend directory, and open index.jsx

  3. In the View function, add a new useState hook to store and set the Weather data:

    1
    2
    const [weather, setWeather] = useState(null)
    
  4. In the View function, use a useEffect hook to invoke the getCurrentWeather

    1
    2
    useEffect(() => {
      invoke('getCurrentWeather').then(setWeather);
    }, []);
    
  5. In the View return statement, add {console.log(weather)} so you can view the weather object in the browser console

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

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

...

Update the Permissions to allow the App to display Images from openweathermap.org

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.

  1. Open the manifest.yml file,

  2. Modify the external permissions as follows:

    1
    2
    permissions:
      external:
        images:
          - 'https://openweathermap.org'
        fetch:
          backend:
            - api.openweathermap.org
    

    For more information on external permissions, see Permissions - External permissions in the Forge docs.

  3. Save the changes you made to manifest.yml

  4. From your app's top-level directory run forge deploy, the following warning message will appear

    1
    2
    We've detected new scopes or egress URLs in your app.
    Run forge install --upgrade and restart your tunnel to put them into effect.
    
  5. From your app's top-level directory run forge install --upgrade and follow the prompts to upgrade your app installation.

Make changes to the App View to display the Weather

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.

  1. From your app's top-level directory run forge tunnel to start the tunnel so you can test your changes as you go

  2. Navigate to the src/frontend directory, and open index.jsx

  3. Modify the import ForgeReconciler, {...} from "@forge/react" statement at the top of the file

    1
    2
    import ForgeReconciler, { Text, useProductContext, Textfield, Form, Button, FormSection, FormFooter, Label, RequiredAsterisk, useForm, RadioGroup, ErrorMessage, Box, Inline, xcss, Heading, Strong, Image } from "@forge/react";
    
  4. 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>
    
  5. 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.

  6. 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>
    
  7. Finally, add a containerStyle to your app - this will allow you to specify the styling changes,

    1
    2
    const containerStyle = xcss({
      padding: 'space.200'
    });
    
  8. Save index.jsx, and wait for the tunnel to reload before testing your changes in your app.

  9. 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
2
import 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>
);

Next steps

Now that you've successfully built your Jira Weather Gadget, why not try improving on it,

Alternatively, why not start building your own app?

Feedback

We'd love to hear from you! Provide feedback on Forge Quest

Rate this page: