Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@ahandsel
Last active January 27, 2024 13:01
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ahandsel/813e642bf36008192708c50a23185935 to your computer and use it in GitHub Desktop.
Save ahandsel/813e642bf36008192708c50a23185935 to your computer and use it in GitHub Desktop.

React & REST API Workshop Part 2

Let's POST Data to a Web Database From a React Component

Thank you for attending our 2nd Kintone x React workshop!
Use the following files to follow along!

Download Links

Click here or the Download Zip button on the upper right corner for all the code & slides you need for our workshop!


Outline


📎 PREREQUISITE

💾 Install Node & npm

  • Node ≥ 10.16 and npm ≥ 5.6 are required for this workshop
  • Confused? 🤔 → Check out the Video Guides

🕹️ Install a Sample React App

🚀 Getting your FREE Kintone Database

① Sign-Up for Developer Program Account (Website) 🌐

  • bit.ly/KDP_signup
    • ⚠ Do NOT use Safari
    • ⚡Accept Cookies First
    • ✅ Use Chrome & Firefox

② THEN Create a Kintone Subdomain (Database) 📂

  • bit.ly/K_DevLic
    • ⚡ Only use lowercase, numbers, & hyphens in your subdomain
    • ⚠ Do not use uppercase nor special characters

Confused? 🤔 → Check out the Video Guides


📺 Quick Videos Going Over the Prep Work

Install Node & Sample React App Signing Up for Kintone
https://youtu.be/4Kw-i_rX3tY https://youtu.be/4Kw-i_rX3tY https://youtu.be/Gzz8SbTuoFg https://youtu.be/Gzz8SbTuoFg

📚 Suggested Reading

We advise you to have a look through the following React documents beforehand:

  1. Hello World
  2. Introducing JSX
  3. Rendering Elements
  4. Components and Props

Details at dev.to

React & REST API Series' Articles

Check out Will's React & REST API Series' Articles at dev.to Community 👩‍💻👨‍💻!


Overview of the Workshop

Parts Type Description
Part A Demo Quick demo using promises in React
Part B Live Coding Let's use useState & useEffect Hooks instead
Part C Setup Guide Create a Kintone account & a database app
Part D Demo Quick demo that results in CORS error
Part E Setup Guide Let's install the backend Express server
Part F Live Coding Getting data from the Kintone App
Part G Live Coding Posting data to the Kintone App

Prepping for React Workshop

This is a step-by-step guide that will go over everything you need to do before our workshop in detail! Let's get started!

Outline:


1. Create a myproject folder

Somewhere inside your Document folder will be good.

Check if you already have Node.js or npm

React requires Node ≥ 10.16 & npm ≥ 5.6
Go inside the myproject folder.

$ node -v
$ npm -v

2. Install Node.js

If Node & npm are missing, let's install them!

Options:

macOS with nodenv

We recommend installing Node.js using nodenv to manage node versions. This allows your computer to have a specific Node.js version per project.

⚠️ Remove any existing installations of Node.js before installing nodenv! ⚠️
Having different Node.js installations can lead to conflict issues.

Step 1: Install nodenv with Homebrew

  • Update Homebrew:

    brew update && brew upgrade
  • Install nodenv:

    brew install nodenv

Step 2: Set up nodenv shell integration

  • Run the initialization command:

    nodenv init
  • Do as instructed by appending the following line into your shell's rc/profile file:

    eval "$(nodenv init -)"
    • For Zsh users:

      $ echo 'eval "$(nodenv init -)"' >> ~/.zshrc
      $ cat < ~/.zshrc
    • For Bash users:

      $ echo 'eval "$(nodenv init -)"' >> ~/.bash_profile
      $ cat < ~/.bash_profile

Step 3: Implement the changes

Close & open a new Terminal window for the changes to take place.

Optional: Verify that nodenv is properly set up using nodenv-doctor script.

  • For those using Z shell (Zsh) shell:

    curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash
  • Expected result:

    Checking for `nodenv' in PATH: /usr/local/bin/nodenv
    Checking for nodenv shims in PATH: OK
    Checking `nodenv install' support: /usr/local/bin/nodenv-install (node-build 3.0.22-4-g49c4cb9)
    Counting installed Node versions: none
      There aren't any Node versions installed under `~/.nodenv/versions'.
      You can install Node versions like so: nodenv install 2.2.4
    Auditing installed plugins: OK

