Working with Next.js + MUI (edit)
Tags: opensource, nextjs, typescript
Published: 2022-04-23 03:21:36

## Intro
This week I worked on the front-end part of telescope. I didn't touch that much on the front-end in telescope, but I had experience with Next.js before. The [Issue](https://github.com/Seneca-CDOT/telescope/issues/3357) I will be working on is to implement a UI for the `dependency-discovery` services. 

## Planning / Implementation

They were no designs pre-planned for this issue. I had to make a simple, fast, and functional design.
Before getting into the design we have to understand the `dependency-discovery` API, we need to know all the possible route and what kind of data each route return to make use of all our API data as much as possible.


### `/projects`

This route returns an array of string of the list of the dependencies we use in telescope repo.

#### Samples of data
```json
[
  "@algolia/client-common",
  "@babel/helpers",
  "react",
]
```

### `/projects/:namespace/:name?`

This route returns the general information of the dependency. It's an object with `id` as string, `license` as string, and 'gitRepository' as object. `gitRepository` has a `type` as string, `url` as string, `directory` as string, and `issuesUrl ` as string.

#### Samples of data
```json
{
  "id": "@babel/core",
  "license": "MIT",
  "gitRepository": {
    "type": "git",
    "url": "https://github.com/babel/babel",
    "directory": "packages/babel-core",
    "issuesUrl": "https://github.com/babel/babel/issues?q=is%3Aopen+is%3Aissue+label%3A%22hacktoberfest%22%2C%22good+first+issue%22%2C%22help+wanted%22"
  }
}
```

### `/github/:namespace/:name?`

This route returns an array of issue label `hacktoberfest`, `Help wanted`, and `good first issue` of the dependency. Each object has a `htmlUrl` as string, `title` as string, `body` as string, and `createdAt` as string.

#### Samples of data
```json
[
  {
    "htmlUrl": "https://github.com/babel/babel/issues/7357",
    "title": "injecting external-helpers in a node app",
    "body": "<!---\r\nThanks for filing an issue 😄 ! Before you submit, please read the following:\r\n\r\nSearch open/closed issues before submitting since someone might have asked the same thing before!\r\n\r\nIf you have a support request or question please submit them to one of this resources:\r\n\r\n* Slack Community: https://slack.babeljs.io/\r\n* StackOverflow: http://stackoverflow.com/questions/tagged/babeljs using the tag `babeljs`\r\n* Also have a look at the readme for more information on how to get support:\r\n  https://github.com/babel/babel/blob/master/README.md\r\n\r\nIssues on GitHub are only related to problems of Babel itself and we cannot answer \r\nsupport questions here.\r\n-->\r\n\r\nChoose one: is this a bug report or feature request? (docs?) bug report\r\n\r\nI'm trying to use a package that assumes that the external-helpers are available as a global. From the [docs](https://babeljs.io/docs/plugins/external-helpers/#injecting-the-external-helpers), I should be able to inject them to `global` in my node app by using `require(\"babel-core\").buildExternalHelpers();`. However, use of that still results in the following error: `ReferenceError: babelHelpers is not defined`\r\n\r\n### Babel/Babylon Configuration (.babelrc, package.json, cli command)\r\nSince the `buildExternalHelpers()` function needs to run before the package is imported and my app uses es module imports, I'm using a bootstrap file as an entry point that is ignored from transpilation and just tries to inject the helpers before loading the actual app:\r\n\r\n```js\r\nrequire(\"babel-core\").buildExternalHelpers();\r\nconst app = require('./app');\r\n```\r\n\r\n### Expected Behavior\r\n\r\n`babelHelpers` should be added to `global` so that it is available for the package that assumes it is available there.\r\n\r\nfrom the docs:\r\n> This injects the external helpers into `global`.\r\n\r\n### Current Behavior\r\n\r\n`babelHelpers` is not made available on `global`, resulting in `ReferenceError: babelHelpers is not defined`\r\n\r\n### Possible Solution\r\n\r\nThe docs also [mention](https://babeljs.io/docs/plugins/external-helpers/#getting-the-external-helpers) generating a helpers file with `./node_modules/.bin/babel-external-helpers [options] > helpers.js`. It wasn't obvious to me that this file could be imported to accomplish the same goal as `buildExternalHelpers()` until I started reading the source of that file. Importing that file instead does work for my app. I'll need this file elsewhere, so I'll likely just continue importing that instead, even if there is a way to use `buildExternalHelpers()`.\r\n\r\nWith that approach, my bootstrap file has the following contents instead:\r\n\r\n```js\r\nrequire('../../vendor/babel-helpers');\r\nconst app = require('./app');\r\n```\r\n\r\n### Your Environment\r\n<!--- Include as many relevant details about the environment you experienced the bug in -->\r\n\r\n| software         | version(s)\r\n| ---------------- | -------\r\n| Babel            | 6.26.0\r\n| node             | 8.9.4\r\n| npm              | 5.6.0\r\n| Operating System | macOS High Sierra \r\n\r\n### Forum\r\n\r\nWhile I was still trying to find a working solution, I was trying to find the best place to ask questions. The website still links to a discourse forum that no longer seems to exist. It'd be a good idea to either remove the link from the site or at least have it link to somewhere that describes the currently recommended approach for getting that type of help.\r\n",
    "createdAt": "2018-02-08T20:49:23Z"
  }
]
```

Now we know what the API return, let's makes a draft design. For simplicity, I'm gonna draw it by hand.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttsx9uk7fzlh02q6ineq.png)

I started to look at what MUI component I could use for this.
- Icons - [link](https://mui.com/material-ui/material-icons/)
- Table - [link](https://mui.com/material-ui/react-table/)
- Collapse - [link](https://mui.com/material-ui/api/collapse/)

After that, I need to plan how I will structure my code and what to add/modify.

1 - Create a route at `/dependencies` in `src\web\app\src\pages` in Next.js the name of the file is our route name. 
The page should follow the other page in telescope which means it has an SEO, and Navbar component. Also, we need to add our DependenciesPage component which is our dependencies page.
I was thinking to use `getStaticProps` + revalidate features from Next.Js to make a static page. But since our API services need to be run at the time when Next.Js builds all static HTML, so we might need to modify our docker-compose to run our `dependency-discovery` services first then after our services it's up we can build our static HTML. For simplicity, I decided to just use `useEffect` to fetch the data.

```tsx
import SEO from '../components/SEO';
import NavBar from '../components/NavBar';
import DependenciesPage from '../components/DependenciesPage';

