A Github PR Dashboard with ReactJS and Material UI
Published: 2021-12-08 22:31:47

Dashboards are great for visualization and keeping track of what's going on. Managing a repository for a team can be cumbersome and keeping a track of all the pull requests even so. One potential solution could be the dashboard, dashboards are great for visualization and keeping track of what's going on. 

In this article, we will discuss how to create a Github Pull Requests Dashboard using ReactJS and Material UI.

## Let's get started


1. **Setting up React**
    
    Let's begin by creating a boilerplate using the `create-react-app`
    
    ```jsx
    npx create-react-app github-pr-dashboard
    ```

2. **Install Material UI**
    
    We will be using Material UI as a UI library. It boosts up the speed of development as we won't have to manually write CSS.
    
    ```jsx
    yarn add @mui/material @emotion/react @emotion/styled
    ```

3. **Creating a Container component**
    
    We will be using a `Container` component to display the columns corresponding to different stages to the PR. Create a directory inside `src` called **components**, then create another directory inside of it called `Container`. Create an `index.js` file that will contain our display logic. 
    
    ```jsx
    import React from "react";
    
    import {
      Card,
      CardContent,
      AppBar,
      Box,
      Toolbar,
      Typography,
      Stack,
    } from "@mui/material";
    
    import "./style.css";

    const Container = () => {
      return (
        <>
          <Box sx={{ flexGrow: 1 }}>
            <AppBar position="static">
              <Toolbar variant="dense">
                <Typography
                  variant="h6"
                  color="inherit"
                  component="div"
                  textAlign="center"
                  width="100%"
                >
                  Github PR Dashboard
                </Typography>
              </Toolbar>
            </AppBar>
          </Box>
          <div className="container">
            <Card className="card">
              <Stack
                direction="row"
                alignItems="center"
                justifyContent="space-between"
              >
                <Typography gutterBottom variant="h5">
                  Merged
                </Typography>
              </Stack>
              <CardContent className="cardContent">
                {/* PRs to be displayed here */}
              </CardContent>
            </Card>
            <Card className="card">
              <Stack
                direction="row"
                alignItems="center"
                justifyContent="space-between"
              >
                <Typography gutterBottom variant="h5">
                  In Review
                </Typography>
              </Stack>
              <CardContent className="cardContent">
                {/* PRs to be displayed here */}
              </CardContent>
            </Card>
            <Card className="card">
              <Stack
                direction="row"
                alignItems="center"
                justifyContent="space-between"
              >
                <Typography gutterBottom variant="h5">
                  Assigned
                </Typography>
              </Stack>
              <CardContent className="cardContent">
                {/* PRs to be displayed here */}
              </CardContent>
            </Card>
            <Card className="card">
              <Stack
                direction="row"
                alignItems="center"
                justifyContent="space-between"
              >
                <Typography gutterBottom variant="h5">
                  Open
                </Typography>
              </Stack>
              <CardContent className="cardContent">
                {/* PRs to be displayed here */}
              </CardContent>
            </Card>
          </div>
        </>
      );
    };
    
    export default Container;
    ```
    
    src/components/Container/Container.js
    
    Create a CSS file as well and name it `style.css`. We will write some custom CSS to display the columns in a 2*2 grid.
    
    ```css
    .container {
      width: 100%;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: space-evenly;
      margin: 2rem auto;
    }
    
    .card {
      width: calc(100% * (1 / 2) - 5rem);
      height: 400px;
      margin: 1.3rem;
      padding: 1rem;
    }
    @media only screen and (max-width: 600px) {
      .card {
        width: 100%;
      }
    }
    .cardContent {
      height: 80%;
      overflow: auto;
    }
    ```
    
    src/components/Container/style.css
    
    Simply import this component in `src/index.js` removing `App.js` in the process.

    ![Step 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6pdlbcv5crr9calxt1k0.png) 

