Guides

The step-by-step guides require the installation of common utilities on your local system.

  • nodejs - JavaScript engine

  • npm - Package manager system

You will also need an editor that understands typescript and JSX. Unless you have another favorite editor, we recommend Visual Studio Code with extensions ESLint, Prettier - Code formatter and Jest.

Getting started

Note

Most of you will already have created a group repository under https://gitlab.met.no/devuke if so you can reuse it, and skip this step!

To start of, we have prepared a simple repository from where we can get oriented. If you haven’t already created a repository, you can do it by

  • Navigating to https://gitlab.met.no/devuke.

  • Click New Project

  • Select From template

  • Select Instance from the navigation bar

  • Click Use template from api-web-exercise

  • Give the project a cool-name and save it under the devuke Project URL.

  • With the project created, you can select preferred way of cloning the repository by clicking the Clone button.

  • Run git clone <copied-clone-method> in the terminal.

You should now have a local repository called cool-name.

1. Workshop 1

In your projects repository, you can now checkout the git branch called frontend-workshop-1

you can do this buy running the command:

git checkout frontend-workshop-1

Note

Make sure that you have the latest changes by running git fetch first.

Build and run the React App:

npm ci
npm start

Check that the application is running in your browser at http://localhost:8080/

In your editor, navigate to the Home.tsx file found under /src/components. This is where we will do most of our work. Inspecting the top of the file, we can see that one of the lazy frontend developers forgot to remove an import for a Mui button component. Luckily for us that is just what we need.

Note

Take some time to look around in the file structure. everything starts from the index.tsx and branches down from there.

Task One

Create a Mui <Button> component within the <Box> component in Home.tsx and call it dark mode.

At the moment the button looks rather bland, and does not do to much. let’s try to fix that.

First we can navigate to the Material ui Button component webpage. This page will quickly become your best friend when working with Material UI.

Try changing the variant and the color of the button as shown in the examples on the page.

Solution
<Button variant="contained" color="secondary">
  dark mode
</Button>

Some of the more nosy people might have wondered “what exactly are these primary and secondary colors?” Good question!

The Material ui theme let’s you easily change the appearance of your application by the flick of a button. let’s do that next.

Task Two

To change the theme we first need to figure out where it comes from. Let’s navigate to the App.tsx file right above. If you haven’t had time to look at this file yet, you can see that it contains our applications Header, Footer and our beloved Home components.

But also something called a ThemeProvider. This is a wrapper around everything that we want to have access to the Material ui theme.

Currently the primary theme is set to a teal_palette, let’s try changing that to something else. In the import statement containing the palettes, add a black_palette and try switching it with the teal_palette as the primary color.

Solution
import { yellow_palette, teal_palette, black_palette } from '../utils/metMuiThemes';

palette: {
  primary: black_palette,
  secondary: yellow_palette,
},

Well that surely changes things up! But doing that ourselevs was a bit tiring work, so let’s make our button do that for us. First we need a way to store the state of the palette, and for this we can use Reacts useState

Try adding a useState inside the App component (before the return statement) called appTheme and setAppTheme. and give it a default value of teal_palette

Hint

Hint

In typescript the state will need a type. If you are lucky maybe one of the developer left one in the imports. The useState would look something like React.useState<MyPaletteType>(my_default_palette);

Solution
const [appTheme, setAppTheme] = React.useState<SimplePaletteColorOptions>(teal_palette);

Now we can start using our state. First let’s set the appTheme variable to be the primary color instead of black_palette Then we can pass the setAppTheme function call to our Home component by adding it like this:

<Home setAppTheme={setAppTheme}/>

Sadly this will temporarily break our application, because the Home component is not expecting anything to be passed to it. Moving back into Home.tsx we need to connect an interfacehttps://www.typescriptlang.org/docs/handbook/interfaces.html to the Component.. and it seems like there is one there already! let’s quickly add it to our React FC component like so:

const Home: React.FC<HomeProps> = ({ setAppTheme }: HomeProps) => {

Here we tell the React Functional Component (FC) that its of type HomeProps which contains the setAppTheme function call that we passed down from App.tsx earlier. Now our application is working again 🎉.

Finally we need to make our button actually switch the Theme. Try going back to the Matrerial UI Button page, and see if you can figure out how! While you are at it, you can also remove the color="secondary" and see that the button also changes with the theme!

Hint

Hint

The Button component has a function called onClick that might be useful. Also Don’t forget that React rerenders on every change, so it might be wise to add an arrow function like () => myFunctionCall() if you end up in an infinite loop.

Solution
import { black_palette, pageSpacing } from '../utils/metMuiThemes';

<Button
  variant="contained"
  onClick={() => setAppTheme(black_palette)}
>
 dark mode
</Button>

Note

Scrolling to the bottom of a Material ui component page gives you the links to the displayed components API’s. Here you can easily see what they can and cannot do!.

And that’s it! If you are done early, try helping others or just play a bit around with the program. Maybe add some new Material ui components, make changes to the Theme or even add a new component file you can try to add to import somewhere yourself.

2. Workshop 2

First things first, let’s check out the frontend-workshop-2 branch so that we all are on the same page. In this section we will have a look at how you can test your code, and what WCAG are up to these days.

Let’s start by running our application again with npm run start.

At first glance everything seems to be working fine. We left off creating a button that can change the theme on a click. Our frontend developers seem to have tried adding some functionality where you can flip the theme back and forth, but this does not seem to be working. And it’s not even tested!😠 Let’s start by adding a test, so we know what should happen.

Task One - Workshop 2

Since we are testing the Home component, let’s create a new file called Home.test.tsx under components in the file structure. The syntax for making a jest test can be seen in the Footer.test.tsx file, let’s just copy the content from here into our newly made file.

Since we are not testing a twitter link here, we can change its description to something like should toggle theme on click. When that is done, we can have a look at what the current code does.

const { getByTestId } = render(<Footer />);
expect(getByTestId('twitter-button').getAttribute('href')).toEqual(
   'https://twitter.com/Meteorologene',
);

Here we are rendering the <Footer> component and using the getByTestId function to get our twitter icon button. We then test that the button’s link goes to where it should. For now we can just remove the expect statement (we will come back to it later). Making our test look like this

it('should toggle theme on click', () => {
   const { getByTestId } = render(<Footer />);
});

Since we are not testing the Footer component here, we can switch it out with Home instead. With jest there are many ways to achieve your goal. For the Home test we can try to get our button component by using the getByText function instead. making our render look something like this

const { getByText } = render(<Home/>);

If your text editor is smart enough- It should now be complaining a bit. The Home component does have prop passed to it after all. When working with testing we don’t want to use any actually react state, instead we mock it. So to our component we can first create, then pass a MockFunction like so:

const mockSetTheme = jest.fn();
const { getByText } = render(<Home setAppTheme={mockSetTheme} />);

Note

read more about mock functions here https://jestjs.io/docs/mock-functions.

Now all errors should have disappeared. Let’s try to use the getByText function to get our button. To do this simply call it with your buttons name as an argument. We can quickly test that it works by printing it to the console.

console.log(getByText('dark mode'));

If we now try running the tests in the terminal with the command npm run test. We should see it print a HTMLButtonElement.

Note

It’s important that the name of the button is exactly the same as what’s written in getByText.

Now that we got our button, let’s try using it. Jest has a practical function called fireEvent which let’s you, well, fire events. instead of logging out the button to console, we can wrap it with fireEvent.click

fireEvent.click(getByText('dark mode'));

The button has now been clicked! At least we hope that is the case. We can make sure by adding an expect statement to make the test run.

expect(mockSetTheme).toHaveBeenCalled()

After running the tests again, we can see that both our test suit pass, nice! The current test only checks if the function has been called. See if you can figure out what the button actually gets called with, and then make the checks needed for the test to fail correctly!

Hint

Hint

the expect option toHaveBeenCalledWith might be useful in this situation.

As mentioned there are many ways this can be solved. this is just one of them!

Solution
fireEvent.click(getByText('dark mode'));
expect(mockSetTheme).toHaveBeenCalledWith(teal_palette);

Now we should have a successfully failing test!

Task Two - Workshop 2

Let’s fix our failing test by solving the issue in Home.tsx. The developers have added a conditional operator to set the theme:

setAppTheme(black_palette ? black_palette : teal_palette)

The operator checks if the first argument (black_palette) is truthy, if so it returns the first value (black_palette), if not the second value (teal_palette). The idea here is good, but the execution is not so much. Try to figure out whats wrong, and use all your recently required knowledge to fix the issue and solve failing tests.

Hint

Hint

The first argument black_palette will always be true! maybe we should try to use the appTheme value instead?

Note

This task can be tricky! if you find yourself stuck try working together or have a peek at the solution found in the frontend-workshop-3 branch.

Extra tasks! - Workshop 2

So you think you are quick huh? well have some of these..

1. (small task) - When you get the tests working again, apply the same logic to the name of the button. Changing its name to light theme if the black_palette is chosen, and dark theme if the teal_palette is. How do you think this can affect the tests?

Solution
{appTheme === teal_palette ? 'dark mode' : 'light mode'}

Note

Since getByText uses the name of our button to find the component, changing its name can cause some unwanted issues down the line! Maybe it would be better to get it in another way. But for now just make sure that it works!

2. (medium task) - It seems like someone with little to no WCAG experience has changed some of the code from the first task. Can you spot the WCAG errors? I would guess there are about 2 of them.. but if you find more they are there on purpose!

Hint

Hint

In WCAG contrasts are an important part. And the same can be said about page navigation!

Solution
1. The search icon button has gotten its color changed to 'primary.light' which is not enough contrast to the background.
2. The Met på facebook iconbutton in the footer has gotten a 'tabIndex={1}', this breaks the continuous flow of the page for screen readers.

3. Workshop 3

For the final part of this workshop we will look at fetching and using data from an API. Most of you have already created an endpoint containing some weather data that can be used for this purpose. First let’s rebuild and start our container to run this locally.

Navigate to api-web-exercise/api folder and run the following for docker:

docker build . -t devuke-api
docker run -p 5000:5000 devuke-api

or podman commands:

podman build --tag devuke -f Dockerfile
podman run -p 5000:5000 localhost/devuke

Make sure that the API is running by opening http://0.0.0.0:5000 in your browser. If everything looks good we can go back to the api-web-exercise/frontend location in a new terminal. When it comes to fetching data in a react application there are a lot of options with pros and cons depedning on what you want to achieve. In this example we will go over the use of Fetch which comes with javascript. Some other libraries to look into could be eg Axios or Tanstack

Task One - Workshop 3

First off let’s get to know how fetching data in react works. we can loosely separate the concept of “data fetching” into two categories: initial data fetching and data fetching on demand. Where data on demand is something that you fetch after a user interacts with a page, and initial data is the data you’d expect to see on a page right away when you open it.

Let’s start out by trying to fetch something from our API the data on demand way. Add a new button called fetch and a async handler function called fetchTimeSeriesData inside the Home component:

async function fetchTimeSeriesData() {
}

Then we can add our fetch logic like this:

async function fetchTimeSeriesData(url: string) {
   await fetch(url)
     .then((response) => response.json())
     .then((data) => console.log(data))
     .catch((err) => {
       console.log(err);
     });
}

Now what happens here? Well, at the moment not much. since nothing is calling the fetchTimeSeriesData function. But if something did, the fetch would first try to read the response from the url as json, and if successful try to console log the data. If not successful an error message will be logged.

Note

Fetch has a lot of options when it comes to headers, requests and responses. read more about it here.

Try calling the fetchTimeSeriesData function from our buttons onClick with a url that returns json from our endpoint. Then check in the console that the object gets printed!

Note

Your browsers console can usually be opened by clicking ctrl + shift + J or simply rightclicking on you page and choosing inspect

Hint

Hint

You can try using the http://0.0.0.0:5000//collections endpoint. call the function with it as a string eg: fetchTimeSeriesData(“mycoolurlendpoint”)

Solution
<Button
    variant="contained"
    onClick={() => fetchTimeSeriesData('http://0.0.0.0:5000/collections')}
    >
 fetch
 </Button>

Task Two - Workshop 3

Now that we know how we can get our data, let’s try using it. First off let’s change the URL to fetch some air temperature data from oslo. While we are at we it we can use the config.API_ENDPOINT variable passed down to the Home component like this:

`${config.API_ENDPOINT}/collections/test/locations/oslo?parameter-name=air_temperature_2m`

The config is a json file that gets overwritten in k8s. This way your endpoint URL will work in production as well! When that is done, we need some place to store the data. Let’s add a useState to our Home component:

const [timeSeries, setTimeSeries] = React.useState<WhatType?>();

In javascript we could then simply set the data to our time series state, but since we are working with Typescript it needs a type. And looking a the json output it kinda looks like a tricky one. For situations like these there are helpful tools like Paste JSON as code in vscode or just about any online json to interface webpage. But luck has it that someone already did this in our new types.ts file. We can then set the useState type to TimeSeries and import it accordingly. Finally let’s set our state instead of logging it out to the console:

.then((data) => setTimeSeries(data))

Try logging the timeSeries object out in the console and see if you can find the timesteps and the temperature values.

Note

If you are using your own API, make sure that the collection has the right variable. in the example URL above it is set to test

To display out data we are going to use a charting library called echarts. There are again many options to go for here, but after some extensive testing this is the library we recommend. Let’s add a echarts and a echarts react wrapper to our project by running the npm install commnads within the frontend folder in our terminal:

npm i echarts
npm i echarts-for-react

Note

The wrapper makes it a bit easier for us to use the echarts library in react. its not necessarily needed, just practial.

When the packages are installed, we can add a chart above the This is a good place to add some content! text in our code:

<ReactECharts option={buildOptions()} />

Note

Remember to import the library it if it does not happen automagically.

import ReactECharts from 'echarts-for-react';

This will temporarily break our application, since we are not passing in any buildOptions() function. Let’s navigate to the echarts examples page and see what the option element should look like. We choose the Basic Line Chart for now. Here the chart is displaying some data based on weekdays. Let’s try using that, add a function above Home like this:

function buildOptions(): any {
   return {
      xAxis: {
         type: 'category',
         data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      },
      yAxis: {
         type: 'value',
      },
      series: [
         {
         data: [150, 230, 224, 218, 135, 147, 260],
         type: 'line',
         },
      ],
   };
}

Your breaking page should now be working again, and even showing a graph! Next let’s try using our own data instead. Since we are going to display a timeseries and not weekdays, we can first change the xAxis type to time instead of category. We will also add both our time and values into the series.data element in a double array that looks something like this:

[[timestep, value], [timestep, value], ...]

So you can safely remove data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] from the xAxis, only leaving the type.

