Skip to content

A Simple React Example

Building a React App

We will build a simple React app on top of TACTIC Project. The app we create will be a simple issue tracker.

If you need help setting up a React project, please refer to the React documention

Jobs

First we start with the simple boilerplate React app. The first file is index.js with the following code:

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

class Jobs extends React.Component {
  render() {
    return (
      <div className="job">
        <div className="job-list">
          <div>Job Code: JOB00XYZ</div>
        </div>
      </div>
    );
  }
}

// ========================================

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

Next we will add a state variable which will contain the list of all of the jobs.

class Jobs extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      jobs: []
    }
  }

  render() {
    return (
      <div className="job">
        <div className="job-list">
          {
            this.state.jobs.map( (job, index) => (
              <div key="{job.code}">
                Job Code: {job.code} <br/>
                Name: {job.name} <br/>
                Status: {job.status}
                <hr/>
              </div>
            ))
          }
        </div>
      </div>
    );
  }
}

// ========================================

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

Since the jobs item in the state variable is empty, nothing will be listed. In order to get the jobs data, we will need to access the TACTIC server and get a ticket. For more information about connecting to the TACTIC server, please refer to the connection documentation.

We are going to create a new Server.js file with the following content:

let server_url = "https://portal.southpawtech.com"
let site = "trial"
let project = "api_test"
let user = "trial_api"
let password = "tactic123"

let base_endpoint = server_url+ "/" + site + "/" + project + "/REST";

const get_ticket = async () => {

    let url = base_endpoint + "/" + "get_ticket";
    let headers = {
        Accept: 'application/json',
    }
    let data = {
        'login': user,
        'password': password,
    };
    let r = await fetch( url, {
        method: 'POST',
        body: JSON.stringify(data),
    } )
    let ticket = await r.json()
    return ticket;

}

const get_endpoint = () => {
    return base_endpoint;
}

const get_project = () => {
    return project;
}

const call_tactic = async (method, kwargs) => {

    let data = kwargs

    let url = get_endpoint() + "/" + method

    let headers = {
        "Authorization": "Bearer " + ticket,
        Accept: 'application/json',
    }
    let r = await fetch( url, {
        method: 'POST',
        mode: 'cors',
        headers: headers,
        body: JSON.stringify(data),
    } )

    if (r.status == 200) {
        let ret = await r.json();
        return ret;
    }
    else {
        throw("ERROR: " + r.statusText);
    }

}

export { get_endpoint, get_ticket, call_tactic, get_project };

We will create Jobs.js to use the connection and query a list of jobs. First, we define a load method which will retrive all of the jobs from the TACTIC server. Here, we will use the TACTIC expression language to easily retrieve this data. This expression will return a list of JSON objects which can be used for drawing a list of job codes.

import React from 'react';

import {
  Link,
} from 'react-router-dom';

import { get_ticket, call_tactic } from "./Server";

class Jobs extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      jobs: []
    }
  }

  load = async () => {
    this.getJobs()
  }

  getJobs = async () => {
    let ticket = await get_ticket()

    //query for a list of jobs
    let search_type = "workflow/job"
    let kwargs = {
        search_type: search_type,
    };
    let sobjects = await call_tactic("query", kwargs);
    this.setState({jobs: sobjects})
  }

  componentDidMount() {
    this.load()
  }

  render() {
    return (
      <div className="job">
        <div className="job-list">
          {
            this.state.jobs.map( (job, index) => (
              <div key="{job.code}">
                Job Code: {job.code} <br/>
                Name: {job.name} <br/>
                Status: {job.status}
                <hr/>
              </div>
            ))
          }
        </div>
      </div>
    );
  }
}

export default Jobs;

We will modify our index.js by first importing Jobs class: import Jobs from './Jobs'; We will also create a new class App with routing capabilities. Please see below.

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

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
  Link
} from 'react-router-dom';

import Jobs from './Jobs';

import './index.css';

class App extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <Link to="/jobs">All Jobs</Link>
          <hr/>
        </div>
        <Switch>
          <Route exact path="/jobs" component={Jobs} />
          <Redirect from="/" to="/jobs" exact/>
        </Switch>
      </Router>
    );
  }
}

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

Job details

Now that we have a simple job list page running. We will create a job detail page. Let's create a new file called JobDetails.js with the following codes.

import React from 'react';

import { get_ticket, call_tactic } from "./Server";