4. **Create a List component**
    
    This component will display the PR details such as title, username etc. Create another component named `List` and create an `index.js` file inside of it. We will set it up for displaying the desired details.
    
    ```jsx
    import React from "react";
    import { Avatar, Paper, Typography, Fab } from "@mui/material";
    
    import "./style.css";

    const List = () => {
      return (
        <Paper elevation={3} sx={{ padding: 2, margin: 2 }}>
          <Typography variant="body1" color="text.secondary">
            Pull Request 1
          </Typography>
          <Fab variant="extended" size="small">
            Bug
          </Fab>
          <div className="avatar">
            {/* Profile picture will be displayed here */}
            <Avatar sx={{ width: 24, height: 24 }} />
            <Typography variant="body2" sx={{ ml: 1 }}>
              John Doe
            </Typography>
          </div>
        </Paper>
      );
    };
    
    export default List;
    ```
    
    src/components/List/List.js
    
    We will add a bit of custom CSS to align Profile picture and username. Let's create `style.css`.
    
    ```css
    .avatar {
      display: flex;
      align-items: center;
      margin: 0.4rem 0;
    }
    ```
    
    src/components/List/style.css
    
    We will then import the List component into Container and call it in CardContent. 

    Import `List` component into `Container` component and replace it at every `/* PRs to be displayed here */` comment.
    
    ```jsx
    <CardContent className="cardContent">
        <List /> //Calling the List component
    </CardContent>      
    ```
    
    src/components/Container/Container.js

    ![Step 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/git7rv3mappqmysdmk3f.png)
 
    
5. **Create a Modal component**
    
    Let's create a modal component to display more details about the PR when clicked on.
    
    ```jsx
    import React from "react";
    import {
      Avatar,
      Typography,
      Button,
      Fade,
      Modal,
      Box,
      Backdrop,
    } from "@mui/material";
    
    export default function Modals() {
      const [open, setOpen] = React.useState(false);
      const handleOpen = () => setOpen(true);
      const handleClose = () => setOpen(false);
      return (
        <div>
          <Modal
            aria-labelledby="transition-modal-title"
            aria-describedby="transition-modal-description"
            open={open}
            onClose={handleClose}
            closeAfterTransition
            BackdropComponent={Backdrop}
            BackdropProps={{
              timeout: 500,
            }}
          >
            <Fade in={open}>
              <Box className="box">
                <div className="avatar">
                  {/* Profile picture will be displayed here */}
                  <Avatar sx={{ width: 24, height: 24 }} />
                  <Typography variant="body2" style={{ marginLeft: 10 }}>
                    John Doe
                  </Typography>
                </div>
                <Typography id="transition-modal-title" variant="h4" component="h2">
                  Pull Request 1
                </Typography>
                <Typography id="transition-modal-description" sx={{ mt: 2, mb: 2 }}>
                  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                  eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
                  enim ad minim veniam, quis nostrud exercitation ullamco laboris
                  nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
                  reprehenderit in voluptate velit esse cillum dolore eu fugiat
                  nulla pariatur. Excepteur sint occaecat cupidatat non proident,
                  sunt in culpa qui officia deserunt mollit anim id est laborum.
                </Typography>
                {/* This will be displayed only if the PR has an assignee assigned */}
                {
                  <>
                    <Typography variant="h6">Assigned to</Typography>
                    <div style={{ display: "flex", alignItems: "center" }}>
                      {/* Assignee's profile picture will be displayed here */}
                      <Avatar sx={{ width: 24, height: 24 }} />
                      <Typography variant="body2" style={{ marginLeft: 10 }}>
                        Jane Doe
                      </Typography>
                    </div>
                  </>
                }
                <Typography id="transition-modal-description" sx={{ mt: 2 }}>
                  <Button
                    variant="contained"
                    href="#"
                    target="_blank"
                    rel="noreferrer"
                    sx={{ mt: 2 }}
                  >
                    Open PR
                  </Button>
                </Typography>
              </Box>
            </Fade>
          </Modal>
        </div>
      );
    }
    ```
    
    src/components/Modal/Modal.js

    Let's import this component inside the `List` component and define a onClick for handling PR click. We will use props to pass the value for open and close inside modal component.
    
    ```jsx
    ...
    ...
    import Modals from "../Modal";
    ...
    const List = () => {
      const [isOpen, setIsOpen] = React.useState();
      const handleClick = React.useCallback((pr, identifier) => {
        setIsOpen(true);
      }, []);
      return (
        <>
          <Paper
            elevation={3}
            sx={{ padding: 2, margin: 2 }}
            onClick={() => handleClick()}
          >
            ...
          </Paper>
          {/* we are passing isOpen and setIsOpen states to Modal, to manage open and close modal on click */}
          <Modals open={isOpen} setOpen={setIsOpen} /> 
        </>
      );
    };
    
    export default List;
    ```
    
    src/components/List/List.js
    
    We have to go back to Modal, to accept the new props.
    
    ```jsx
    ...
    ...
    
    export default function Modals({ setOpen, open }) {    
      const handleClose = React.useCallback(() => {
        setOpen(!open);
      }, [open, setOpen]);
      return (
        <div>
          ...
        </div>
      );
    }
    ```
    
    src/components/Modal/Modal.js
    
    We will also need CSS to correctly display the Modal, create a style.css file inside Modal directory and then import in the component.
    
    ```css
    .box {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 50%;
      height: 40%;
      overflow: auto;
      background: #fff;
      border: 2px solid #000;
      box-shadow: 24px;
      padding: 4rem;
    }
    
    .avatars {
      display: flex;
      align-items: center;
      margin: 1rem 0 2rem 0;
    }
    ```
    
    src/components/Modal/style.css
    
    ![Step 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qff21u8elu7xpv1v9uca.png)
 
    
    ˳˳˳Now all the components that will display the data is ready. It's time to integrate the backend.
    