const dependencies = () => {
  return (
    <div>
      <SEO pageTitle="Dependencies | Telescope" />
      <NavBar />
      <DependenciesPage />
    </div>
  );
};

export default dependencies;
```

2 - Add a new icon to redirect into our new route in the navbar `src\web\app\src\components\NavBar\index.tsx`

```tsx
import { FiPackage } from 'react-icons/fi';

const iconProps: NavBarIconProps[] = [
 {
  ...
 },
 {
    href: '/dependencies',
    title: 'Dependencies',
    ariaLabel: 'Dependencies',
    Icon: FiPackage,
  },
]
```

3 - Set our `dependencyDiscoveryUrl` env.
In docker-compose `docker\docker-compose.yml`, we need forward `DEPENDENCY_DISCOVERY_URL` in the build args.
```yml
services:
  nginx:
    build:
      context: ../src/web
      dockerfile: Dockerfile
      cache_from:
        - docker.cdot.systems/nginx:buildcache
      # next.js needs build-time access to a number of API URL values, forward as ARGs
      args:
        ...
        - DEPENDENCY_DISCOVERY_URL
```

We also need to modify the `Dockerfile` in `src\web\Dockerfile` to add `DEPENDENCY_DISCOVERY_URL` as build args.

```docker
ARG DEPENDENCY_DISCOVERY_URL
ENV NEXT_PUBLIC_DEPENDENCY_DISCOVERY_URL ${DEPENDENCY_DISCOVERY_URL}
```

Now we need to forward that env to be accessible in Next.Js. We will need to modify `src\web\app\next.config.js`
```js
const envVarsToForward = [
 ...,
 'DEPENDENCY_DISCOVERY_URL',
]
```

4 - Create our `DependenciesPage` components in `src\web\app\src\components` (Because it contains a lot of lines of code I'm just putting some parts. [Read more](https://github.com/Seneca-CDOT/telescope/blob/master/src/web/app/src/components/DependenciesPage.tsx))
Our `DependenciesPage` components should have a `useEffect` that runs once on `onMount` to fetch our dependencies at route `/projects`. As JSX, it will have our Page title, and a `DependenciesTable` component which is our table of dependencies, it has a dependencies(List of the dependencies) props.
We also need some style to make our page responsive and adjust color in light/dark mode.

```jsx
import { dependencyDiscoveryUrl } from '../config';

import { makeStyles } from '@material-ui/core/styles';
import { useState } from 'react';