class JobDetails extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      job: []
    }
  }

  load = async () => {
    //get a job info
    this.getJob()

  }

  getJob = async () => {
    let job_code = this.props.match.params.job_code

    let ticket = await get_ticket()

    //query for the job
    let search_type = "workflow/job"
    let kwargs = {
        search_type: search_type,
        filters: [['code', job_code]],
    };

    let sobjects = await call_tactic("query", kwargs)
    this.setState({job: sobjects})
  }


  componentDidMount() {
    this.load()
  }

  render() {
    return (
      <div className="job">
        <div className="job-list">
          {
            this.state.job.map( (job, index) => (
              <div key="{job.code}">
                Job Code: {job.code} <br/>
                Name: {job.name} <br/>
                Status: {job.status}
                <hr/>
              </div>
            ))
          }
        </div>
      </div>
    );
  }
}

export default JobDetails;

We will import this into index.js:

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

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
  Link
} from 'react-router-dom';

import JobDetails from './JobDetails';
import Jobs from './Jobs';

import './index.css';

class App extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <Link to="/jobs">All Jobs</Link>
          <hr/>
        </div>
        <Switch>
          <Route exact path="/jobs" component={Jobs} />
          <Route exact path="/job_details/:job_code" component={JobDetails} />
          <Redirect from="/" to="/jobs" exact/>
        </Switch>
      </Router>
    );
  }
}

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

Go back to Jobs.js and add a link to the job detail page through the job code in render():

<Link to={"/job_details/" + job.code }>{job.code}</Link>

render() from Jobs.js will look like this now:

render() {
    return (
      <div className="job">
        <div className="job-list">
          {
            this.state.jobs.map( (job, index) => (
              <div key="{job.code}">
                Job Code: <Link to={"/job_details/" + job.code }>{job.code}</Link>  <br/>
                Name: {job.name} <br/>
                Status: {job.status}
                <hr/>
              </div>
            ))
          }
        </div>
      </div>
    );
  }

Tasks

We are going to retrieve tasks associated with a particular job. Let's define a function called getTasks in JobDetails.js:

  getTasks = async () => {

    let job_code = this.props.match.params.job_code

    let ticket = await get_ticket()

    // get tasks
    let search_type = "sthpw/task"
    let kwargs = {
        search_type: search_type,
        filters: [['search_code', job_code]],
    };

    let sobjects = await call_tactic("query", kwargs)
    this.setState({tasks: sobjects})

  }

We will call this from load().

  load = async () => {
    //get a job info
    this.getJob()

    //get tasks
    this.getTasks()
  }

We will modify render() function with the following:

  render() {
    return (
      <div className="job">
        <div className="job-list">
          {
            this.state.job.map( (job, index) => (
              <div key="{job.code}">
                Job Code: {job.code} <br/>
                Name: {job.name} <br/>
                Status: {job.status}

                <h1>Tasks</h1>
                {
                  this.state.tasks.map((tasks,task_index) => (
                    <div class="item">
                      <h2>Process: {tasks.process}</h2> <br/>
                      <h3>Assigned: {tasks.assigned}</h3>
                      <p>Timestamp: {tasks.timestamp}</p>
                    </div>

                  ))
                }
              </div>
            ))
          }
        </div>
      </div>
    );
  }

Job Assets and Snapshots

We currently need to make 2 calls to retrieve all the job assets, and their associated files (snapshots in TACTIC data structure). We will first search job_asset using keywords filter. After getting the job_asset objects, we use the function, query_snapshots, to retrieve the files. See the codes below:

    get_asset_info = async () => {
        let search_text = this.state.search_text // search text from the form

        // First, let's retrieve assets using the keywords search.
        let search_type = "workflow/job_asset"
        let filters = [
            ['keywords', 'contains', search_text]
        ]
        let kwargs = {
            search_type: search_type,
            filters: filters,
        };
        let ticket = await get_ticket()
        let sobjects = await call_tactic("query", kwargs)

        // nothing found
        if (sobjects.length <= 0) {
            this.setState({assets: []})  // set the state variable.
            return;
        }

        // We will retrieve snapshots (files) for the assets. We are going to use
        // query_snapshots function to get the relevant files that are web-accessible.
        const asset_codes = sobjects.map(element => element.code);

        let asset_codes_str = asset_codes.join("|")

        let project_code = get_project()
        let asset_codes_filter = [
            ['search_code', 'in', asset_codes_str], ['is_latest', 'true'], ['project_code', project_code]
        ]

        // include_web_paths_dict gives us the web-accessible paths for the files.
        kwargs = {
            include_paths_dict: true,
            include_web_paths_dict: true,
            filters: asset_codes_filter,
        }

        ticket = await get_ticket()
        let snapshots = await call_tactic("query_snapshots", kwargs)
        console.log(snapshots)

        // we will process the assets and snapshots to be returned or set
        // to the state variable.
        let res = sobjects.map(function(o, i) {
            return {
            code: o.code,
            name: o.name,
            status: o.status,
            snapshot: snapshots[i].__web_paths_dict__
            }
        })
        console.log(res)

        this.setState({assets: res}) // set the state variable.
        return;
    }