Note

You can read more about the data structure in the eCharts api here.

Now see if you manage to merge find and merge time and value into a double array. Make sure it looks correct by again logging the result to the console.

Note

This can be a bit tricky if you are new to frontend! dont be scared to look at hints or solutions! And remember to click your fetch button to actually get the results.

Hint

Hint

There are multiple ways to do this, but they all start by finding out where our data is. The timesteps and values can be found under

timeSeries.domain.axes.t.values
timeSeries.ranges['air_temperature_2m'].values

Now we need to merge them somehow. Using the map function can be quite practical here. You can add it at the end of one of your values. The syntax is something like:

const myPlussOneArray = myArray.map((element, index) => element + 1)

In this example we add 1 to every value in the myArray list, then return it as a new array called myPlussOneArray.

Solution
console.log(timeSeries?.ranges['air_temperature_2m'].values.map(
   (value, index) => [timeSeries?.domain.axes.t.values[index], value],
))

Note

We are guessing here that there will always be a one to one value and timestep, as well as the order being correct. The ? added after the timeSeries object removes the pesky undefined error by conditionally rendering it. We will solve this in a better way soon.

Now we can overwrite the series.data values [150, 230, 224, 218, 135, 147, 260] in our buildOptions function with our merged data. We also need to pass the (timeSeries: TimeSeries) to our function so that we actually get the data we are merging from.

