Environment | Branch | Status | Maintainability | Test Coverage |
---|---|---|---|---|
production | production | |||
staging | master |
This repo uses nvm for node version management. Configure your node version:
nvm use
Next, install the dependencies:
npm install
To run a local development server:
npm start
To create a new deployment bundle, run npm run build
To run unit tests, run: npm run test
. This should generate code coverage files and an lcov.info
file that is compatible with most code-coverage highlighting plugins.
The CIDC leverages Google Cloud Storage's static site-hosting capabilities for serving the Portal UI. It's recommended that you rely on the GitHub Actions workflow for deployment to staging and production.
However, here's how to deploy manually, should you need to. Set IS_PROD=true
if you are deploying to production, then run:
## 1. BUILD
# Create an optimized production build with environment-specific configuration
if $IS_PROD; then cat .env.prod > .env; else cat .env.staging > .env; fi
npm run build
## 2. DEPLOY
# Figure out the UI bucket to deploy to
if $IS_PROD; then export BUCKET='gs://cidc-ui-prod'; else export BUCKET='gs://cidc-ui-staging'; fi
# Copy the build to the GCS UI bucket
gsutil -m -h 'Cache-Control:no-cache,max-age=0' cp -r build/* $BUCKET
# Make all objects in the GCS UI bucket public
gsutil iam ch allUsers:objectViewer $BUCKET
This repo’s codebase was initialized using Create React App, and its top-level structure is pretty typical of apps created this way:
- public/ contains static assets used to build the CIDC portal site, the most important of which is probably index.html (the HTML page that the React app gets loaded into - this is what gets sent to the browser initially on page load, before React starts up and begins rendering additional content to the page). The public/static/cg subdirectory contains code adapted (copy-and-pasted, with the exception of these lines) from the clustergrammer repo that is used in our clustergrammer-in-react implementation.
- src/ contains the majority of the app’s code, including all React components. The index.tsx file contains the function call that attaches our React app to the static HTML code in the public/index.html file (to this div in particular). The App.tsx file contains the App component, which is the root node in the app’s React tree - that is, only components that are descendants of the App component will be included in the UI. Stepping through this App component is a good way to get a sense of how all the UI code is connected (I’ll do this in more detail below).
- test/ contains helper methods used in tests, kept separately from the rest of the code because they should only be called by tests. The tests themselves, however, appear in the same directory as the file they’re testing, usually in a file called, e.g., “MyComponent.test.tsx”.
- .env and .env.<prod|staging> contain environment variables used to configure the app. Environment variables are loaded from .env by default, so the appropriate staging/prod configuration must be copied into .env at build time. If you create a .env.local file, the app will load config from that file instead of .env, but the .env.local file won’t be tracked by git (since it’s gitignored).
- package.json is the npm analog to a python setup.py file. It contains a dependency list (with separate dev dependencies) and some scripts that you’ll find yourself running in the course of UI development.
- tsconfig.json configures the typescript compiler. This file is based pretty closely off of defaults that create-react-app gives you out of the box, and it’s pretty rare you would ever need to update it.
The src/ directory is where you’ll spend most of your time, so It's broken it down further here:
- @types/ contains typescript module stubs for packages the CIDC uses that don’t have typescript type definitions available (many packages implemented in javascript provide typescript types, but some don’t). These files contain a one-line module declaration that tells the typescript compiler to essentially do no type checking for the associated package.
- api/ contains generic client functions for interacting with the API. The key part of this code is the buildRequester factory function. This function takes as its argument an HTTP method (GET, POST, etc.) and returns another function called requester that makes requests using that HTTP method. This approach allows us to use the same requesting logic for our swr fetcher as we do for requests that don’t fetch data. The function handles logic related to building request headers, issuing requests to the API, handling etag conflicts, and retrying failed requests. It’s used to build the four helper functions that are used for issuing requests to the API throughout the UI code: apiFetch, apiCreate, apiUpdate, and apiDelete.
- components/ contains, unsurprisingly, most of the React component definitions for the UI. Generally, each subdirectory corresponds to a page in the UI - for example, profile/, home/, transfer-data/, etc.. There are some exceptions, though: generic/ contains “generic” components that are used throughout the UI, like the InfoTooltip component; visualizations/ contains data visualization components; identity/ contains a collection of components related to authentication and authorization.
- model/ contains typescript definitions generally corresponding to response types returned by the API.
- util/ contains miscellaneous helper functions.
- App.tsx contains the root component in the app’s React tree.
- index.css contains global styles applied to the app via this import.
- index.tsx renders the React app into the DOM.
- react-app-env.d.ts is autogenerated by create-react-app (I’m not sure why).
- rootStyles.tsx configures the global Material UI theme and creates some page layout default style classes.
- setupTests.ts runs before any tests are executed. It’s analogous to pytest’s conftest.py.
First, we lazily import code for the different pages in the UI. Using lazy imports tells React to only load javascript related to a certain component into the browser when that component is rendered. It’s not critical to understand this, since it’s just a minor performance optimization, but here are the relevant docs if you want to learn more.
Next, we define the root App component. This component ties together all the different parts of the UI. I’ll go through these different parts:
- Router is a react-router component that provides context related to navigation (e.g., what page the user is currently on, what page they were on previously, etc.) to all child components. It’s required that we wrap our app in this component for all routing related functionality to work correctly.
- SWRConfig provides global configuration to the swr library, which we rely on for smart data fetching throughout the UI code (for example, here’s how it’s used to load user account info).
- QueryParamProvider sets up the library we use for reading and updating URL query params, called use-query-params (here’s an example usage).
- CIDCThemeProvider provides the global Material UI theme defaults defined in rootStyles.tsx to all child components.
- ErrorGuard is a custom component that is used for displaying “fatal” errors, like a user's account being disabled.
- InfoProvider provides global info loaded from the API’s “/info” resource to all child components.
- IdentityProvider provides user authentication/authorization/account info to all child components.
- Header defines the UI page header (the tabs and everything above them).
- This mysterious looking code is required for lazy component imports to work.
- Switch is a react-router component that handles toggling between a set of different Route components. This block of code lays out all of the top-level pages that appear in the UI, so you’ll need to add code here if you’re adding a new page to the UI. See the Switch docs and Route docs for more info.
- To see one interesting example of how a route component works, let’s look at the FileDetailsPage route. The path prop contains a parameter, fileId, which is accessible in the render method of the FileDetailsPage component like so (it’s used to load data from the API for the relevant file). In addition to URL path params, react-router passes other useful props to route components.
Anything that appears in the UI is somehow a descendant of the App component. If you can’t find where the code corresponding to something in the UI lives (and React Developer Tools doesn’t do the trick), then starting at the App component and making your way down the component hierarchy should get you to your answer.
- Install nvm
- In cidc-ui directory
nvm use
npm install
- Install a browser extension that blocks CORS such as CORS Everywhere if using firefox or launch chrome without security if using chrome
- To run tests,
npm test
- To run a development server,
npm start
3. To point to prod, create top-level.env.local
4. Uncomment prod config, comment out staging config
We use jest and React Testing Library for testing the components that make up our UI. There are so many excellent resources online that help with learning them (between the docs, blog posts, talks).
- Create a new React component that will hold the contents for this page. By convention (not requirement), you should do this inside of a new subdirectory inside the src/components/ directory, in a file and component named like “<SomeName>Page”. Generally, the page’s body should be styled using the centeredPage style class defined in rootStyles.tsx (accessible via the useRootStyles hook). Add a test file and some initial tests, too - even just making sure the component renders without runtime errors!
- Add a route component corresponding to this new page to the Switch component in App.tsx. If the path string for this page matches a substring of any other route paths (e.g., “/path” and “/path-1”), make sure that the substring appears before the longer path in the Switch component’s children - otherwise, you won’t be able to navigate to the substring path.
- Add some interaction that makes it possible for the user to get to the new page. This could be anything from adding a new tab in the Header component, a button that sends the user to the page when it’s clicked, or a redirect based on an API response.
- (Maybe) add a smoke test to App.test.tsx to make sure the new page is reachable.
Whereas the API’s authentication flow is centered around validating clients’ identity tokens, the UI’s auth flow is centered around obtaining valid identity tokens from Auth0 to use when authenticating with the API. Once a valid id token is obtained, the UI can then perform basic authorization checks after loading a user’s account information.
The authentication flow is handled by the IdentityProvider component (which wraps children of the App component here). The IdentityProvider’s implementation contains two other context providers which handle key logic related to authentication and user identity/authorization:
- AuthProvider handles the authentication flow with the Auth0 API. We use the Auth0 web client for interacting with the Auth0 API. Here’s how the AuthProvider component works. When the component is rendered:
- It starts the user in a “loading” state, since we’re not sure if the user is logged in or not yet.
- After initial render in the “loading” state, performs logic based on the current URL path to determine what to do. If the path is “/logout”, then log the user out. If the path is “/callback”, then the user was just redirected back to the portal after logging into their google account, meaning they are now authenticated, so we send them along to their target path (i.e., the path that they initially tried to visit before being sent to google to log in) or to the home page. Otherwise, if the user isn’t already logged in, we try to log them in using the checkSession function.
- The Auth0 web client checkSession function (docs) checks whether there’s currently a valid id token available for the user (the web client caches the user’s id token behind the scenes).
- If no token is available and the user is visiting the home page, don’t automatically log them in (set their state to “logged-out”). From the home page, the user can elect to log in by pressing the sign up / log in button.
- If no token is available and the user is trying to visit a page other than the home page (e.g., the file browser), automatically log the user in.
- If a valid id token is available, unpack user info from the token payload and store that data in the AuthProvider state. In this case, the user is “logged-in” from the perspective of Auth0. However, that doesn’t mean the user has a CIDC account - checks related to the user’s CIDC account are executed by the UserProvider.
- UserProvider handles logic related to checking whether an authenticated user has a CIDC account and if that account has been approved to use the CIDC. Here’s how it does that: 4. It loads data passed down to it from the AuthProvider context. 5. If the AuthProvider says the user is authenticated, it tries to load CIDC account info associated with the user's identity token. 6. If the user has an existing CIDC account, it: * Displays an error if the account is disabled. * Redirects to the home page if the user’s account is registered but not approved (i.e., a CIDC admin hasn’t granted them a role yet). 7. If the user hasn’t registered, it sends the user to the account registration page. 8. If there was some other error loading the user’s CIDC account info, display a generic error message. There’s probably room for improvement here by handling other specific error messages. 9. If the user is logged in and registered/approved (we won’t hit this line of code otherwise, since the user will have been redirected to another page), load their data access permissions from the API for use throughout the app. 10. It determines which tabs a user should see based on their role.
So, in this way, the IdentityProvider component checks if a user is logged in and loads their account information before allowing the rest of the app to render.
Since the trial/file browser is a critical part of the CIDC portal and since the way all its subcomponents string together is a bit complicated, we are stepping through it at a high level here.
BrowseDataPage is the container for both the trial and file browsers, and it handles layout and logic for switching between the two. By default, it displays the trial browser on initial render. It renders the browsers inside of a component called FilterProvider, which provides context about which facets the user has selected.
FilterProvider implements logic for loading available facets from the API and for updating which facets the user has selected. The facet update logic related to nested facets is kind of hard to understand at first glance, but the bulk of the complexity comes from handling the case where a parent facet (e.g. WES) is selected and we need to determine whether all of its subfacets should be selected or deselected.
Filters renders the filter facet checkboxes. It relies on data and functions for updating filters loaded from the FilterProvider's context and uses FilterCheckboxGroup for facet layout. The BrowseDataPage renders the Filters component alongside both the file and trial browsers.
Both the FileTable and TrialTable components change what data they load from the API based on the current state of the filter facets. FileTable does so here and TrialTable does so here.
Since clustergrammer doesn’t natively provide a component for use in React apps, we had to hack one ourselves by injecting the clustergrammer js code into an iframe. Here’s how the custom Clustergrammer component works:
- It loads the clustergrammer HTML as a string from the file stored here.
- It defines a function to be executed from within the clustergrammer iframe (this function can operate on the iframe’s window and document objects). This function passes the clustergrammer network data provided to the Clustergrammer component props to a function called DrawClustergram, which is defined in the clustergrammer HTML document and does the actual rendering of the clustergrammer visualization. This clustergrammer function will be re-run every time the window size changes.
- It renders the iframe with the clustergrammer HTML injected into the iframe document.
Note: this solution may become entirely moot if the frontend switches over to clustergrammer2. Since clustergrammer2 is packaged as an ipython widget instead of a plain HTML file, the solution may look more like finding a way to render an ipython widget initialized with data inside a React component.