Working with Next.js + MUI (edit)

pandanoxes

Kevan Y

Posted on April 23, 2022

Working with Next.js + MUI (edit)

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

[
  "@algolia/client-common",
  "@babel/helpers",
  "react",
]
Enter fullscreen mode Exit fullscreen mode

/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

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

/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

[
  {
    "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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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

Image description

I started to look at what MUI component I could use for this.

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.

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;
Enter fullscreen mode Exit fullscreen mode

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

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

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

3 - Set our dependencyDiscoveryUrl env.
In docker-compose docker\docker-compose.yml, we need forward DEPENDENCY_DISCOVERY_URL in the build args.

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
Enter fullscreen mode Exit fullscreen mode

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

ARG DEPENDENCY_DISCOVERY_URL
ENV NEXT_PUBLIC_DEPENDENCY_DISCOVERY_URL ${DEPENDENCY_DISCOVERY_URL}
Enter fullscreen mode Exit fullscreen mode

Now we need to forward that env to be accessible in Next.Js. We will need to modify src\web\app\next.config.js

const envVarsToForward = [
 ...,
 'DEPENDENCY_DISCOVERY_URL',
]
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

6 - Create Row component in src\web\app\src\components\DependenciesTable\Row.tsx (Read more). 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:

  • 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:

  • Reset item number to 1 when we search for something

Feedback from @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/

💖 💪 🙅 🚩
pandanoxes
Kevan Y

Posted on April 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related