Solution
function buildOptions(timeSeries: TimeSeries): any {
   return {
      xAxis: {
         type: 'time',
      },
      yAxis: {
         type: 'value',
      },
      series: [
         {
         data: timeSeries.ranges['air_temperature_2m'].values.map(
            (value, index) => [timeSeries.domain.axes.t.values[index], value],
         ),
         type: 'line',
         },
      ],
   };
}

Note

We are guessing here that there will always be a one to one value and timestep, as well as the order being correct.

At the moment your buildOptions should be complaining that type undefined is not assignable to type TimeSeries. This happens because our timeSeries state is not set on the initial render, so we have to wait for the fetch to finish first. We can fix this by first checking if the timeSeries is a Coverage object before even rendering the graph to our page. And if it is not, we can just display our default text, but edit it to say something generic like Missing data.:

{timeSeries?.type === 'Coverage' ? (
       <ReactECharts option={buildOptions(timeSeries)} />
     ) : (
       <Typography variant="h5" sx={{ paddingTop: 25, textAlign: 'center' }}>
         Missing data.
       </Typography>
     )}

Now we again should have a working graph, albeit a bit weird because of the timesteps. Let’s quickly clean that up by adding:

axisLabel: {
    formatter: function (value: any) {
      return new Date(value).toLocaleString('nb-NB').split(',')[0];
    },
  },