Step 4: Install Node.js inside the React Workshop folder (myproject)

  • Now you're ready to install specific Node.js versions!

  • Inside myproject folder, install Node.js version 14.5.0:

    $ cd myproject/
    $ nodenv install 14.5.0
    $ nodenv local 14.5.0

Alright! Your Mac is now armed with Node.js!
Skip down to the 3. Install a Sample React App section!


Windows with nvm-windows

The following steps are straight from the Microsoft Docs on Set up NodeJS on native Windows. We recommend installing and managing Node.js with nvm-windows

⚠️ Remove any existing installations of Node.js before installing nvm-windows! ⚠️
Having different Node.js installations can lead to conflict issues.

Step 1: Go to the windows-nvm's latest release.

Step 2: Download the nvm-setup.zip file for the most recent release.

Step 3: Once downloaded, open the zip file, then open the nvm-setup.exe file.

Step 4: The Setup-NVM-for-Windows installation wizard will walk you through the setup steps, including choosing the directory where both nvm-windows and Node.js will be installed.

  • install-nvm-for-windows-wizard.png

Step 5: After the installation is complete, open PowerShell & enter nvm ls

  • nvm ls lists out installed Node versions (should be none at this point)
  • windows-nvm-powershell-no-node.png

Step 6: Install Node.js inside the React Workshop folder (myproject)

  • Now you're ready to install specific Node.js versions!

  • Inside myproject folder, install Node.js version 14.5.0:

    $ cd .\Documents\myproject
    $ nvm install 14.5.0
    $ nvm use 14.5.0

Alright! Your Windows is now armed with Node.js!
Skip down to the 3. Install a Sample React App section!


3. Install a Sample React App

Still inside the myproject folder, let's install a React App named frontend.

Install:

  • npx create-react-app frontend

Starting it up:

  • Go inside the frontend folder
  • npm start

⚠️ Got an error?

  • Make sure you are inside the myproject folder when setting up Node.js
    • Mac: nodenv local 14.5.0
    • Windows: nvm use 14.5.0
  • Make sure you are inside the frontend folder when running npm start!

YouTube Quick Videos Going Over the Node Install & Create-React-App

Installing Node.js & Create a New React App YouTube Thumbnail

Part A: Using Promises in React

When getting data from an API call, we usually need to use promises, right?
So let's try that in our frontend React App.


Sample Code for index.js

Here is an example that tries to output an API response in the frontend React App.
It calls Random User Generator API, waits for a response, and then creates a React element using the JSON data.
Let's see what happens!

File Location: .../myproject/frontend/src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

// Call Random User Generator API
const restEndpoint = 'https://randomuser.me/api/';

// Wait for response & tries to output it to React
const callRestApi = async () => {
  const response = await fetch(restEndpoint);
  const jsonResponse = await response.json();
  console.log(jsonResponse);

  // React.createElement( type, [properties], [...children]);
  return React.createElement('h1', null, JSON.stringify(jsonResponse));
};

ReactDOM.render(
  callRestApi(),
  document.querySelector('#root')
);

Result - Error

The callRestApi() async function returns a promise object immediately as it waits for the Random User Generator API's response.
React doesn't "wait" for the REST API result, so it tries to create and render the element with the promise object immediately and failing. It attempts to display the Random User Generator API response into a React element.
However, the following error will display instead:

Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

Promise_Error.png - Kintone_React_Workshop - v2.1

Part B: Using Hooks in React!

We will use useState & useEffect Hooks to handle the REST API calls in React.


What is useState & useEffect Hooks

useState hooks allow us to use special "state" variables that we can utilize to render into the React UI.

useEffect hooks allow us to run functions after the rendering has finished. We'll use this to run a REST API call and update the state variable, which will then cause React to re-render the UI.

Sample Code for index.js

File Location: .../myproject/frontend/src/index.js