Displaying the results:

<div className="job_asset-list">
    {
        this.state.assets.map( (asset, index) => (
            <div key="{asset.code}">
            Asset Code: {asset.code} <br/>
            Name: {asset.name} <br/>
            Status: {asset.status} <br/>
            File:<br/>
            <a href={get_server_url() + asset.snapshot.main[0]} target="_blank">
                <img src={get_server_url() + asset.snapshot.web[0]}/>
            </a>
            <hr/>
            </div>
        ))
    }
</div>

Putting it all together, save the file as JobAssetList.js:

import React from 'react'

import { get_ticket, call_tactic, get_server, get_server_url, get_project } from "./Server";

class AssetList extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            assets: [],
            search_text: "",
        }
    }

    load = async () => {
    }

    get_asset_info = async () => {
        let search_text = this.state.search_text
        let search_type = "workflow/asset"
        let filters = [
            ['keywords', 'contains', search_text]
        ]
        let kwargs = {
            search_type: search_type,
            filters: filters,
        };
        let ticket = await get_ticket()
        let sobjects = await call_tactic("query", kwargs)

        //nothing found
        if (sobjects.length <= 0) {
            this.setState({assets: []})
            return;
        }

        const asset_codes = sobjects.map(element => element.code);
        let asset_codes_str = asset_codes.join("|")

        let project_code = get_project()
        let asset_codes_filter = [
            ['search_code', 'in', asset_codes_str], ['is_latest', 'true'], ['project_code', project_code]
        ]

        kwargs = {
            include_paths_dict: true,
            include_web_paths_dict: true,
            filters: asset_codes_filter,
            order_bys: "search_code",
        }

        let snapshots = await call_tactic("query_snapshots", kwargs)
        console.log(snapshots)

        let obj = {}
        sobjects.forEach(function (a) {
            obj[a.code] = a;
        });

        let res = snapshots.map(function (a) {
            return {
                code: obj[a.search_code].code,
                name: obj[a.search_code].name,
                status: obj[a.search_code].status,
                snapshot: a.__web_paths_dict__
            };
        });

        this.setState({assets: res})
        return;
    }

    componentDidMount() {
        this.load()
    }

    handleInputChange = (event) => {
        this.setState({search_text: event.target.value })
    }

    render() {
        return (
            <div className="job_asset">
                <div className="searchForm">
                    <input type="text" id="filter" placeholder="Search for..." onChange={this.handleInputChange}/>
                    <button onClick={e => this.get_asset_info()}>Search</button>
                </div>
                <div className="job_asset-list">
                    {
                        this.state.assets.map( (asset, index) => (
                            <div key="{asset.code}">
                            Asset Code: {asset.code} <br/>
                            Name: {asset.name} <br/>
                            Status: {asset.status} <br/>
                            File:<br/>
                            <a href={get_server_url() + asset.snapshot.main[0]} target="_blank">
                                {
                                    typeof asset.snapshot.web !=='undefined' ?
                                    <img src={get_server_url() + asset.snapshot.web[0]}/>
                                    : "Download"
                                }
                            </a>
                            <hr/>
                            </div>
                        ))
                    }
                </div>
            </div>
        );
    }
  }

export default JobAssetList;

Now, modify render() from index.js. Note that you need to import first:

import AssetList from './JobAssetList';
render() {
    return (
      <Router>
        <div>
          <Link to="/jobs">All Jobs</Link>
          <Link to="/jobassets">Job Assets</Link>
          <hr/>
        </div>
        <Switch>
          <Route exact path="/jobs" component={Jobs} />
          <Route exact path="/job_details/:job_code" component={JobDetails} />
          <Route exact path="/jobassets" component={JobAssetList} />
          <Redirect from="/" to="/jobs" exact/>
        </Switch>
      </Router>
    );
  }

Assets and Snapshots