6. **Time to get your APIs ready!**
        
    Getting your APIs for Github Dashboard comes at real ease. All you have to do is clone this production-ready [project](https://app.canonic.dev/projects/61b0eedde08af1002f135b04/graph?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_2&utm_term=step6_project&utm_content=day_10) on Canonic and you're done. It will provide you with the backend, APIs, and documentation you need for integration, without writing any code. Just make sure you add your own Github's credentials (You don't need to enter the credentials if you are fetching open-source repository)
        
    ![Step 6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh316gnhmafuzk2lr8t9.png)   
    

---

## Backend integration with GraphQL

Let's now integrate! Now that we have our APIs ready, let's move on by installing **GraphQL packages.** 

7. **Install GraphQL packages**
    
    To pull our data from the backend, we will need two packages - Apollo Client and GraphQL 
    
    ```jsx
    yarn add @apollo/client graphql
    ```

8. **Configure GraphQL to communicate with backend** 
    
    Configure the Apollo Client in the project directory, inside `index.js` configure your apollo client so it would communicate with the backend.
    
    > Note to replace the `uri` with the one you'll get from Canonic.
    > 
    
    ```jsx
    import React from "react";
    import ReactDOM from "react-dom";
    import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
    
    import Container from "./components/Container";
    import "./index.css";
    
    const client = new ApolloClient({
      uri: "https://github-pr-dashboard-d3d.can.canonic.dev/graphql",
      cache: new InMemoryCache(),
    });
    ReactDOM.render(
      <ApolloProvider client={client}>
        <Container></Container>
      </ApolloProvider>,
      document.getElementById("root")
    );
    ```
    
    src/index.js
    
9.  **Query the data**
    
    Let's define our queries inside `src/gql` directory in a file called `query.js`. 
    
    ```jsx
    import { gql } from "@apollo/client";
    
    export const GET_CONTAINERS = gql`
      query {
        containers {
          #Container is on the the table on the Canonic backend
          title #Title contains the title of the container
          identifier #Using the identifier to associate the PR with the container.
        }
      }
    `;
    
    export const GET_PR = gql`
      query {
        pullRequests {
          #Pull Requests is another table on the Canonic backend
          github {
            #Github is the name of our Github integration.
            title #title will contain PR's title
            state #state will contain PR's state (etiher 'open' or 'closed')
            body #body contains the description of the PR
            draft #draft contains a boolean value indicating if the PR is a draft or not
            html_url #html_url contains the link to PR on github.com
            user {
              #user will hold user's data, here we are only fetching their profile picture link and their username
              avatar_url #avatar_url has the profile picture link of the user
              login #login has user's username
            }
            assignee {
              #assignee contains either null means PR has no assignee or return a user's data, here as well we are fetching their profile picture link and their username.
              avatar_url #avatar_url has the assignee's profile picture link of the user
              login #login has assignee's username
            }
            labels {
              #labels contains the array of labels. Here we will be just needing name and color
              name #name has title of the label
              color #color contains the hexadecimal value of color without a '#'
            }
          }
        }
      }
    `;
    ```
    
    src/gql/query.js
    
10. **Fetching data from the API**
    
    Inside our `Container` component, we execute the query.
    
    ```jsx
    import { React, useEffect, useState, useCallback } from "react";
    import { useQuery } from "@apollo/client";
    
    ...
    
    //Local dependencies
    import { GET_CONTAINERS } from "../../gql/query";
    ...
    
    const Container = () => {
      const { data: containerData } = useQuery(GET_CONTAINERS); // Fetching all 4 of our containers from graphQL
    
      const [containers, setContainers] = useState(); //Using this state to populate the containers.
    
      useEffect(() => {
        if (containerData) {
          setContainers(containerData.containers); //Setting up the container state
        }
      }, [containerData]); //Re-rendering state whenever  Container data changes
    
      // We are using this util function to assign a background color based on the column's identifier.
      const setColor = useCallback((identifier) => {
        let color = "";
        switch (identifier) {
          case "merge":
            color = "#6fbf73";
            break;
          case "pending":
            color = "#4dabf5";
            break;
          case "assigned":
            color = "#ffcd38";
            break;
          case "review":
            color = "#ffa733";
            break;
          default:
            color = "";
        }
        return color;
      }, []);
    
      return (
        <>
          {/* The Box component by the MaterialUI is responsible for display the header */}
          <Box sx={{ flexGrow: 1 }}>
            ...
          </Box>
          <div className="container">
            {/* Mapping over the container data to display each container and display its corresponding PRs */}
            {containers &&
              containers.map((containerName, i) => (
                <Card className="card" key={i}>
                  {/* The Stack is a Material UI component, it will display a content inside a square box */}
                  <Stack
                    direction="row"
                    alignItems="center"
                    justifyContent="space-between"
                    sx={{
                      px: 2,
                      py: 1,
                      mb: 2,
                      bgcolor: setColor(containerName.identifier), //Here we are calling the setColor function to dynamically assigning a background color based on name/identifier of the container.
                      color: "#FFFFFF",
                      borderRadius: "4px",
                    }}
                  >
                    <Typography
                      gutterBottom
                      variant="h5"
                      sx={{
                        width: "100%",
                        paddingTop: "0.5rem",
                        textAlign: "center",
                      }}
                    >
                      {containerName.title}
                    </Typography>
                  </Stack>
    
                  <CardContent className="cardContent">
                    {/* There are in total 4 identifier created on the backend (merge,review,assigned,draft) based on that associating the PRs */}
                  </CardContent>
                </Card>
              ))}
          </div>
        </>
      );
    };
    
    export default Container;
    ```
    
    src/components/Container/Container.js
    
    ![Step 10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6vufdy96gs46x4980lu2.png)
 
    
    
11. **Passing data as props**
    
    We will be passing the data for the PRs from `Container` component.
    
    ```jsx
    ...
    
    //Local dependencies
    import List from "../List";
    import { GET_CONTAINERS, GET_PR } from "../../gql/query";
    ...
    
    const Container = () => {
      ...
      const { loading, data: prData } = useQuery(GET_PR);
      ...
      ...
    
      const pullRequests = useMemo(
        () => prData?.pullRequests?.map((item) => item.github)?.[0] || [],
        [prData?.pullRequests] //Mapping over the data and assigning it to 'pullRequests' variable
      );
      const opened = useMemo(
        () => pullRequests.filter((item) => item.state === "open"),
        [pullRequests] // Filtering and populating all the PRs with 'open' status
      );
      const closed = useMemo(
        () => pullRequests.filter((item) => item.state === "closed").slice(0, 15),
        [pullRequests] // Filtering and populating all the PRs with 'closed' status
      );
      const draft = useMemo(
        () => pullRequests.filter((item) => item.draft === true),
        [pullRequests] // Filtering and populating all the draft PRs
      );
      const assigned = useMemo(
        () =>
          pullRequests.filter(
            (item) => item.assignee !== null && item.assignee.login.length > 1
          ),
        [pullRequests] // Filtering and populating all the PRs who has assignee.
      );
    
      ...
      return (
        <>
          <Box sx={{ flexGrow: 1 }}>
            <AppBar position="static">
              <Toolbar variant="dense">
                <Typography
                  variant="h6"
                  color="inherit"
                  component="div"
                  textAlign="center"
                  width="100%"
                >
                  Github PR Dashboard
                </Typography>
              </Toolbar>
            </AppBar>
          </Box>
          <div className="container">
            {loading && (
              <Typography
                gutterBottom
                variant="h5"
                component="div"
                marginBottom="5rem"
              >
                Loading......
              </Typography>
            )}
            {!loading &&
              containers &&
              containers.map((containerName, i) => (
                <Card className="card" key={i}>
                    ...
                  </Stack>
    
                  <CardContent className="cardContent">
                    {/* There are in total 4 identifier created on the backend (merge,review,assigned,draft) based on that associating the PRs */}
                    {containerName.identifier === "merge" && closed && (
                      <List
                        data={closed}
                        key={i}
                        identifier={containerName.identifier}
                      />
                    )}
                    {containerName.identifier === "review" && draft && (
                      <List
                        data={draft}
                        key={i}
                        identifier={containerName.identifier}
                      />
                    )}
                    {containerName.identifier === "assigned" && assigned && (
                      <List
                        data={assigned}
                        key={i}
                        identifier={containerName.identifier}
                      />
                    )}
                    {containerName.identifier === "pending" && opened && (
                      <List
                        data={opened}
                        key={i}
                        identifier={containerName.identifier}
                      />
                    )}
                  </CardContent>
                </Card>
              ))}
          </div>
        </>
      );
    };
    
    export default Container;
    ```
    
    src/components/Container/Container.js
    
    Now we have received the `data` as a prop, we will map over the `data` in order to fetch username,profile picture, the labels associated with the PR. Modify `List` component for that.
   
    ```jsx
    import React, { useState, useCallback } from "react";
    ...
    ...
    
    const List = ({ data, identifier }) => {
      const [isOpen, setIsOpen] = useState(); 
      const [modalData, setModalData] = useState();
      const [containersName, setContainersName] = useState(); 
      const handleClick = useCallback((pr, identifier) => {
        setIsOpen(true);
        setModalData(pr); 
        setContainersName(identifier);
      }, []);
    
      return (
        <>
          {data.map((item, i) => (
            <Paper
              elevation={3}
              sx={{ padding: 2, margin: 2, cursor: "pointer" }}
              key={i}
              onClick={() => handleClick(item, identifier)}
            >
              ...
            </Paper>
          ))}
          <Modals
            open={isOpen}
            setOpen={setIsOpen}
            pr={modalData}
            containerName={containersName}
          />
        </>
   ...
    ```
    
    We are receiving the PR body text in markdown format, we will use `ReactMarkdown` along with `remarkGfm` which helps to render Github flavoured markdown.
    
    ```jsx
    yarn add react-markdown remark-gfm
    ```
    
    Let's edit `Modal` component to account for props data.
    
    ```jsx
    ...
    import { ..., Fab } from "@mui/material";
    import ReactMarkdown from "react-markdown";
    import remarkGfm from "remark-gfm";
    
    ...
    
    export default function Modals({ pr, containerName, open, setOpen }) {
      ...
      return (
        <div>
          <Modal
            ...
          >
            {pr && (
              <Fade in={open}>
                <Box className="box">
                  <Fab variant="extended" size="small" sx={{ mb: 2 }}>
                    {containerName}
                  </Fab>
                  <div className="avatars">
                    {/* Displays user's profile picture */}
                    <Avatar
                      alt={pr.user.login}
                      src={pr.user.avatar_url}
                      sx={{ width: 24, height: 24 }}
                    />
                    <Typography variant="body2" style={{ marginLeft: 10 }}>
                      {/* Displays user's username */}
                      {pr.user.login}
                    </Typography>
                  </div>
                  <Typography
                    id="transition-modal-title"
                    variant="h4"
                    component="h2"
                  >
                    {/* Displays the title of the PR */}
                    {pr.title}
                  </Typography>
                  <Typography
                    id="transition-modal-description"
                    sx={{ mt: 2, mb: 2 }}
                  >
                    {/* Displays the PR's body content. Using react-markdown to display markdown */}
    
                    <ReactMarkdown children={pr.body} remarkPlugins={[remarkGfm]} />
                  </Typography>
                  {/* This will be displayed only if the PR has an assignee assigned */}
                  {pr.assignee && pr.assignee.login.length > 1 && (
                    <>
                      <Typography variant="h6">Assigned to</Typography>
                      <div style={{ display: "flex", alignItems: "center" }}>
                        {/* Displays assignee's profile picture */}
                        <Avatar
                          alt={pr.assignee.login}
                          src={pr.assignee.avatar_url}
                          sx={{ width: 24, height: 24 }}
                        />
                        <Typography variant="body2" style={{ marginLeft: 10 }}>
                          {/* Displays assignee's username */}
                          {pr.assignee.login}
                        </Typography>
                      </div>
                    </>
                  )}
                  <Typography id="transition-modal-description" sx={{ mt: 2 }}>
                    {
                      // Displays the hyperlink of PR
    
                      <Button
                        variant="contained"
                        href={pr.html_url}
                        target="_blank"
                        rel="noreferrer"
                        sx={{ mt: 2 }}
                      >
                        Open PR
                      </Button>
                    }
                  </Typography>
                </Box>
              </Fade>
            )}
    ...
    ```
    
    src/components/Modal/Modal.js
    
    ![Step 11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5sk2zuksyoxwuy9i7h3n.png)

### Voila! we are done.

[**Live Demo**](https://canonic-github-dashboard.netlify.app/?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_2&utm_term=voila_live_demo&utm_content=day_10)
[**Sample Code on Github**](https://github.com/canonic-dev/canonic-samples#github-pr-dashboard). 

## Conclusion:

This dashboard helps you gain visibility into PRs for a particular repository.

If you want, you can also duplicate this project from Canonic's sample app and easily get started by customizing it as per your experience. Check it out [here](https://app.canonic.dev/projects/61b0eedde08af1002f135b04/graph?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_2&utm_term=conclusion_here&utm_content=day_10).

You can also check out our other guides [here](https://dev.to/canonic).

Join us on discord to discuss or share with our community. Write to us for any support requests at [support@canonic.dev](mailto:support@canonic.dev). Check out our [website](https://www.canonic.dev/30-days-of-canonic/?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_2&utm_term=conclusion_website&utm_content=day_10) to know more about Canonic.