// Get started by importing the React JavaScript library & Hooks
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

// Call Random User Generator API
const restEndpoint = "https://randomuser.me/api/";

// Wait for response & return the API response
const callRestApi = async () => {
  const response = await fetch(restEndpoint);
  const jsonResponse = await response.json();

  console.log(jsonResponse);

  return JSON.stringify(jsonResponse);
};

const RenderResult = () => {
  // Establish useState by giving it our initial state
  // const [state, setState] = useState(initialState);
  const [apiResponse, setApiResponse] = useState("*** now loading ***");

  // useEffect takes 2 arguments:
  // 1st = a function, called effect, that is executed when the React Component is rendered
  // 2nd = Array of dependencies to control when effect is to be executed after mounting the component; Empty array = only invoke effect once

  useEffect(() => {
    callRestApi().then(
      result => setApiResponse(result));
  }, []);

  return (
    // JSX includes html-like syntax
    <div>
      <h1>React App</h1>
      <p>{apiResponse}</p>
    </div>
  );
};

// Where the magic happens!
ReactDOM.render(
  <RenderResult />,
  document.getElementById('root')
);

Result - Success

Expected output:
Outputs the Random User API Call to the frontend React App.

React App with Random User API - Will's Article

Kintone Database Solution

Built for teamwork, designed by you

🚀 Getting your FREE Kintone Database

① Sign-Up for Developer Program Account (Website) 🌐

  • bit.ly/KDP_signup
    • ⚠ Do NOT use Safari
    • ⚡Accept Cookies First
    • ✅ Use Chrome & Firefox

② THEN Create a Kintone Subdomain (Database) 📂

  • bit.ly/K_DevLic
    • ⚡ Only use lowercase, numbers, & hyphens in your subdomain
    • ⚠ Do not use uppercase nor special characters

Confused? 🤔 → Check out the video below:

📺 Sign up for Kintone Developer Program & Developer License

https://youtu.be/Gzz8SbTuoFg
https://youtu.be/Gzz8SbTuoFg

Create a Kintone App

Let's create a Kintone App to list off your favorite Mangas!

Here are the required fields & their configurations for our workshop:

Field Type Field Name Field Code Note
Text Title title The manga's title
Text Author author The manga's author
Record number Record number recordID Auto generated ID for each entry

Create_App_Demo.gif Kintone_React_Workshop v2.1

Part D: CORS Error Demo

Now that we are outputting Random User Generator API's response onto our frontend React App let's output more useful results!
Using Kintone's low-code Database platform, we can easily create & host a database that is accessible with REST APIs.

But can we just swap the request endpoint with Kintone's?


Sample Code for index.js

We will try a GET request to Kintone from our frontend React App.

If you want to run this code, be sure to input your Kintone specifications for subdomain, appID, and apiToken.

File Location: .../myproject/frontend/src/index.js

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

// Kintone API Setup
const subdomain = ""; //Enter your Kintone Subdomain (ex: devevents)
const appID = ""; //Enter your App's ID number (ex: 1)
const apiToken = ""; //Enter your App's API Token

const getKintoneData = async () => {
  const requestEndpoint = `https://${subdomain}.kintone.com/k/v1/records.json?app=${appID}`;
  const fetchOptions = {
    method: 'GET',
    headers: { 'X-Cybozu-API-Token': apiToken }
  };
  const response = await fetch(requestEndpoint, fetchOptions);
  const jsonResponse = await response.json();
  console.log(jsonResponse);
  return JSON.stringify(jsonResponse);
};

const RenderResult = () => {
  const [apiResponse, setApiResponse] = useState("*** now loading ***");

  useEffect(() => {
    getKintoneData().then(
      result => setApiResponse(result));
  }, []);

  return (
    <div>
      <h1>React App</h1>
      <p>{apiResponse}</p>
    </div>
  );
};

ReactDOM.render(
  <RenderResult />,
  document.getElementById('root')
);

Result - CORS Error

Cross-Origin Resource Sharing (CORS) Error occurs since we cannot access the resources that lie on Kintone's domain directly from our domain (frontend React App).