const useStyles = makeStyles((theme) => ({
  root: {
    backgroundColor: theme.palette.background.default,
    fontFamily: 'Spartan',
    padding: '1em 0 2em 0',
    paddingTop: 'env(safe-area-inset-top)',
    wordWrap: 'break-word',
    [theme.breakpoints.down(1024)]: {
      maxWidth: 'none',
    },
    '& h1': {
      color: theme.palette.text.secondary,
      fontSize: 24,
      transition: 'color 1s',
      marginTop: 0,
    },
    '& p, blockquote': {
      color: theme.palette.text.primary,
      fontSize: 16,
      margin: 0,
    },
  },
  container: {
    padding: '2vh 18vw',
    [theme.breakpoints.down(1024)]: {
      padding: '2vh 8vw',
      wordWrap: 'break-word',
    },
  },
}));
const DependenciesPage = () => {
   const [dependencies, setDependencies] = useState<string[]>();
   const classes = useStyles();
   
   useEffect(() => {
    (async () => {
      try {
        const fetchDependenciesData = await fetch(`${dependencyDiscoveryUrl}/projects`);
        setDependencies(await fetchDependenciesData.json());
      } catch (e) {
        console.error('Error Fetching Dependencies', { e });
      }
    })();
  });

  return (
    <div className={classes.root}>
      <div className={classes.container}>
        <h1>Dependencies</h1>
        <DependenciesTable dependencies={dependencies} />
      </div>
    </div>
  );
};

export default DependenciesPage;
```

5 - Create `DependenciesTable` component in `src\web\app\src\components\DependenciesTable\index.tsx` (Because it contains a lot of lines of code I'm just putting some parts. [Read more](https://github.com/Seneca-CDOT/telescope/blob/master/src/web/app/src/components/DependenciesTable/index.tsx)). This would be the component that contains our table, search bar, and table navigation. We can get our dependency list from the props dependencies. Create a function to update the dependency list based on the search query.
Set the limit of the rows per page to 15.
We also need to add some style to match our drawing design and adjust color for light/dark mode.

```tsx
type DependenciesTableProps = {
  dependencies: string[];
};

const DependenciesTable = ({ dependencies }: DependenciesTableProps) => {
  const classes = useStyles();
  const [page, setPage] = useState(0);
  const rowsPerPage = 15; // Set 15 element per page
  const [searchField, setSearchField] = useState('');

  // Compute dependencyList based on search query
  const dependencyList = useMemo(() => {
    setPage(0);
    if (!searchField) return dependencies;
    return dependencies.filter((dependency: string) => {
      return dependency.toLowerCase().includes(searchField.toLowerCase());
    });
  }, [dependencies, searchField]);

  return (
    <>
      <SearchInput text={searchField} setText={setSearchField} labelFor="Browse for a dependency" />

      <TableContainer>
        <Table sx={{ minWidth: 450 }} aria-label="custom pagination table">
          <TableBody>
            {dependencyList
              .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
              .map((dependency) => {
                return <Row key={dependency} dependency={dependency} />;
              })}
          </TableBody>
        </Table>
        <TablePagination
          className={classes.root}
          rowsPerPageOptions={[]}
          component="div"
          count={dependencyList.length}
          rowsPerPage={rowsPerPage}
          page={page}
          onPageChange={handleChangePage}
        />
      </TableContainer>
    </>
  );
};

export default DependenciesTable;
```

6 - Create `Row` component in `src\web\app\src\components\DependenciesTable\Row.tsx` ([Read more](https://github.com/Seneca-CDOT/telescope/blob/master/src/web/app/src/components/DependenciesTable/Row.tsx)). This content each of our row with the collapse. We have some `useEffect` waiting on a state called `open` (state for the collapsed component) to be changed to trigger the fetch for dependency information. For fetch GitHub issues, add more checks to see if API returns 403 which means API limit reached, if 403 we need to show a message saying `Github API reached a limit, please use the link directly <Dependency name>`.
We also need to add some style to match our drawing design and adjust color for light/dark mode.

7 - Testing part : 
   - Making sure our design is responsive,
   - Show the right color in light/dark mode. 
   - Working search bar.
   - Working pagination.
   - Working collapse and data fetch on collapse open.

## Pull request reviews 

Feedback from [@humphd](https://github.com/humphd):

- Use SWR instead of using `useEffect` for fetching
- Add a paragraph to explain what is it after the title.
- Add a spinner when content is loading.
- Fix color, and font size issues.

Feedback from [@joelazwar](https://github.com/joelazwar):

- Reset item number to 1 when we search for something

Feedback from [@DukeManh](https://github.com/DukeManh):

- Use the Default SWR fetcher instead of creating one.
- Rename some functions.

## Final products
Once the code is merged the staging environment will ship this feature first. 
On release 3.0.0, it will be shipped to production environment
Staging: https://dev.telescope.cdot.systems/dependencies/
Production: https://telescope.cdot.systems/dependencies/