0

I am currently working on a React + Node application.

The application stores data entries in a data base. Users should be able to download all stored data from the database to an excel workbook. To achieve this, I'm using exceljs npm module.

The Problem: I implement a POST route called downloadFile which should be triggered by a client when the download button is clicked. The route is admittedly triggered but no excel file is downloaded.

Ironically, while troubleshooting, I changed the POST route to a GET route in the index.js of my node application, then manually typed the route (http://localhost:3001/downloadFile) and hit the enter key on the keyboard and the workbook was downloaded. This made me believe that the core functionality of the downloadFile route works.

What can I do to enable clients trigger the downloadFile POST route by clicking a button?

Below are the codes for the project thus far. I didn't include codes for other routes in the index.js file that are working perfectly to ensure brevity and more clarity.

SERVER-SIDE CODE (NODEJS & EXPRESS)

index.js

const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const excel = require("exceljs");
const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

mongoose.connect("mongodb://localhost:27017/NOC", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", function () {
  console.log("We're connected to db!");
});

//Ticket schema
const ticketSchema = new mongoose.Schema({
  nodeA: {
    type: String,
    required: true,
  },
  nodeB: {
    type: String,
    required: true,
  },
  vendor: {
    type: String,
    required: true,
  },
  impact: {
    type: String,
    required: false,
  },
  route: {
    type: String,
    required: false,
  },
  timeDown: {
    type: String,
    required: true,
  },
  timeUp: {
    type: String,
    required: false,
  },
  TTR: {
    type: Number,
    required: false,
  },
  RDT: {
    type: Number,
    required: false,
  },
  siteIDWithPowerFailure: {
    type: String,
    required: false,
  },
  COF: {
    type: String,
    required: false,
  },
  action: {
    type: String,
    required: false,
  },
  byWhom: {
    type: String,
    required: true,
  },
  subSystem: {
    type: String,
    required: false,
  },
});

const ticket = mongoose.model("ticket", ticketSchema);

...

//downloadFile post route
app.post("/downloadFile", function (req, res) {

  //retrieve all tickets in database and store it as result array
  ticket.find({}, function (err, results) {
    let modifiedResult = results.reverse();

    if (err) {
      console.log("Could not retrieve data from database " + err);
      res.status(400);
    } else {
      //create a new excel workbook, a worksheet and set its properties.
      const workbook = new excel.Workbook();
      const sheet = workbook.addWorksheet("FIBRE_SWITCH", {
        properties: {
          tabColor: { argb: "FFc0000" },
        },
      });
      sheet.columns = [
        { header: "S/N", key: "S/N", width: 4 },
        { header: "TERMINAL A", key: "nodeA", width: 12 },
        { header: "TERMINAL B", key: "nodeB", width: 12 },
        { header: "VENDOR", key: "vendor", width: 9 },
        { header: "IMPACT", key: "impact", width: 12 },
        { header: "ROUTE", key: "route", width: 10 },
        {
          header: "FROM (DATE & TIME)",
          key: "timeDown",
          width: 12,
        },
        { header: "TO (DATE & TIME)", key: "timeUp", width: 12 },
        { header: "TTR (HRS)", key: "TTR (HRS)", width: 8 },
        { header: "RDT (HRS)", key: "RDT (HRS)", width: 8 },
        {
          header: "SITE ID OF OFC ISSUE WITH POWER",
          key: "siteIDWithPowerFailure",
          width: 9,
        },
        {
          header: "PROBLEM/SPECIFIC CAUSE",
          key: "COF",
          width: 14,
        },
        { header: "ACTION TAKEN", key: "action", width: 14 },
        { header: "BY WHOM", key: "byWhom", width: 12 },
        { header: "SUBSYSTEM", key: "subSystem", width: 12 },
      ];
      
      //write the data from the database to the excel worksheet cells.
      sheet.addRows(modifiedResult);
      res.setHeader(
        "Content-Type",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
      );
      res.setHeader(
        "Content-Disposition",
        "attachment; filename=" + "OFC REPORT.xlsx"
      );

      //Download workbook.
      return workbook.xlsx.write(res).then(function () {
        console.log("Download success");
        res.status(200);
      });
    }
  });
});

app.listen(3001, function () {
  console.log("Server started at port 3001");
});

CLIENT-SIDE CODE (REACT.JS)

App.js


import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import Failure from "./Failure";

function App() {
  return (
    <div className="App">
      <Failure />
    </div>
  );
}

export default App;

Failure.js

import React, { useEffect, useState } from "react";

export default function Failure() {
  const [ticketList, setTicketList] = useState([]);
  const [formData, setFormData] = useState({
    nodeA: "",
    nodeB: "",
    vendor: "",
    timeDown: "",
    OFCsiteWithPowerIssue: "",
    byWhom: "",
  });

  useEffect(() => {
    if (ticketList.length === 0) {
      getAllTicket();
    }
  });

  const onChangeHandler = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    setFormData((prevState) => {
      return {
        ...prevState,
        [name]: typeof value === "string" ? value.toUpperCase() : value,
      };
    });
  };

  const updateUi = (res) => {
    res &&
      res.json().then((data) => {
        const reversedData = data.reverse();
        setTicketList(reversedData);
      });
  };

  const registerFailure = (event, data) => {
    event.preventDefault();
    fetch("/registerFailure", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to register ticket, " + err);
      });
  };

  const deleteTicket = (_id) => {
    const id = { _id: _id };
    fetch("/deleteTicket", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(id),
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to delete ticket, " + err);
      });
  };

  const getAllTicket = () => {
    fetch("/getTable", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to fetch all ticket, " + err);
      });
  };

  //Download excel workbook handler function
  const downloadExcelFile = (e) => {
    e.preventDefault();
    fetch("/downloadFile", {
      method: "POST",
    })
      .then((response) => {
        if (response.status === 200) {
          console.log("Download started");
        }
      })
      .catch((err) => {
        alert("Failed to fetch all ticket, " + err);
      });
  };

  return (
    <>
      <h1>Register Failure</h1>
      <div className="row contact-form">
        <div className="col-sm-12">
          <form
            className="form-horizontal"
            onSubmit={(e) => registerFailure(e, formData)}
            method="POST"
          >
            <div className="form-group">
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Terminal A"
                  name="nodeA"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.nodeA}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Terminal B"
                  name="nodeB"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.nodeB}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Vendor"
                  name="vendor"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.vendor}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="From"
                  name="timeDown"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.timeDown}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Site with power issue"
                  name="OFCsiteWithPowerIssue"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.OFCsiteWithPowerIssue}
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="By Whom"
                  name="byWhom"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.byWhom}
                  required
                />
              </div>
            </div>
            <div className="form-group">
              <div className="col-sm-offset-2 col-sm-12">
                <button
                  type="submit"
                  className="btn btn-outline-success form-button"
                >
                  Submit
                </button>
              </div>
            </div>
          </form>
          <form
            className="form-horizontal"
            onSubmit={(e) => downloadExcelFile(e)}
            method="POST"
          >
            <div className="form-group">
              <div className="col-sm-offset-2 col-sm-12">
                <button
                  type="submit"
                  className="btn btn-outline-success form-button"
                >
                  Download Excel File
                </button>
              </div>
            </div>
          </form>
          {ticketList.length === 0 ? (
            <h1 style={{ textAlign: "center" }}>
              There are no entries to display
            </h1>
          ) : (
            <table className="table">
              <thead>
                <tr>
                  <th>Terminal A</th>
                  <th>Terminal B</th>
                  <th>Vendor</th>
                  <th>Impact</th>
                  <th>Route</th>
                  <th>From</th>
                  <th>To</th>
                  <th>TTR</th>
                  <th>RDT</th>
                  <th>SITE ID WITH POWER FAILURE</th>
                  <th>PROBABLE/SPECIFIC CAUSE</th>
                  <th>ACTION TAKEN</th>
                  <th>BY WHOM</th>
                  <th>SUB SYSTEM</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {ticketList.map((ticket) => {
                  return (
                    <tr key={ticket._id}>
                      <td>{ticket.nodeA}</td>
                      <td>{ticket.nodeB}</td>
                      <td>{ticket.vendor}</td>
                      <td>{ticket.impact}</td>
                      <td>{ticket.route}</td>
                      <td>{ticket.timeDown}</td>
                      <td>{ticket.timeUp}</td>
                      <td>{ticket.TTR}</td>
                      <td>{ticket.RDT}</td>
                      <td>{ticket.siteIDWithPowerFailure}</td>
                      <td>{ticket.COF}</td>
                      <td>{ticket.action}</td>
                      <td>{ticket.byWhom}</td>
                      <td>{ticket.subSystem}</td>
                      <td>
                        <button
                          className="btn btn-outline-danger"
                          onClick={() => deleteTicket(ticket._id)}
                        >
                          Delete Ticket
                        </button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </>
  );
}
Ehizo
  • 15
  • 1
  • 4

1 Answers1

0

I think what's happening is Fetch is swallowing the contents of the response - AFAIK it doesn't by default trigger a download.

You can verify this using the browser tools to see what comes back on the wire when you click the button - but your server side code looks solid to me.

Solution

  1. Switch the endpoint to a get
  2. Switch the button to an <a href="/downloadFile" download>Download File</a>
  3. Click the link - you should see a new tab open very quickly then a download start in the browser
  4. Style the a tag appropriately

Caveats

One potential issue here is that you won't be able to put error handling logic into your React code (there's no support for arbitrary code execution on failure to download from an a tag). If you need this behaviour you can do something like this:

  1. Using fetch, when a button is clicked retrieve the file contents (exactly like your current example)
    • If the request fails - show an error state
    • If the request succeeds - push the payload into a dynamically created a tag and programatically click it see examples
Jack Dunleavy
  • 249
  • 1
  • 7
  • So I changed the endpoint to "get" and switched the button to an "a" tag just like you suggested, but the it resulted in a failed download. The file it intended to download was a "downloadFile.html". I changed the href attribute from "/downloadFile" to "http://localhost:3001/downloadFile" and clicking the "a" tag triggered the download perfectly. But then, "http://localhost:3001/" is solely for development purpose and will definitely change when the app is deployed. Is there a way to modify the "a" tag's href attribute to properly target the "/downloadFile" endpoint? – Ehizo Mar 08 '21 at 09:58