CORS_Error_Crop.png Kintone_React_Workshop v2.1

Installing the Express Server (backend)

Outline

What is Express?

Express is a backend web application framework for Node.

We will use Express to create custom endpoints that our frontend React App can make calls to. When we make requests to these custom endpoints, the Express server will make REST API calls to our desired 3rd party service endpoint, receive the response, and then route the response back to our frontend React App.

Installing the Express Server

(1) From your terminal, go to your myproject folder

  • $ cd .../myproject

(2) Create backend folder

  • $ mkdir backend
  • $ cd backend

(3) Create your Express project

  • $ npm init
  • Hit enter to skip the questions

(4) Continue to install some dependencies

  • $ npm install express node-fetch cors

(5) Create a server.js file inside the backend folder

  • $ touch server.js

Starting the Express Server

(1) Configure the server.js file

(2) From your terminal, go to your backend folder

  • $ cd .../myproject/backend

(3) Start the Express Server

  • $ node server.js

Debugging

No response when starting the Express server?

  • Make sure you are inside the backend folder when starting the Express server

Got a UnhandledPromiseRejectionWarning error?

(node:5379) UnhandledPromiseRejectionWarning: FetchError: request to https://.kintone.com/k/v1/records.json?app= failed, reason: getaddrinfo ENOTFOUND .kintone.com
  • Looks like Kintone API credentials are missing
  • Be sure to enter your Subdomain, App ID, and API Token under the Kintone API Setup section in server.js

Got a GAIA_IA02 error?

  • Enter the App's API Token in apiToken
  • Be sure to hit the save button & the Update App button to implement the API Token change.

Part F: GET data from the Kintone App

Let's grab data from the Kintone Database App and output it to our frontend React App!

To implement the GET request, we will be editing the following two files:


backend - server.js

We will set up an Express server that calls Kintone on behalf of the frontend React App to avoid the CORS error.

Expected result:

File Location: .../myproject/backend

// Express Server Setup
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');

const PORT = 5000;
const app = express();

// Parse incoming requests with JSON payloads
app.use(express.json());

// Set Cross-Origin Resource Sharing (CORS) to frontend React App
app.use(cors());
const corsOptions = {
  origin: "http://localhost:3000"
};

// Kintone API Setup
const subdomain = ""; //Enter your Kintone Subdomain (ex: devevents)
const appID = ""; //Enter your App's ID number (ex: 1)
const apiToken = ""; //Enter your App's API Token (ex: cJrAD9...)

// Append a Query Parameters to the Request Endpoint
const parameters = "query=order by recordID asc";

const multipleRecordsEndpoint = `https://${subdomain}.kintone.com/k/v1/records.json?app=${appID}&${parameters}`

// This runs if a GET request calls for localhost:5000/getData
app.get('/getData', cors(corsOptions), async (req, res) => {
  const fetchOptions = {
    method: 'GET',
    headers: {
      'X-Cybozu-API-Token': apiToken
    }
  }
  const response = await fetch(multipleRecordsEndpoint, fetchOptions);
  const jsonResponse = await response.json();
  res.json(jsonResponse);
});

app.listen(PORT, () => {
  console.log(`Example app listening at http://localhost:${PORT}`);
});

frontend - index.js

Now that we got our Express server setup, time for configuring the frontend React App!

Expected Result:
Data from the Kintone App will be outputted as bullet points at http://localhost:3000/.

File Location: .../myproject/frontend/src/index.js

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

// Declare the GET endpoint defined in our Express server
const getRecordsEndpoint = "http://localhost:5000/getData";

const callRestApi = async () => {
  const response = await fetch(getRecordsEndpoint);
  const jsonResponse = await response.json();

  console.log(jsonResponse);

  // return JSON.stringify(jsonResponse);

  // Create an array of lists by looping through Kintone's responded array

  //record.title.value = value of the Title field
  //record.author.value = value of the author field

  // In React, assign a unique ID to each created list
  // Use record.recordID.value for key
  const arrayOfLists = jsonResponse.records.map(
    record => <li key={record.recordID.value}><b>{record.title.value}</b> written by {record.author.value}</li>
  )
  return arrayOfLists;
};

