Working with Next.js + MUI (edit)
Kevan Y
Posted on April 23, 2022
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",
]
/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"
}
}
/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"
}
]
Now we know what the API return, let's makes a draft design. For simplicity, I'm gonna draw it by hand.
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;
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,
},
]
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
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}
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',
]
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;
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;
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/
Posted on April 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.