Inside our xAxis option. Then adding a tooltip in the base of the object:

tooltip: {
   trigger: 'axis',
 },
Solution
function buildOptions(timeSeries: TimeSeries): any {
   return {
      xAxis: {
         type: 'time',
         axisLabel: {
         formatter: function (value: any) {
            return new Date(value).toLocaleString('nb-NB').split(',')[0];
         },
         },
      },
      yAxis: {
         type: 'value',
      },
      tooltip: {
         trigger: 'axis',
      },
      series: [
         {
         data: timeSeries.ranges['air_temperature_2m'].values.map(
            (value, index) => [timeSeries.domain.axes.t.values[index], value],
         ),
         type: 'line',
         },
      ],
   };
}

Note

We are guessing here that there will always be a one to one value and timestep, as well as the order being correct.

Now our page is showing a graph with data from our API and it even got a hover so you can see the values more closely. well done!

Task Three - Workshop 3

Authentication!

In our endpoint URI try changeing the location from oslo to bergen:

`${config.API_ENDPOINT}/collections/test/locations/bergen?parameter-name=air_temperature_2m`

If we now click on the FETCH-button. No data is displayed? If we again open the browser console there is an error message. The backend returns an 401 (Unauthorized) HTTP response code. The reason is that the backend has protected this location and it wants us to login before we can view the data.

To fix this, we first need to create some login functionallity. If we look at burger menu in our header we can see that there is an option called login! Lets navigate to the Menu.tsx file in our components to see what the button does. Here we can see a <MenuList> element containing two conditional <MenuItem>’s:

{!isLoggedIn ? (
   <MenuItem onClick={handleLogin}>Login</MenuItem>
 : (
   <MenuItem onClick={handleLogout}>Logout</MenuItem>
)}

So if the isLoggedIn variable is true we render the login button, and if not the logout one. Clicking the login button seems to work, prompting us to do a AD login. But the state does not switch nor are we able to fetch our well hidden bergen data.

To solve this we will start by making code to validate if the user is logged in or not.

For this we will use something that’s called a useEffect hook. The hook let’s you choose what to do when a given state changes, let’s try adding a simple example and see what happens. Add this code to the Menu component.

useEffect(() => {
   console.log('Im in a useEffect!');
});

Try opening your browser’s console and refresh the page. You should now see the text printed in the console. So how does this update based on a given state? Well at the moment it does not, it actually runs whenever any state changes in the Menu component. Try switching the theme back and forth and see what happens in the console. Fetching data like this would be quite suboptimal, so let’s try adding a dependencies array to the useEffect

useEffect(() => {
   console.log('Im in a useEffect!');
},[]);

Adding an empty array tells the useEffect to only run on an initial render, so now a button clicking should not print out the message like it did earlier. For fun try adding the appTheme variable inside the brackets and you will notice that it again starts updating whenever we change its state.

Note

useEffect can be a tricky hook to use correctly. In most cases it’s advised to work around using it if possible. But for smaller applications it works quite well!

Now that our useEffect hook only renders when we want it, we can add a fetch statement to it. We can use the fetchData function in the Menu. This function is used to log the user out, but if we change the endpoint we can check if the user is logged in or not. We just need to change the endpoint to /check_login.

useEffect(() => {
   fetchData(`${config.API_ENDPOINT}/check_login`);
},[]);

In our console we can now see that we are trying to check our login status when the page renders. But why do we still get 401 (Unauthorized) http code from the endpoint when it runs?

This is because the backend is setting the login as a coockie session, but fetch will as default only send user credentials if the endpoint is at the same origin (protocol, domain, and port). Try setting the header in the fetch call to credentials: 'include'.

Hint

Hint

The headers can be set in the options object in fetch

Solution
async function fetchData(url: string) {
  await fetch(url, {
    credentials: 'include',
  })
    .then((response) => response.json())
    .then((data) => {
      console.log(data.detail);
      if (data.detail === 'Active login session found') {
        setIsLoggedIn(true);
      } else {
        setIsLoggedIn(false);
      }
    })
    .catch((err) => {
      console.error(err);
    });
}

We also needs to add this in the fetch call in Home. Add credentials: 'include' to fetchTimeSeriesData.

Solution
async function fetchTimeSeriesData(url: string) {
  await fetch(url, {
    credentials: 'include',
  })
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((err) => {
      console.log(err);
    });
}

Now you can try logging in. If the login is successful you should a logout text in the menu. You can try logging in and out.

If you now click on the ‘FETCH’-button data will be displayed if you are logged in.

For this to work the backend needs to have set the correct CORS-settings. Cross-Origin Resource Sharing (CORS)

Note

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

The backend needs to whitelist all the origins that are allowed to connect with credentials: 'include'. So for it to work for our frontend running on localhost the backend needs to return this header in the http respons:

Access-Control-Allow-Origin: http://localhost:8080

If you try to remove http://localhost:8080 from the backend in origins in the file main.py and start the backend again. Then the authentication should no longer work.

Note

Why is CORS important?

CORS is important because it allows browsers to enforce the same-origin policy. The same-origin policy is a security measure that prevents a malicious script from accessing resources that it should not have access to.

Without CORS, a malicious script could make a request to a server in another domain and access the resources that the user of the page is not intended to have access to.

Well done! that was all for this workshop. There are a lot of things that can be added and improved, try playing with the code yourself or do some of the extra tasks below if you are up for it!

Extra task / Future work!

If this has been a breeze for you so far, or you just want to make things look even better: there are many things that can be changed or improved! Here’s a list that gets harder by the step. See how far you can come!

  1. (small task) - See if you can get the “unit” displayed in the graph/hover.

  2. (medium task) - Try adding a second timeseries in the graph by using the pressure property.

  3. (large task) - In the App component we are passing the API_ENDPOINT variable as a prop. This could instead be done with React Context.

  4. (very large task) - At the moment we are hardcoding the weathertype “air_temperature_2m”, this should be fetched and set from the collection/{collection} “observedProperty”