function RenderResult() {
  const [apiResponse, setApiResponse] = useState("*** now loading ***");

  useEffect(() => {
    callRestApi().then(
      result => setApiResponse(result));
  }, []);

  return (
    <div>
      <h1>React App</h1>
      <ul>{apiResponse}</ul>
    </div>
  );
};

ReactDOM.render(
  <RenderResult />,
  document.querySelector('#root')
);

Result - Kintone Database App's data displayed as bullet points

Here is what it looks like when displaying our Manga DB App.
It lists out our favorite Japanese comics, ordered by recordID.

Will's Article - React App with Clean Kintone Data

Part G: POST data to the Kintone App

Now that we can retrieve & display the data from the Kintone Database App let's submit new data via our frontend React App!
We will do this by adding a POST request route on the Express server used when the user inputs via the form on the frontend React App.

To implement the POST request, we will be editing the following two files:

Note
Be sure to restart your Express server when updating server.js.

  • Control + C to end the Express server
  • node server.js to start it up again

backend - server.js

We will add another endpoint to make our POST requests.

Expected result:

  • When localhost:5000/getData endpoint is called, a GET request is sent to Kintone for data retrieval.
  • When localhost:5000/postData endpoint is called, a POST request is sent to Kintone to update the database with the submitted entry.

File Location: .../myproject/backend

// Express Server Setup
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');

const PORT = 5000;
const app = express();

// Parse incoming requests with JSON payloads
app.use(express.json());

// Set Cross-Origin Resource Sharing (CORS) to frontend React App
app.use(cors());
const corsOptions = {
  origin: "http://localhost:3000"
};

// Kintone API Setup
const subdomain = ""; //Enter your Kintone Subdomain (ex: devevents)
const appID = ""; //Enter your App's ID number (ex: 1)
const apiToken = ""; //Enter your App's API Token (ex: cJrAD9...)

// Append a Query Parameters to the Request Endpoint
const parameters = "query=order by recordID asc";

// Call Kintone's GET Records API
const multipleRecordsEndpoint = `https://${subdomain}.kintone.com/k/v1/records.json?app=${appID}&${parameters}`

// Call Kintone's GET Record API
const singleRecordEndpoint = `https://${subdomain}.kintone.com/k/v1/record.json?app=${appID}&${parameters}`;

// This runs if a GET request calls for localhost:5000/getData
app.get('/getData', cors(corsOptions), async (req, res) => {
  const fetchOptions = {
    method: 'GET',
    headers: {
      'X-Cybozu-API-Token': apiToken
    }
  }
  // const response = await fetch(requestEndpoint, fetchOptions);
  const response = await fetch(multipleRecordsEndpoint, fetchOptions);

  const jsonResponse = await response.json();
  res.json(jsonResponse);
});

// Add a New Route for a POST request using singleRecordEndpoint

// This runs if a POST request calls for localhost:5000/postData
app.post('/postData', cors(corsOptions), async (req, res) => {
  const requestBody = {
    "app": appID,
    "record": {
      "title": {
        "value": req.body.title
      },
      "author": {
        "value": req.body.author
      }
    }
  };
  const options = {
    method: 'POST',
    headers: {
      'X-Cybozu-API-Token': apiToken,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestBody)
  }
  const response = await fetch(singleRecordEndpoint, options);
  const jsonResponse = await response.json();
  res.json(jsonResponse);
});

app.listen(PORT, () => {
  console.log(`Example app listening at http://localhost:${PORT}`);
});

frontend - index.js

We will add a form for user input and function to make a POST request on our newly defined Express server's endpoint.

Expected result:

  • Display Kintone app data as a clean list
  • Form at the bottom to add user input
  • When an input is submitted, a POST request is sent out & the list is updated

File Location: .../myproject/frontend/src/index.js

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

// Declare the GET & POST endpoints defined in our Express server
const getRecordsEndpoint = "http://localhost:5000/getData";
const addRecordEndpoint = "http://localhost:5000/postData";

