Mark Tripoli I am HP Nonstop developer, who develops train control systems. I love open source projects and information and attempting to teach others new tools. I am a blockchain junkie and love its tech!

Build an issue tracker with jHipster (part 2)

  Reading Time:

Introduction

Welcome back! This is the second part of our JHipster tutorial. We will be working on our frontend portion of our PWA. We will be using ReactJS. If you have not seen the first part of this tutorial, I would recommend you go check it out before diving into this.

Part 1

For this tutorial, there are a few things you should be familiar with prior to starting.

  • Webservices
  • Javascript
  • ReactJS
  • Typescript (with use in React)

In this tutorial, I will be covering a few topics to mold our code changes on the backend to our frontend. I will also show you how to set up new routes, create custom pages, and how to add new items to our reducers.

This tutorial may also be beneficial to those newer to React, especially those with experience with React who want to use Typescript w/ React. Typescript can be a little rocky at first, but once you get the hang of it, it just makes sense.

So buckle up and enjoy the ride.

Navigating the Webapp

If you have dealt with any React application before, you should be relatively familiar with how they are set up. Everyone does theirs differently, luckily, jHipster's default setup is quite easy to catch on to.

JHipster breaks down each of their directories by section.
Screenshot-from-2019-06-06-14-24-05

Pretty straight forward right? There may or may not be more directories, depending on all the added features you chose to select when generating the project, such as Internationalization.

Your business logic will go into "app", where you make changes to your UI, reducers, routes, etc, etc.. You will, of course, put all of our static files in "static". The directories "swagger-ui" and "manifest" do not need to be touched during this tutorial. We will spend basically our entire time in "app" for this lesson.

Screenshot-from-2019-06-06-14-24-25

Entities
Each of the entities we have created is located under:

webapp/app/entities

Screenshot-from-2019-06-06-14-45-56
You will see the two entities we have already created, Issue and Comment. If you go into the Issue directory, you will see all of its relevant files.
Screenshot-from-2019-06-06-14-46-25

index.tsx

Contains routes specific to each entity.

issue.reducer.tsx

Contains our reducer, actions, initial state, etc.

issue.tsx

Contains our Component for our overview of all saved issues.
Screenshot-from-2019-06-06-15-07-31

issue-delete-dialog.txt

Is the dialog component which appears when trying to delete an entity, shocker.
Screenshot-from-2019-06-06-15-08-41

issue-detail.tsx

the component which gives a complete detailed overview of the entity's data.
Screenshot-from-2019-06-06-15-09-40

issue-update.tsx

Finally, we have the component tasked with creating and updating the data of an entity.
Screenshot-from-2019-06-06-15-11-03

All of these components and files have been pre-generated by jHipster. Just about all of them will look the same, regardless of how you structured your entities.

Modules
From my understanding, and I may be wrong, the Modules folder is used for any non-entity related components/views. This is where we have our homepage, admin menus, etc. You could put other pages here that will utilize the entities and present them in a more organized fashion.

Shared
The Shared directory contains our components and methods which will be used across the app. Items such as our header, footer, some reducers, utilities, and our entity models live here.

App Root
In the root of app, we have our standard index.html file for entry into the React app, our top-level routes, etc.

Let's jump into joining our backend to our frontend now.

Some things to Do

Before we get started, in a terminal type the following.

npm i --save @fortawesome/free-regular-svg-icons
npm  i --save @fortawesome/free-brands-svg-icons

Redux

As you should already know, Redux is a tool for managing an applications state (not only for React apps). As applications grow in size, it becomes a very difficult task to manage the state of each component. Hence why we have Redux which will manage and update the states for our components.

If you're still new to Redux it may seem difficult to understand. Trust me, it kicked my *** when I was learning it. Then it clicked once I saw some decent examples and realized it is actually quite simple to understand. You may wonder how useful it really is? To be honest, if you were to make a simple app like we are here, it's really not worth it to use Redux, due to the simplicity of this app. Let's say you were creating an e-commerce store though, where you need to consistently check if the user is authenticated to access certain parts of the website, to check out, etc. You would want an easy way of doing this check right? Well, you will see later on how Redux does this and how simple it really is to implement.

Initial State
Now, if you remember from my previous tutorial, we added a few endpoints to access specific data. In this project, we are going to want to display that data in a few different areas throughout the website. A simple way of doing this is adding those values to our state. Where we can access them simply and will be updated instantly to any change. To do this we need to add a new variable to our initialState in each of our relevant reducer files.

In our React project, jump over and open up

app/entities/issue/issue.reducer.tsx

Take a look at our initial state:
Screen-Shot-2019-06-11-at-3.51.36-PM

You will notice all the items pre-generated by jHipster. We simply will want to modify this structure by adding our own, new values, which Redux will update and store for us.

const initialState = {
  loading: false,
  errorMessage: null,
  entities: [] as ReadonlyArray<IIssue>,
  openIssues: [] as ReadonlyArray<IIssue>,      // new
  reviewedIssues: [] as ReadonlyArray<IIssue>,  // new
  entity: defaultValue,
  updating: false,
  updateSuccess: false
};

Hopefully, if you understand a little bit of Typescript you will know what is going on here. For those that don't, we are simply telling our app that openIssues and reviewIssues will be Read-only arrays (we cannot modify) that will contain Issue objects. The initial value of both objects will be empty.

Actions
For Redux to know what to do when a user performs a function which will affect the state, we need to create actions. These actions tell Redux what to do what the data it's been given for an object. The actions describe what happened (Added, Deleted, Updated, etc.). An action consists of two items

  • Type - The action being performed
  • Payload - The object (data)

Since we are only going to query (request) Issues which are open or reviewed, we only need to create an action to "Fetch" these types of issues. Add the following new Actions.

export const ACTION_TYPES = {
  FETCH_OPEN_ISSUE_LIST: 'issue/FETCH_OPEN_ISSUE_LIST',           //new
  FETCH_REVIEWED_ISSUE_LIST: 'issue/FETCH_REVIEWED_ISSUE_LIST',  //new
  FETCH_ISSUE_LIST: 'issue/FETCH_ISSUE_LIST',
  FETCH_ISSUE: 'issue/FETCH_ISSUE',
  CREATE_ISSUE: 'issue/CREATE_ISSUE',
  UPDATE_ISSUE: 'issue/UPDATE_ISSUE',
  DELETE_ISSUE: 'issue/DELETE_ISSUE',
  SET_BLOB: 'issue/SET_BLOB',
  RESET: 'issue/RESET'
};

Reducer
To update our state, we need a way of determining which state needs to be updated. The job of the Reducer is to annotate how the state will change in response to action which is sent to the store. We will add multiple case statements to our reducer, the move relevant statement will be the SUCCESS. Add the following statements where have the comment "new. next to them.

export default (state: IssueState = initialState, action): IssueState => {
  switch (action.type) {
    case REQUEST(ACTION_TYPES.FETCH_ISSUE_LIST):
    case REQUEST(ACTION_TYPES.FETCH_OPEN_ISSUE_LIST):       //new
    case REQUEST(ACTION_TYPES.FETCH_REVIEWED_ISSUE_LIST):   //new
    case REQUEST(ACTION_TYPES.FETCH_ISSUE):
      return {
        ...state,
        errorMessage: null,
        updateSuccess: false,
        loading: true
      };
    case REQUEST(ACTION_TYPES.CREATE_ISSUE):
    case REQUEST(ACTION_TYPES.UPDATE_ISSUE):
    case REQUEST(ACTION_TYPES.DELETE_ISSUE):
      return {
        ...state,
        errorMessage: null,
        updateSuccess: false,
        updating: true
      };
    case FAILURE(ACTION_TYPES.FETCH_ISSUE_LIST):
    case FAILURE(ACTION_TYPES.FETCH_OPEN_ISSUE_LIST):       //new
    case FAILURE(ACTION_TYPES.FETCH_REVIEWED_ISSUE_LIST):  //new
    case FAILURE(ACTION_TYPES.FETCH_ISSUE):
    case FAILURE(ACTION_TYPES.CREATE_ISSUE):
    case FAILURE(ACTION_TYPES.UPDATE_ISSUE):
    case FAILURE(ACTION_TYPES.DELETE_ISSUE):
      return {
        ...state,
        loading: false,
        updating: false,
        updateSuccess: false,
        errorMessage: action.payload
      };
    case SUCCESS(ACTION_TYPES.FETCH_ISSUE_LIST):
      return {
        ...state,
        loading: false,
        entities: action.payload.data
      };
    case SUCCESS(ACTION_TYPES.FETCH_OPEN_ISSUE_LIST):     //new
      return {
        ...state,
        loading: false,
        openIssues: action.payload.data
      };
    case SUCCESS(ACTION_TYPES.FETCH_REVIEWED_ISSUE_LIST): //new
      return {
        ...state,
        loading: false,
        reviewedIssues: action.payload.data
      };  
    case SUCCESS(ACTION_TYPES.FETCH_ISSUE):
      return {
        ...state,
        loading: false,
        entity: action.payload.data
      };
    case SUCCESS(ACTION_TYPES.CREATE_ISSUE):
    case SUCCESS(ACTION_TYPES.UPDATE_ISSUE):
      return {
        ...state,
        updating: false,
        updateSuccess: true,
        entity: action.payload.data
      };
    case SUCCESS(ACTION_TYPES.DELETE_ISSUE):
      return {
        ...state,
        updating: false,
        updateSuccess: true,
        entity: {}
      };
    case ACTION_TYPES.SET_BLOB:
      const { name, data, contentType } = action.payload;
      return {
        ...state,
        entity: {
          ...state.entity,
          [name]: data,
          [name + 'ContentType']: contentType
        }
      };
    case ACTION_TYPES.RESET:
      return {
        ...initialState
      };
    default:
      return state;
  }
};

Handle Actions
Now with our states, actions, and reducer created, we need a way of actually performing CRUD operations to change the state. To do this we need to create some functions (methods) to actually obtain our open and reviewed issues.

Since we added 2 new endpoints, we need to create some constants for them, add the following:

const apiUrl      = 'api/issues';
const reviewedUrl = 'api/reviewedissues'; //new
const openUrl     = 'api/openissues';     //new

Now let's create our functions to update our new states.

export const getOpenIssues: ICrudGetAllAction<IIssue> = () => ({
  type: ACTION_TYPES.FETCH_OPEN_ISSUE_LIST,
  payload: axios.get<IIssue>(openUrl)
});

export const getReviewedIssues: ICrudGetAllAction<IIssue> = () => ({
  type: ACTION_TYPES.FETCH_REVIEWED_ISSUE_LIST,
  payload: axios.get<IIssue>(reviewedUrl)
});

Awesome, now our reducer has been updated and we are ready to start building the UI portion of the webapp to consume this data. Before we do that, we are going to create a new view to display this data.

Creating a new View

If we want to display our new data we need to add it somewhere. We could use one of our existing views, but that would defeat the purpose of learning how to use the jHipster base, right? So we are going to create a new view, which has the responsibility of displaying and updating data in real time.

To start off we are going to create a few directories and files. First, under the webapp/app directory, create a new directory named components, this will house some of our custom components. Add a new file named "IssueTR.tsx". Next, under webapp/app/modules, create a new directory named monitor. Under this new directory, create a new file named monitor-view.tsx, here we will build our view for the new page.

With these created, we will now want to construct our view. Open monitor-view.tsx and add the following.

IssueCard Component

Our component will be "dumb", meaning, it will simply just recieve data and format it nicely for us to view. It will be a simple card, which we will display how we want. Open the IssueTR file and add the following.

import { TextFormat } from 'react-jhipster';
import { APP_LOCAL_DATE_FORMAT } from 'app/config/constants';
import React from 'react';
import {
  Card,
  CardBody,
  CardFooter,
  Container,
  CardTitle,
  Row,
  Col
} from 'reactstrap';

export const IssueTR = ({ issue }) => (
  <React.Fragment>
    <tr>
      <td>
        {issue.number}
      </td>
      <td>
        <TextFormat value={issue.reportedDate} type="date" format={APP_LOCAL_DATE_FORMAT}/>
      </td>
      <td>
        {issue.priority}
      </td>
      <td>
        {issue.resolution}
      </td>
      <td>
        {issue.description}
      </td>
    </tr>
  </React.Fragment>
);


Monitor View

With our component created, the next part is to actually show it! To do so we need to create our view. The view is simply the web page which will be shown (our Monitor page). The following to monitor-view.tsx

import React from 'react';
import { connect } from 'react-redux';
import {
  Button,
  Row,
  Col,
  CardGroup,
  Card,
  Container,
  CardDeck,
  CardBody,
  CardTitle
} from 'reactstrap';

import { IRootState } from 'app/shared/reducers';
import { getSession } from 'app/shared/reducers/authentication';
import { getProfile } from 'app/shared/reducers/application-profile';
import { hasAnyAuthority } from 'app/shared/auth/private-route';
import { AUTHORITIES } from 'app/config/constants';
import { getOpenIssues, getReviewedIssues } from 'app/entities/issue/issue.reducer';
import { IssueTR } from 'app/components/IssueTR';

export interface IMonitorViewProps extends StateProps, DispatchProps {}

export interface IMonitorViewState {
  seconds: number;
}

export class MonitorView extends React.Component<IMonitorViewProps, IMonitorViewState> {
  private interval: number;
  constructor(props) {
    super(props);
    this.state = {
      seconds: 0
    };
  }
  componentDidMount() {
    this.props.getSession();
    this.props.getOpenIssues();
    this.props.getReviewedIssues();
    this.interval = window.setInterval(() => this.tick(), 10000);
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  tick() {
    this.setState(prevState => ({
      seconds: prevState.seconds + 1
    }));
    this.props.getOpenIssues();
    this.props.getReviewedIssues();
  }
  render() {
    const { openIssues, reviewedIssues } = this.props;
    return (
      <>
        <div className="container-fluid">
          <Row>
            <Col>
              <Row>
                <h5><strong>Open Issues</strong></h5>
                <table className="table table-borderless table-sm">
                  <thead>
                  <tr>
                    <th>Issue #</th>
                    <th>Date</th>
                    <th>Priority</th>
                    <th>Resolution</th>
                    <th>Description</th>
                  </tr>
                  </thead>
                  <tbody>
                  {openIssues === undefined
                    ? (
                      <Card className="card-stats mb-4 mb-xl-0">
                        <Container>
                          <h6>It looks like there are no open issues at this time!</h6>
                        </Container>
                      </Card>
                    ) : (openIssues.map(issue => (
                          <React.Fragment key={issue.id}>
                            <IssueTR issue={issue} />
                          </React.Fragment>
                        )))
                  }
                  </tbody>
                </table>
              </Row>
              <hr/>
              <Row>
                <h5><strong>Reviewed Issues</strong></h5>
                <table className="table table-borderless table-sm">
                  <thead>
                  <tr>
                    <th>Issue #</th>
                    <th>Date</th>
                    <th>Priority</th>
                    <th>Resolution</th>
                    <th>Description</th>
                  </tr>
                  </thead>
                  <tbody>
                  {reviewedIssues === undefined
                    ? (
                      <Card className="card-stats mb-4 mb-xl-0">
                        <Container>
                          <h6>It looks like there are no reviewed issues at this time!</h6>
                        </Container>
                      </Card>
                    ) : (reviewedIssues.map(issue => (
                          <React.Fragment key={issue.id}>
                            <IssueTR issue={issue} />
                          </React.Fragment>
                        )))
                  }
                  </tbody>
                </table>
              </Row>
            </Col>
          </Row>
        </div>
      </>
    );
  }
}

const mapStateToProps = ({ authentication, applicationProfile, issue }: IRootState) => ({
  isAuthenticated: authentication.isAuthenticated,
  isAdmin: hasAnyAuthority(authentication.account.authorities, [AUTHORITIES.ADMIN]),
  ribbonEnv: applicationProfile.ribbonEnv,
  isInProduction: applicationProfile.inProduction,
  isSwaggerEnabled: applicationProfile.isSwaggerEnabled,
  openIssues: issue.openIssues,
  reviewedIssues: issue.reviewedIssues
});

const mapDispatchToProps = {
  getSession,
  getProfile,
  getOpenIssues,
  getReviewedIssues
};

type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(MonitorView);

In this, we are creating a timer which will "tick" every 10 seconds. Each tick, we will query our backend to look for updated Issues. If one is found, we will add it, if one is removed, we remove it. Simple enough? To generate our initial data, we need to execute the functions, we created earlier in our action handling in Redux, in

componentDidMount() {}

We will also initiate our timer.

In our render(), each time we receive new data, we will iterate through the array and create components (IssueCards) for these issues and render them out.

Now let's finish up so we can actually see what we have made.

Update Navbar

We will want our users to access this page, right? So we should probably give them the means to do so. So let's go ahead and create a menu item which will direct them there.

Under webapp/app/shared/layout/header, open

header.tsx

and

header-components.tsx

In header-components, let's create a new component which will be our navitem. Add the following to the header.

import { faTv } from '@fortawesome/free-solid-svg-icons';

Then the following to the bottom

export const MonitorView = () => (
  <NavItem>
    <NavLink tag={Link} to="/monitor" className="d-flex align-items-center">
      <FontAwesomeIcon icon={faTv} />
      <span>Monitor Mode</span>
    </NavLink>
  </NavItem>
);

Now jump over to header.tsx and add the following snippet in the Nav component

{isAuthenticated && <MonitorView />}

This little bit ensures that we only display the new menu item if we are logged in. Go ahead an log out (if logged in) of the application. Take a look at the NavBar, you should not see (if you don't refresh browser).
Screen-Shot-2019-06-12-at-11.26.10-AM
Log back and do the same, you should now see our new item.
Screen-Shot-2019-06-12-at-11.33.26-AM

Add Route

To access our new view, we need to add a route for it. Under webapp/app/routes.tsx, add the following snippets.

Add its import.

import MonitorView from 'app/modules/monitor/monitor-view';
<PrivateRoute path="/monitor" component={MonitorView} hasAnyAuthorities={[AUTHORITIES.USER]} />

What the above snippet does, is add private access to our new view. Only individuals with the AUTHORITY of USER will be able to access this page. So anyone who is not logged in will not be able to see it, anyone who is logged in will since USER is the lowest authority we have configured.

See it in action

Awesome, we have everything set up and we are now ready to view it in action. First, ensure you have at least 1 new and 1 reviewed Issue. Next, make sure you are logged in, and go to the Monitor Mode page. You should see

Screen-Shot-2019-06-12-at-12.34.02-PM

Ok, ok, it may not be beautiful, but it works. If you want to get some more practice, try styling it up to your way. This project uses Reactstrap, check out the components they have. Reactstrap uses Bootstrap for styling, which is widely used.

Extras (Optional)

There are a few more things you can do with your webapp which are useful to know. If you understand the general items we covered above, you should be able to tinker with and figure the following information out, quite easily.

If you're up to it, for our new view, try creating:

  • An admin only view
  • A public view (where any non-registered user and see the tables we made)
  • Add additional styling/components
  • Show some more data we have.
  • Create a new view where you can view all the comments for an issue or add a comment yourself (outside of the entities menu).

If you have any questions about how to perform these. Feel free to drop a comment or join my Discord and i'll help you out!

Wrapping things up

In this tutorial we covered:

  • How to tie in our frontend to our backend.
  • Add new Routes
  • Add new components
  • Add items to our state
  • Add an action
  • Add a reducer
  • Add items to our Navbar
  • Add a new View

With all this knowledge
knowledge

If you found this helpful and would like a tutorial on deployment, drop a comment below.

Thanks for stopping by!

All code for this tutorial can be found on our Github page

Build an issue tracker with jHipster (part 1)

Learn the basics of how to create a PWA with JHipster and ReactJS. Part 1 consists of the server backend....

Creating Push Notifications With Java

In this tutorial, I will be showing you how to create push notifications using java. I will show you how to create one using SystemTray in...

Triippz Tech   Never miss a story from Triippz Tech, get updates in your inbox.