We currently need to make 2 calls to retrieve all the assets, and their associated files (snapshots in TACTIC data structure). We will first search asset using keywords filter. After getting the asset objects, we use the function, query_snapshots, to retrieve the files. See the codes below:

    get_asset_info = async () => {
        let search_text = this.state.search_text // search text from the form

        // First, let's retrieve assets using the keywords search.
        let search_type = "workflow/asset"
        let filters = [
            ['keywords', 'contains', search_text]
        ]
        let kwargs = {
            search_type: search_type,
            filters: filters,
        };
        let ticket = await get_ticket()
        let sobjects = await call_tactic("query", kwargs)

        // nothing found
        if (sobjects.length <= 0) {
            this.setState({assets: []})  // set the state variable.
            return;
        }

        // We will retrieve snapshots (files) for the assets. We are going to use
        // query_snapshots function to get the relevant files that are web-accessible.
        const asset_codes = sobjects.map(element => element.code);

        let asset_codes_str = asset_codes.join("|")

        let asset_codes_filter = [
            ['search_code', 'in', asset_codes_str], ['is_latest', 'true']
        ]

        // include_web_paths_dict gives us the web-accessible paths for the files.
        kwargs = {
            include_paths_dict: true,
            include_web_paths_dict: true,
            filters: asset_codes_filter,
        }

        ticket = await get_ticket()
        let snapshots = await call_tactic("query_snapshots", kwargs)
        console.log(snapshots)

        // we will process the assets and snapshots to be returned or set
        // to the state variable.
        let res = sobjects.map(function(o, i) {
            return {
            code: o.code,
            name: o.name,
            status: o.status,
            snapshot: snapshots[i].__web_paths_dict__
            }
        })
        console.log(res)

        this.setState({assets: res}) // set the state variable.
        return;
    }

Displaying the results:

<div className="asset-list">
    {
        this.state.assets.map( (asset, index) => (
            <div key="{asset.code}">
            Asset Code: {asset.code} <br/>
            Name: {asset.name} <br/>
            Status: {asset.status} <br/>
            File:<br/>
            <a href={get_server_url() + asset.snapshot.main[0]} target="_blank">
                <img src={get_server_url() + asset.snapshot.web[0]}/>
            </a>
            <hr/>
            </div>
        ))
    }
</div>

Putting it all together, save the file as AssetList.js:

import React from 'react'

import { get_ticket, call_tactic, get_server, get_server_url } from "./Server";

class AssetList extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            assets: [],
            search_text: "",
        }
    }

    load = async () => {
    }

    get_asset_info = async () => {
        let search_text = this.state.search_text
        let search_type = "workflow/asset"
        let filters = [
            ['keywords', 'contains', search_text]
        ]
        let kwargs = {
            search_type: search_type,
            filters: filters,
        };
        let ticket = await get_ticket()
        let sobjects = await call_tactic("query", kwargs)

        //nothing found
        if (sobjects.length <= 0) {
            this.setState({assets: []})
            return;
        }

        const asset_codes = sobjects.map(element => element.code);
        let asset_codes_str = asset_codes.join("|")

        let asset_codes_filter = [
            ['search_code', 'in', asset_codes_str], ['is_latest', 'true']
        ]

        kwargs = {
            include_paths_dict: true,
            include_web_paths_dict: true,
            filters: asset_codes_filter,
            order_bys: "search_code",
        }

        ticket = await get_ticket()
        let snapshots = await call_tactic("query_snapshots", kwargs)
        console.log(snapshots)

        let obj = {}
        sobjects.forEach(function (a) {
            obj[a.code] = a;
        });

        let res = snapshots.map(function (a) {
            return {
                code: obj[a.search_code].code,
                name: obj[a.search_code].name,
                status: obj[a.search_code].status,
                snapshot: a.__web_paths_dict__
            };
        });

        this.setState({assets: res})
        return;
    }

    componentDidMount() {
        this.load()
    }

    handleInputChange = (event) => {
        this.setState({search_text: event.target.value })
    }

    render() {
        return (
            <div className="asset">
                <div className="searchForm">
                    <input type="text" id="filter" placeholder="Search for..." onChange={this.handleInputChange}/>
                    <button onClick={e => this.get_asset_info()}>Search</button>
                </div>
                <div className="asset-list">
                    {
                        this.state.assets.map( (asset, index) => (
                            <div key="{asset.code}">
                            Asset Code: {asset.code} <br/>
                            Name: {asset.name} <br/>
                            Status: {asset.status} <br/>
                            File:<br/>
                            <a href={get_server_url() + asset.snapshot.main[0]} target="_blank">
                                {
                                    typeof asset.snapshot.web !=='undefined' ?
                                    <img src={get_server_url() + asset.snapshot.web[0]}/>
                                    : "Download"
                                }
                            </a>
                            <hr/>
                            </div>
                        ))
                    }
                </div>
            </div>
        );
    }
  }

export default AssetList;

Now, modify render() from index.js. Note that you need to import first:

import AssetList from './AssetList';
render() {
    return (
      <Router>
        <div>
          <Link to="/jobs">All Jobs</Link>
          <Link to="/assets">Assets</Link>
          <hr/>
        </div>
        <Switch>
          <Route exact path="/jobs" component={Jobs} />
          <Route exact path="/job_details/:job_code" component={JobDetails} />
          <Route exact path="/jobassets" component={JobAssetList} />
          <Route exact path="/assets" component={AssetList} />
          <Redirect from="/" to="/jobs" exact/>
        </Switch>
      </Router>
    );
  }

Congratulations, we have just built a simple React app, listing jobs and assets.