const callRestApi = async () => {
  const response = await fetch(getRecordsEndpoint); //Update endpoint
  const jsonResponse = await response.json();
  console.log(jsonResponse);
  const arrayOfLists = jsonResponse.records.map(
    record => <li key={record.recordID.value}><b>{record.title.value}</b> written by {record.author.value}</li>
  )
  return arrayOfLists;
};

// Make REST API Calls & take in the values stored in the state variables related to the input fields
const AddNewRecord = async (Title, Author) => {
  const RecordBodyParameters = {
    'title': Title,
    'author': Author
  }

  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(RecordBodyParameters)
  }

  const response = await fetch(addRecordEndpoint, options);
  const jsonResponse = await response.json();
  console.log(JSON.stringify(jsonResponse));
  return jsonResponse;
};

function RenderResult() {
  const [apiResponse, setApiResponse] = useState("*** now loading ***");

  // Create States for the Input Fields
  const [titleValue, setTitleValue] = useState("");
  const [authorValue, setAuthorValue] = useState("");
  const [successCounter, setSuccessCounter] = useState(0);

  useEffect(() => {
    callRestApi().then(
      result => setApiResponse(result));
  }, [successCounter]);

  // Define the onChange functions
  function HandleTitleChange(event) {
    setTitleValue(event.target.value);
  }

  function HandleAuthorChange(event) {
    setAuthorValue(event.target.value);
  }

  // Define the Button Click function
  function ButtonClick() {
    setApiResponse(apiResponse.concat(<li key="0" >*** now loading ***</li>));
    AddNewRecord(titleValue, authorValue)
      .then(response => {
        setSuccessCounter(successCounter + 1);
      });
  }

  // Append a form for user input
  return (
    <div>
      <h1>React App</h1>
      <ul>{apiResponse}</ul>
      <form>
        <div>
          <label htmlFor="title-input">Title:</label>
          <input type="text" value={titleValue} id="title-input" onChange={HandleTitleChange} />
        </div>
        <div>
          <label htmlFor="author-input">Author:</label>
          <input type="text" value={authorValue} id="author-input" onChange={HandleAuthorChange} />
        </div>
        <button type="button" onClick={ButtonClick}>Add data</button>
      </form>
    </div>
  );
};

ReactDOM.render(
  <RenderResult />,
  document.querySelector('#root')
);

Result - Kintone Database App's data displayed as bullet points with a form to submit a new entry

Will's Article - React App with Data & Form

Troubleshooting Notes

Starting the frontend React Project

Enter the following commands in your terminal

  • $ cd ../myproject/frontend
  • $ npm start

Then access your React project at

  • http://localhost:3000/

Starting the backend Express Server

Enter the following commands in your terminal

  • $ cd ../myproject/backend
  • $ node server.js

Then access your Express server at

  • http://localhost:5000/

Access the Kintone API call at

  • http://localhost:5000/getData

Kintone Section: React not updating after updating server.js

Be sure to restart the server after making changes to server.js!

(1) Go to the terminal running the Express server. It should look something like this:

user@computer backend % node server.js
Example app listening at http://localhost:${PORT}

(2) Restart the Express server

  • Stop the server: ctrl + c
  • Start the server: $ node server.js

(3) Reload the browser showing the React App.

  • http://localhost:3000/

Kintone API Token

To generate an API Token for a Kintone App:

  1. Go to the Kintone App
  2. Go to the Gear icon ⚙️ (top right corner) > Open the App Settings page
  3. Click on the App Settings Tab > Click on API Token settings
  4. Click the Generate button to generate a token
  5. Click the Save button (top left corner) to save the token setting
  6. Finally, click the Update App button (top right corner) to implement the token setting change.

Generating an API Token Gif

Thank you for attending our workshop!

Your Feedback Please 🙇

Please fill out this quick survey for a chance to win 💰️ $25 Amazon Gift Card:

Next Step

For those wanting a visualization projects:

  • Try linking to different amChart libraries, and reuse the code we used in the workshop!

For those wanting to create projects on Kintone and want to retrieve & store data from public APIs:

For those wanting to use Kintone as a back-end database:

Where to Get Help?

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment