<|start_filename|>tornjak-frontend/src/components/cluster-edit.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import { Dropdown, TextInput, MultiSelect, TextArea } from 'carbon-components-react';
import GetApiServerUri from './helpers';
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';
import './style.css';
import {
  clustersListUpdateFunc,
  clusterTypeInfoFunc,
  serverSelectedFunc,
  selectorInfoFunc,
  agentsListUpdateFunc,
  tornjakMessageFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc
} from 'redux/actions';

class ClusterEdit extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.onChangeClusterNameList = this.onChangeClusterNameList.bind(this);
    this.onChangeClusterName = this.onChangeClusterName.bind(this);
    this.onChangeClusterType = this.onChangeClusterType.bind(this);
    this.onChangeManualClusterType = this.onChangeManualClusterType.bind(this);
    this.onChangeClusterDomainName = this.onChangeClusterDomainName.bind(this);
    this.onChangeClusterManagedBy = this.onChangeClusterManagedBy.bind(this);
    this.prepareClusterNameList = this.prepareClusterNameList.bind(this);
    this.onChangeAgentsList = this.onChangeAgentsList.bind(this);
    this.onSubmit = this.onSubmit.bind(this);

    this.state = {
      originalClusterName: "",
      clusterName: "",
      clusterType: "",
      clusterDomainName: "",
      clusterManagedBy: "",
      clusterAgentsList: "",
      clusterNameList: [],
      clusterTypeList: this.props.clusterTypeList,
      clusterTypeManualEntryOption: "----Select this option and Enter Custom Cluster Type Below----",
      clusterTypeManualEntry: false,
      message: "",
      statusOK: "",
      selectedServer: "",
      agentsListDisplay: "Select Agents",
      agentsListSelected: [],
      assignedAgentsListDisplay: "",
    }
  }

  componentDidMount() {
    if (IsManager) {
      if (this.props.globalServerSelected !== "" && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
        this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc)
        this.setState({ selectedServer: this.props.globalServerSelected });
      }
    } else {
      this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
    }
    this.prepareClusterNameList();
  }

  componentDidUpdate(prevProps, prevState) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.setState({ selectedServer: this.props.globalServerSelected });
      }
    }
    if (prevProps.globalClustersList !== this.props.globalClustersList) {
      this.prepareClusterNameList();
    }
    if (prevProps.globalAgentsList !== this.props.globalAgentsList) {
      this.prepareClusterNameList();
    }
  }

  prepareClusterNameList() {
    var clusters = this.props.globalClustersList;
    let localClusterNameList = [];
    if (this.props.globalServerInfo.length === 0) {
      return
    }
    for (let i = 0; i < clusters.length; i++) {
      localClusterNameList[i] = clusters[i].name;
    }
    this.setState({
      clusterNameList: localClusterNameList
    });
  }

  onChangeClusterNameList(e) {
    var sid = e.selectedItem, clusters = this.props.globalClustersList, cluster = [], assignedAgentsDisplay = "", agentsDisplay = "", agentsListSelected = [];
    for (let i = 0; i < clusters.length; i++) {
      if (clusters[i].name === sid) {
        cluster = clusters[i];
      }
    }
    for (let i = 0; i < cluster.agentsList.length; i++) {
      agentsListSelected[i] = {}
      agentsListSelected[i]["label"] = cluster.agentsList[i];
    }
    assignedAgentsDisplay = cluster.agentsList.join("\n");
    agentsDisplay = cluster.agentsList.toString();
    this.setState({
      originalClusterName: cluster.name,
      clusterName: cluster.name,
      clusterType: cluster.platformType,
      clusterDomainName: cluster.domainName,
      clusterManagedBy: cluster.managedBy,
      clusterAgentsList: cluster.agentsList,
      agentsListDisplay: agentsDisplay, //agents list multiselect display
      assignedAgentsListDisplay: assignedAgentsDisplay, //agents list text box display
      agentsListSelected: agentsListSelected, //initial selected agents
    });
    return
  }

  onChangeClusterName(e) {
    var sid = e.target.value;
    this.setState({
      clusterName: sid
    });
    return
  }

  onChangeClusterType = selected => {
    var sid = selected.selectedItem;
    if (sid.length === 0) {
      this.setState({
        clusterType: sid,
      });
      return
    }
    if (sid === this.state.clusterTypeManualEntryOption) {
      this.setState({
        clusterTypeManualEntry: true,
        clusterType: sid,
      });
      return
    }
    this.setState({
      clusterTypeManualEntry: false,
      clusterType: sid,
    });
    return
  }

  onChangeManualClusterType(e) {
    var sid = e.target.value;
    if (sid.length === 0) {
      this.setState({
        clusterType: sid
      });
      return
    }
    this.setState({
      clusterType: sid
    });
    return
  }

  onChangeClusterDomainName(e) {
    var sid = e.target.value;
    this.setState({
      clusterDomainName: sid
    });
    return
  }

  onChangeClusterManagedBy(e) {
    var sid = e.target.value;
    this.setState({
      clusterManagedBy: sid
    });
    return
  }

  onChangeAgentsList = selected => {
    var sid = selected.selectedItems, agents = "", agentsDisplay = "", assignedAgentsDisplay = "";
    let localAgentsIdList = [];
    for (let i = 0; i < sid.length; i++) {
      localAgentsIdList[i] = sid[i].label;
    }
    console.log("sid", sid)
    agents = localAgentsIdList;
    agentsDisplay = localAgentsIdList.toString();
    assignedAgentsDisplay = localAgentsIdList.join("\n");
    if (agentsDisplay.length === 0) {
      agentsDisplay = "Select Agents"
    }
    this.setState({
      clusterAgentsList: agents,
      agentsListDisplay: agentsDisplay,
      assignedAgentsListDisplay: assignedAgentsDisplay,
      agentsListSelected: sid,
    });
  }

  getApiEntryCreateEndpoint() {
    if (!IsManager) {
      return GetApiServerUri('/api/tornjak/clusters/edit')
    } else if (IsManager && this.state.selectedServer !== "") {
      return GetApiServerUri('/manager-api/tornjak/clusters/edit') + "/" + this.state.selectedServer
    } else {
      this.setState({ message: "Error: No server selected" })
      return ""
    }
  }

  onSubmit(e) {
    e.preventDefault();

    if (this.state.originalClusterName.length === 0) {
      this.setState({ message: "ERROR: Please Choose a Cluster" });
      return
    }

    if (this.state.clusterName.length === 0) {
      this.setState({ message: "ERROR: Cluster Name Can Not Be Empty - Enter Cluster Name" });
      return
    }

    if (this.state.clusterType.length === 0) {
      this.setState({ message: "ERROR: Cluster Type Can Not Be Empty - Choose/ Enter Cluster Type" });
      return
    }

    var cjtData = {
      "cluster": {
        "Name": this.state.originalClusterName,
        "EditedName": this.state.clusterName,
        "PlatformType": this.state.clusterType,
        "DomainName": this.state.clusterDomainName,
        "ManagedBy": this.state.clusterManagedBy,
        "AgentsList": this.state.clusterAgentsList
      }
    }

    let endpoint = this.getApiEntryCreateEndpoint();
    if (endpoint === "") {
      return
    }
    axios.post(endpoint, cjtData)
      .then(
        res => this.setState({
          message: "Request:" + JSON.stringify(cjtData, null, ' ') + "\n\nSuccess:" + JSON.stringify(res.data, null, ' '),
          statusOK: "OK",
        })
      )
      .catch(
        err => this.setState({
          message: "ERROR:" + err.response.data,
          statusOK: "ERROR"
        })
      )
      if (IsManager) {
        if (this.props.globalServerSelected !== "" && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
          this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
        }
      } else {
        this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      }
  }
  render() {
    const ClusterNames = this.state.clusterNameList, ClusterType = this.props.clusterTypeList;
    return (
      <div>
        <div className="cluster-edit">
          <div className="create-edit-title">
            <h3>Edit Cluster</h3>
          </div>
          <form onSubmit={this.onSubmit}>
            <br /><br />
            <div className="entry-form">
              <div className="clustertype-drop-down">
                <Dropdown
                  aria-required="true"
                  ariaLabel="clustertype-drop-down"
                  id="clustertype-drop-down"
                  items={ClusterNames}
                  label="Select Cluster"
                  titleText="Choose Cluster [*required]"
                  onChange={this.onChangeClusterNameList}
                  required />
                <p className="cluster-helper">i.e. Choose Cluster Name To Edit</p>
              </div>
              <div className="clustername-input-field">
                <TextInput
                  helperText="i.e. exampleabc"
                  id="clusterNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Edit Cluster Name"
                  placeholder="Edit CLUSTER NAME"
                  defaultValue={this.state.clusterName}
                  onChange={this.onChangeClusterName}
                  required />
              </div>
              <div className="clustertype-drop-down">
                <Dropdown
                  ariaLabel="clustertype-drop-down"
                  id="clustertype-drop-down"
                  items={ClusterType}
                  label="Select Cluster Type"
                  selectedItem={this.state.clusterType}
                  titleText="Edit Cluster Type"
                  onChange={this.onChangeClusterType}
                  required />
                <p className="cluster-helper">i.e. Kubernetes, VMs...</p>
              </div>
              {this.state.clusterTypeManualEntry === true &&
                <div className="clustertype-manual-input-field">
                  <TextInput
                    helperText="i.e. Kubernetes, VMs..."
                    id="clusterTypeManualInputField"
                    invalidText="A valid value is required - refer to helper text below"
                    labelText="Cluster Type - Manual Entry"
                    placeholder="Enter Cluster Type"
                    onChange={(e) => {
                      this.onChangeManualClusterType(e);
                    }}
                  />
                </div>}
              <div className="cluster-domain-name-input-field">
                <TextInput
                  helperText="i.e. example.org"
                  id="clusterDomainNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Edit Cluster Domain Name/ URL"
                  placeholder="Edit CLUSTER DOMAIN NAME/ URL"
                  defaultValue={this.state.clusterDomainName}
                  onChange={this.onChangeClusterDomainName}
                />
              </div>
              <div className="cluster-managed-by-input-field">
                <TextInput
                  helperText="i.e. person-A"
                  id="clusterNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Edit Cluster Managed By"
                  placeholder="Edit CLUSTER MANAGED BY"
                  defaultValue={this.state.clusterManagedBy}
                  onChange={this.onChangeClusterManagedBy}
                />
              </div>
              <div className="agents-multiselect">
                <MultiSelect.Filterable
                  key={this.state.agentsListDisplay}
                  titleText="Edit Assigned Agents To Cluster"
                  helperText="i.e. spiffe://example.org/agent/myagent1..."
                  placeholder={this.state.agentsListDisplay}
                  ariaLabel="selectors-multiselect"
                  id="selectors-multiselect"
                  initialSelectedItems={this.state.agentsListSelected}
                  items={this.props.agentsList}
                  label={this.state.agentsListDisplay}
                  onChange={this.onChangeAgentsList}
                />
              </div>
              <div className="selectors-textArea">
                <TextArea
                  cols={50}
                  helperText="i.e. spiffe://example.org/agent/myagent1..."
                  id="selectors-textArea"
                  invalidText="A valid value is required"
                  labelText="Assigned Agents"
                  placeholder="Assigned agents will be populated here - Refer to Assign Agents To Cluster"
                  defaultValue={this.state.assignedAgentsListDisplay}
                  rows={8}
                  disabled
                />
              </div>
              <div className="form-group">
                <input type="submit" value="Edit Cluster" className="btn btn-primary" />
              </div>
              <div>
                {this.state.statusOK === "OK" &&
                  <p className="success-message">--CLUSTER SUCCESSFULLY EDITED--</p>
                }
                {(this.state.statusOK === "ERROR") &&
                  <p className="failed-message">--CLUSTER EDIT FAILED--</p>
                }
              </div>
              <div className="alert-primary" role="alert">
                <pre>
                  {this.state.message}
                </pre>
              </div>
            </div>
          </form>
        </div>
      </div>
    )
  }
}


const mapStateToProps = (state) => ({
  globalClusterTypeInfo: state.clusters.globalClusterTypeInfo,
  globalClustersList: state.clusters.globalClustersList,
  globalServerSelected: state.servers.globalServerSelected,
  globalSelectorInfo: state.servers.globalSelectorInfo,
  globalAgentsList: state.agents.globalAgentsList,
  globalServerInfo: state.servers.globalServerInfo,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
  globalWorkloadSelectorInfo: state.servers.globalWorkloadSelectorInfo,
  globalAgentsWorkLoadAttestorInfo: state.agents.globalAgentsWorkLoadAttestorInfo,
})

export default connect(
  mapStateToProps,
  { clusterTypeInfoFunc, serverSelectedFunc, selectorInfoFunc, agentsListUpdateFunc, tornjakMessageFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, clustersListUpdateFunc }
)(ClusterEdit)


<|start_filename|>tornjak-frontend/src/redux/store.js<|end_filename|>
//---redux--------------------------------------
import {applyMiddleware, createStore} from 'redux';
import allReducers from './reducers'; //import all reducers
import thunk from "redux-thunk";
import { composeWithDevTools } from 'redux-devtools-extension'; //for Redox dev

function saveToLocalStorage(state) {
    try {
        const serializedState = JSON.stringify(state)
        sessionStorage.setItem('state', serializedState)
    } catch(e) {
        console.log(e)
    }
}

function loadFromLocalStorage() {
    try {
        const serializedState = sessionStorage.getItem('state')
        if (serializedState === null) return undefined
        return JSON.parse(serializedState)
    } catch(e) {
        console.log(e)
        return undefined
    }
}

const persistedState = loadFromLocalStorage()
// STORE -> Globalized state - where to store all data
const store = createStore(
    allReducers, 
    persistedState,
    //applyMiddleware(thunk)
    composeWithDevTools(applyMiddleware(thunk)) //for redux devtools extension for dev
  ); //store all combined reducers in one
  //--------------------------------------------

  store.subscribe(() => saveToLocalStorage(store.getState()))
  export default store;

<|start_filename|>tornjak-frontend/src/redux/reducers/tornjakReducer.js<|end_filename|>
import {
    GLOBAL_MESSAGE,
    GLOBAL_CLICKED_DASHBOARD_TABLE,
} from '../actions/types';

const initialState = {
    globalErrorMessage: "",
    globalClickedDashboardTable: "",
};

export default function tornjakReducer(state = initialState, action) {
    switch (action.type) {
        case GLOBAL_MESSAGE:
            return {
                ...state,
                globalErrorMessage: action.payload
            };
        case GLOBAL_CLICKED_DASHBOARD_TABLE:
            return {
                ...state,
                globalClickedDashboardTable: action.payload
            };
        default:
            return state;
    }
}

<|start_filename|>tornjak-frontend/src/components/dashboard/clusters-dashboard-table.js<|end_filename|>
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import TableDashboard from './table/dashboard-table';
import SpiffeHelper from '../spiffe-helper';
import TornjakHelper from 'components/tornjak-helper';

const columns = [
  { field: "name", headerName: "Name", width: 200 },
  { field: "created", headerName: "Created", width: 300 },
  { field: "numNodes", headerName: "Number Of Nodes", width: 300 },
  { field: "numEntries", headerName: "Number of Entries", width: 200 }
];

const styles = theme => ({
  seeMore: {
    marginTop: theme.spacing(3),
  },
});

class ClusterDashboardTable extends React.Component {
  constructor(props) {
    super(props);
    this.SpiffeHelper = new SpiffeHelper();
    this.TornjakHelper = new TornjakHelper();
  }

  clusterList() {
    var filterByValue = [];
    const { filterByCluster } = this.props;
    let clustersList = [];
    if (typeof this.props.globalClustersList === 'undefined') {
      return [];
    }
    clustersList = this.props.globalClustersList.map(a => this.TornjakHelper.getClusterMetadata(a, this.props.globalEntries.globalEntriesList, this.props.globalAgents.globalAgentsList));
    //For details page filtering data
    if (filterByCluster === undefined) {
      return clustersList;
    }
    for (let i = 0; i < clustersList.length; i++) {
      if ((filterByCluster !== undefined && clustersList[i].name === filterByCluster)) {
        filterByValue.push(clustersList[i]);
      }
    }
    return filterByValue;
  }

  render() {
    const { numRows } = this.props;
    var data = this.clusterList();
    return (
      <div>
        <TableDashboard
          title={"Clusters"}
          numRows={numRows}
          columns={columns}
          data={data} />
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  globalClustersList: state.clusters.globalClustersList,
  globalAgents: state.agents,
  globalEntries: state.entries,
})

export default withStyles(styles)(
  connect(mapStateToProps, {})(ClusterDashboardTable)
)


<|start_filename|>tornjak-frontend/src/components/dashboard/clusters-pie-chart.js<|end_filename|>
import React from 'react';
import Title from './title';
import { connect } from 'react-redux';
import PieChart1 from "charts/PieChart";


class ClustersPieChart extends React.Component {
  cluster(entry) {
    return {
      "group": entry.name,
      "value": entry.agentsList.length
    }
  }

  clusterList() {
    if (typeof this.props.globalClustersList !== 'undefined') {
      return this.props.globalClustersList.map(currentCluster => {
        return this.cluster(currentCluster);
      })
    } else {
      return []
    }
  }

  render() {
    var sections = this.clusterList()
    return (
      <React.Fragment>
        <Title>Number of Agents per Cluster</Title>
        {sections.length === 0 &&
          <p className="no-data">No Data To Display</p>
        }
        {sections.length !== 0 &&
        <PieChart1
          data={sections}
        />
        }
      </React.Fragment>
    );
  }
}

const mapStateToProps = (state) => ({
  globalClustersList: state.clusters.globalClustersList,
})

export default connect(mapStateToProps, {})(ClustersPieChart)


<|start_filename|>tornjak-frontend/src/components/style.css<|end_filename|>
.navigation-bar {
  min-width: 20px;
  z-index: 3;
  background-color: black;
  min-width: 1300px;
  width: 100%;
}

.logo {
  margin-top: -50px;
}

.dropdown-container {
  margin-left: 200px;
}
.dropbtn {
    color: white;
    padding: 30px;
    font-size: 16px;
    border: none;
  }
  
  .dropdown {
    position: relative;
    display: inline-block;
    padding: 15px;
  }
  
  .dropdown-content {
    margin-top: 10px;
    position: absolute;
    background-color: #f1f1f1;
    display: none;
    min-width: 130px;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    z-index: 3;
  }
  
  .dropdown-content a {
    color: black;
    padding: 12px 16px;
    z-index: 3;
  }
  
  .dropdown-content a:hover {background-color: #ddd;}
  .dropdown:hover .dropdown-content {display: block;}
  .dropdown:hover .dropbtn {color: #ffffff; text-decoration: none;}

  .indvidual-list-table {
    margin-top: 10px;
    width: 98%;
    margin-bottom: 20px;
  }

  .create-entry-title {
    margin-left: auto;
    margin-right: auto;
  }
  
  .entry-form {
    margin-left: auto;
    margin-right: auto;
    width: 60%;
    border-width:1px;
    border-style:solid;
    border-color: rgb(180, 178, 178);
    padding: 30px 30px 30px 30px;
  }
  .parentId-drop-down {
    margin-bottom: 20px;
  }
  
  .clustertype-drop-down {
    margin-bottom: 20px;
  }

  .parentId-manual-input-field {
    margin-bottom: 20px;
    margin-left: 40px;
  }

  .clustertype-manual-input-field {
    margin-bottom: 20px;
    margin-left: 40px;
  }

  .spiffeId-input-field {
    margin-bottom: 20px;
  }

  .cluster-domain-name-input-field {
    margin-bottom: 20px;
  }

  .cluster-managed-by-input-field {
    margin-bottom: 20px;
  }

  .clustername-input-field {
    margin-bottom: 20px;
  }

  .parentId-helper {
    font-size: 12px;
    color:rgb(82, 79, 79);
    margin-top: 2px;
  }

  .cluster-helper {
    font-size: 12px;
    color:rgb(82, 79, 79);
    margin-top: 2px;  
  }
  .selectors-multiselect {
    margin-bottom: 20px;
  }

  .agents-multiselect {
    margin-bottom: 20px;
  }
  .selectors-textArea {
    margin-bottom: 20px;
  }

  .ttl-input {
    display: inline-block;
    margin-right: 20px;
    margin-bottom: 20px;
    width: 160px
  }

  .expiresAt-input {
    display: inline-block;
    margin-bottom: 20px;
    width: 160px;
  }
  
  .federates-with-input-field {
    margin-bottom: 20px;
    width: 55%;
  }
  .dnsnames-input-field {
    margin-bottom: 20px;
    width: 55%;
  }

  .servers-drp-dwn {
    margin-top: 5px;
  }

  .success-message {
    font-weight: bold;
    color: green;
  }

  .failed-message {
    font-weight: bold;
    color:red;
  }

  .no-data {
    margin-top: 120px;
    margin-left: 200px;
  }

<|start_filename|>tornjak-frontend/src/components/dashboard/dashboard-details-render.js<|end_filename|>
import React from 'react';
import { Component } from 'react';
import { connect } from 'react-redux';
import IsManager from '../is_manager';
import DashboardDetails from './dashboard-details';
import TornjakHelper from '../tornjak-helper';
import TornjakApi from '../tornjak-api-helpers';
import {
    serverSelectedFunc,
    clustersListUpdateFunc,
    agentsListUpdateFunc,
    entriesListUpdateFunc,
    tornjakServerInfoUpdateFunc,
    serverInfoUpdateFunc,
    selectorInfoFunc,
    tornjakMessageFunc,
    workloadSelectorInfoFunc,
    agentworkloadSelectorInfoFunc,
    clusterTypeInfoFunc,
    clickedDashboardTableFunc,
} from 'redux/actions';

// DashboardDetailsRender takes in details url parameters
// returns details page for dashboard for a specific entity
class DashboardDetailsRender extends Component {
    constructor(props) {
        super(props);
        this.TornjakApi = new TornjakApi();
        this.TornjakHelper = new TornjakHelper();
        this.state = {};
    }

    componentDidMount() {
        const { params } = this.props;
        if (this.props.globalTornjakServerInfo === "" || this.props.globalServerInfo === undefined) {
            if (IsManager) {
                if (this.props.globalServerSelected !== "") {
                    this.props.clickedDashboardTableFunc(params.entity + "details");
                    this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
                    this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
                    this.TornjakApi.populateEntriesUpdate(this.props.globalServerSelected, this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc);
                    this.TornjakApi.refreshSelectorsState(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc);
                    this.TornjakApi.populateTornjakAgentInfo(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc, "");
                    this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
                }
            } else {
                this.props.clickedDashboardTableFunc(params.entity + "details");
                this.TornjakApi.refreshLocalSelectorsState(this.props.agentworkloadSelectorInfoFunc);
                this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
                this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
                this.TornjakApi.populateLocalEntriesUpdate(this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc);
                this.TornjakApi.populateLocalTornjakAgentInfo(this.props.agentworkloadSelectorInfoFunc, "");
                this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
                if (this.props.globalTornjakServerInfo !== "") {
                    this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
                }
            }
        }
    }

    render() {
        const { params } = this.props;
        return (
            <DashboardDetails selectedData={this.TornjakHelper.detailsDataParse(params, this.props)} />
        );
    }
}

const mapStateToProps = (state) => ({
    globalServerInfo: state.servers.globalServerInfo,
    globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
    globalClustersList: state.clusters.globalClustersList,
    globalAgentsList: state.agents.globalAgentsList,
    globalEntriesList: state.entries.globalEntriesList,
    globalAgentsWorkLoadAttestorInfo: state.agents.globalAgentsWorkLoadAttestorInfo,
})

export default connect(
    mapStateToProps,
    { serverSelectedFunc, clustersListUpdateFunc, agentsListUpdateFunc, entriesListUpdateFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, selectorInfoFunc, tornjakMessageFunc, workloadSelectorInfoFunc, agentworkloadSelectorInfoFunc, clusterTypeInfoFunc, clickedDashboardTableFunc }
)(DashboardDetailsRender)


<|start_filename|>tornjak-backend/pkg/agent/db/sqlite_txhelpers.go<|end_filename|>
package db

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/mattn/go-sqlite3"
	"github.com/pkg/errors"

	"github.com/spiffe/tornjak/tornjak-backend/pkg/agent/types"
)

type tornjakTxHelper struct {
	ctx context.Context
	tx  *sql.Tx
}

func getTornjakTxHelper(ctx context.Context, tx *sql.Tx) *tornjakTxHelper {
	return &tornjakTxHelper{ctx, tx}
}

func (t *tornjakTxHelper) rollbackHandler(err error) error {
	if err == nil { // THIS SHOULD NOT HAPPEN
		return errors.New("Rollback handler called upon no error")
	} else {
		rollbackErr := t.tx.Rollback()
		var rollbackStatus string
		if rollbackErr != nil {
			rollbackStatus = fmt.Sprintf("[Unsuccessful rollback [%v] upon error]", rollbackErr.Error())
		} else {
			rollbackStatus = "[Successful rollback upon error]"
		}
		if serr, ok := err.(SQLError); ok {
			return SQLError{serr.Cmd, errors.Errorf("%v: %v", serr.Err, rollbackStatus)}
		} else if serr, ok := err.(GetError); ok {
			return GetError{fmt.Sprintf("%v: %v", serr.Message, rollbackStatus)}
		} else if serr, ok := err.(PostFailure); ok {
			return PostFailure{fmt.Sprintf("%v: %v", serr.Message, rollbackStatus)}
		} else {
			return errors.Errorf("%v: %v", err.Error(), rollbackStatus)
		}
	}
}

// insertClusterMetadata attempts insert into table clusters
// returns SQLError upon failure and PostFailure on cluster existence
func (t *tornjakTxHelper) insertClusterMetadata(cinfo types.ClusterInfo) error {
	cmdInsert := `INSERT INTO clusters (name, created_at, domain_name, managed_by, platform_type) VALUES (?,?,?,?,?)`
	statement, err := t.tx.PrepareContext(t.ctx, cmdInsert)
	if err != nil {
		return SQLError{cmdInsert, err}
	}
	defer statement.Close()
	_, err = statement.ExecContext(t.ctx, cinfo.Name, time.Now().Format("Jan 02 2006 15:04:05"), cinfo.DomainName, cinfo.ManagedBy, cinfo.PlatformType)
	if err != nil {
		if serr, ok := err.(sqlite3.Error); ok && serr.Code == sqlite3.ErrConstraint {
			return PostFailure{"Cluster already exists; use Edit Cluster"}
		}
		return SQLError{cmdInsert, err}
	}
	return nil
}

// updateClusterMetadata attempts update of entry in table clusters
// returns SQLError on failure and PostFailure on cluster non-existence
func (t *tornjakTxHelper) updateClusterMetadata(cinfo types.ClusterInfo) error {
	cmdUpdate := `UPDATE clusters SET name=?, domain_name=?, managed_by=?, platform_type=? WHERE name=?`
	statement, err := t.tx.PrepareContext(t.ctx, cmdUpdate)
	if err != nil {
		return SQLError{cmdUpdate, err}
	}
	defer statement.Close()
	res, err := statement.ExecContext(t.ctx, cinfo.EditedName, cinfo.DomainName, cinfo.ManagedBy, cinfo.PlatformType, cinfo.Name)
	if err != nil {
		if serr, ok := err.(sqlite3.Error); ok && serr.Code == sqlite3.ErrConstraint {
			return PostFailure{"Cluster already exists; use Edit Cluster"}
		}
		return SQLError{cmdUpdate, err}
	}

	// check if update was successful
	numRows, err := res.RowsAffected()
	if err != nil {
		return SQLError{cmdUpdate, err}
	}
	if numRows != 1 {
		return PostFailure{"Cluster does not exist; use Create Cluster"}
	}

	return nil
}

// deleteClusterMetadata attemps delete of entry in table clusters
// returns SQLError on failure and PostFailure on cluster non-existence
func (t *tornjakTxHelper) deleteClusterMetadata(name string) error {
	cmdDelete := `DELETE FROM clusters WHERE name=?`
	statement, err := t.tx.PrepareContext(t.ctx, cmdDelete)
	if err != nil {
		return SQLError{cmdDelete, err}
	}
	res, err := statement.ExecContext(t.ctx, name)
	if err != nil {
		return SQLError{cmdDelete, err}
	}
	numRows, err := res.RowsAffected()
	if err != nil {
		return SQLError{cmdDelete, err}
	}
	if numRows != 1 {
		return PostFailure{"Cluster does not exist"}
	}
	return nil
}

// addAgentBatchToCluster adds entries in clusterMemberships table
// takes in cluster name and list of agent spiffeids
// returns SQLError on failure and PostFailure on conflict (an agent is already assigned)
func (t *tornjakTxHelper) addAgentBatchToCluster(clustername string, agentsList []string) error {
	if len(agentsList) == 0 {
		return nil
	}
	// Add into agents table
	cmdAgents := "INSERT OR IGNORE INTO agents (spiffeid, plugin) VALUES "
	agents := []interface{}{}
	for i := 0; i < len(agentsList); i++ {
		cmdAgents += "(?, NULL),"
		agents = append(agents, agentsList[i])
	}
	cmdAgents = strings.TrimSuffix(cmdAgents, ",")
	statementAgentInsert, err := t.tx.PrepareContext(t.ctx, cmdAgents)
	if err != nil {
		return SQLError{cmdAgents, err}
	}
	_, err = statementAgentInsert.ExecContext(t.ctx, agents...)
	if err != nil {
		return SQLError{cmdAgents, err}
	}

	// generate single statement
	cmdBatch := "INSERT OR ABORT INTO cluster_memberships (agent_id, cluster_id) VALUES "
	vals := []interface{}{}
	for i := 0; i < len(agentsList); i++ {
		cmdBatch += "((SELECT id FROM agents WHERE spiffeid=?), (SELECT id FROM clusters WHERE name=?)),"
		vals = append(vals, agentsList[i], clustername)
	}
	cmdBatch = strings.TrimSuffix(cmdBatch, ",")

	// prepare statement
	statementInsert, err := t.tx.PrepareContext(t.ctx, cmdBatch)
	if err != nil {
		return SQLError{cmdBatch, err}
	}
	// execute single statement and check error
	_, err = statementInsert.ExecContext(t.ctx, vals...)
	if err != nil {
		if serr, ok := err.(sqlite3.Error); ok && serr.Code == sqlite3.ErrConstraint {
			// TODO add more details of agent conflict?
			return PostFailure{serr.Error()}
		}
		return SQLError{cmdBatch, err}
	}
	return nil

}

// deleteClusterAgents attempts removal of all agent-cluster pairs in clusterMemberships table
// returns SQLError on failure
func (t *tornjakTxHelper) deleteClusterAgents(clustername string) error {
	cmdDelete := "DELETE FROM cluster_memberships WHERE cluster_id=(SELECT id FROM clusters WHERE name=?)"
	statementDelete, err := t.tx.PrepareContext(t.ctx, cmdDelete)
	if err != nil {
		return SQLError{cmdDelete, err}
	}
	_, err = statementDelete.ExecContext(t.ctx, clustername)
	if err != nil {
		return SQLError{cmdDelete, err}
	}
	return nil
}


<|start_filename|>tornjak-frontend/src/components/select-server.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import GetApiServerUri from './helpers';
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';

import {
    serverSelectedFunc,
    serversListUpdateFunc,
    tornjakServerInfoUpdateFunc,
    serverInfoUpdateFunc,
    agentsListUpdateFunc,
    tornjakMessageFunc
} from 'redux/actions';

const ServerDropdown = props => (
    <option value={props.value}>{props.name}</option>
)

class SelectServer extends Component {
    constructor(props) {
        super(props);
        this.TornjakApi = new TornjakApi();
        this.serverDropdownList = this.serverDropdownList.bind(this);
        this.onServerSelect = this.onServerSelect.bind(this);

        this.state = {
        };
    }

    componentDidMount() {
        if (IsManager) {
            this.populateServers()
            if ((this.props.globalServerSelected !== "") && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
                this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
            }
            if ((this.props.globalTornjakServerInfo !== "") && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
                this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
                this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc)
            }
        }
    }

    componentDidUpdate(prevProps) {
        if (IsManager) {
            if (prevProps.globalServerSelected !== this.props.globalServerSelected ) {
                this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
                this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
                this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
            }
        }
    }

    populateServers() {
        axios.get(GetApiServerUri("/manager-api/server/list"), { crossdomain: true })
            .then(response => {
                this.props.serversListUpdateFunc(response.data["servers"]);
            })
            .catch((error) => {
                console.log(error);
            })
    }

    serverDropdownList() {
        if (typeof this.props.globalServersList !== 'undefined') {
            return this.props.globalServersList.map(server => {
                return <ServerDropdown key={server.name}
                    value={server.name}
                    name={server.name} />
            })
        } else {
            return ""
        }
    }

    onServerSelect(e) {
        const serverName = e.target.value;
        if (serverName !== "") {
            this.props.serverSelectedFunc(serverName);
        }
    }

    getServer(serverName) {
        var i;
        const servers = this.props.globalServersList
        for (i = 0; i < servers.length; i++) {
            if (servers[i].name === serverName) {
                return servers[i]
            }
        }
        return null
    }

    render() {
        let managerServerSelector = (
            <div id="server-dropdown-div">
                <label id="server-dropdown">Choose a Server</label>
                <div className="servers-drp-dwn">
                    <select name="servers" id="servers" onChange={this.onServerSelect}>
                        <optgroup label="Servers">
                            <option value="none" selected disabled>Select an Option </option>
                            <option value="none" selected disabled>{this.props.globalServerSelected} </option>
                            {this.serverDropdownList()}
                        </optgroup>
                    </select>
                </div>
            </div>
        )
        return (
            <div>
                {IsManager && managerServerSelector}
            </div>
        )
    }
}

const mapStateToProps = (state) => ({
    globalServerSelected: state.servers.globalServerSelected,
    globalServersList: state.servers.globalServersList,
    globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
    globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
    mapStateToProps,
    { serverSelectedFunc, serversListUpdateFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, agentsListUpdateFunc, tornjakMessageFunc }
)(SelectServer)


<|start_filename|>tornjak-frontend/src/components/tornjak-helper.js<|end_filename|>
// Additional objects the functions take
// agentsSpiffeIdDict - list of avaialble agent's spiffe ids
// workLoadAttestorInfo - https://github.com/spiffe/tornjak/blob/main/docs/tornjak-ui-api-documentation.md#:~:text=AgentWorkloadSelectorInfo
// clusterEntry = single cluster metadata entry
import { Component } from 'react';
import SpiffeHelper from './spiffe-helper';

class TornjakHelper extends Component {
  constructor(props) {
    super(props);
    this.SpiffeHelper = new SpiffeHelper();
    this.state = {
    };
  }
  
  // detailsLink takes in selectedRows/ specified data and entity
  // returns a specfic routing link/ URL for an entity
  detailsLink(selectedRows, entity) {
    const dashboardDetailsLink = "/tornjak/dashboard/details/";
    var detailsLink = "", searchParams = new URLSearchParams(window.location.search);;
    if (selectedRows.length !== 0) {
      searchParams.set("id", encodeURIComponent(selectedRows.id)); //encode URL since spiffeid contains special characters
      detailsLink = dashboardDetailsLink + entity.toLowerCase() + "?" + searchParams.toString();
    }
    return detailsLink;
  }

  // detailsDataParse takes in url parameters for a specific url for details page and
  // properties of a class including clustersList, agentsList, entriesList and agentsWorkLoadAttestorInfo
  // returns a parsed and filtered data for the specifed entity from the url parameteres 
  detailsDataParse(urlParams, props) {
    const searchParams = new URLSearchParams(window.location.search);
    const parsed = searchParams.get("id")
    let selectedData = [{}], id = decodeURIComponent(parsed);
    const { globalClustersList, globalAgentsList, globalEntriesList, globalAgentsWorkLoadAttestorInfo } = props;
    if (urlParams.entity === "clusters") {
      for (let i = 0; i < globalClustersList.length; i++) {
        if (globalClustersList[i].name === id) {
          selectedData = this.getClusterMetadata(globalClustersList[i], globalEntriesList, globalAgentsList);
        }
      }
    } else if (urlParams.entity === "agents") {
      for (let i = 0; i < globalAgentsList.length; i++) {
        let agentId = this.SpiffeHelper.getAgentSpiffeid(globalAgentsList[i]);
        if (agentId === id) {
          selectedData = this.getDashboardAgentMetaData(globalAgentsList[i], globalEntriesList, globalAgentsList, globalAgentsWorkLoadAttestorInfo);
        }
      }
    } else if (urlParams.entity === "entries") {
      for (let i = 0; i < globalEntriesList.length; i++) {
        if (globalEntriesList[i].id === id) {
          selectedData = this.workloadEntry(globalEntriesList[i], globalAgentsWorkLoadAttestorInfo);
        }
      }
    }
    return selectedData;
  }

  // numberEntriesOfCluster takes in an clusterEntry and list of entries
  // returns number of workload/child entries for a cluster
  numberEntriesOfCluster(clusterEntry, globalEntries, globalAgents) {
    var agents = globalAgents.filter(a => 
        clusterEntry.agentsList.includes(this.SpiffeHelper.getAgentSpiffeid(a)))
    var entriesPerAgent = agents.map(currentAgent => {
      return this.SpiffeHelper.numberEntriesOfAgent(currentAgent, globalEntries)
    })
    var sum = entriesPerAgent.reduce((acc, curVal) => {
      return acc + curVal;
    }, 0)
    return sum
  }

  // getClusterMetadata takes in an clusterEntry and list of entries
  // returns cluster metadata info for dashboard table
  getClusterMetadata(entry, globalEntries, globalAgents) {
    return {
      id: entry.name,
      name: entry.name,
      created: entry.creationTime,
      numNodes: entry.agentsList.length,
      numEntries: this.numberEntriesOfCluster(entry, globalEntries, globalAgents),
    }
  }

  // getDashboardAgentMetaData takes in an agent metadata, list of entries and workload attestor info for specified agents
  // returns agent metadata info for dashboard table
  getDashboardAgentMetaData(agent, globalEntries, globalAgents, workLoadAttestorInfo) {
    var thisSpiffeid = this.SpiffeHelper.getAgentSpiffeid(agent);
    // get status
    var status = this.SpiffeHelper.getAgentStatusString(agent);
    // get tornjak metadata
    var metadataEntry = this.SpiffeHelper.getAgentMetadata(thisSpiffeid, workLoadAttestorInfo);
    var plugin = "None"
    var cluster = "None"
    if (typeof metadataEntry["plugin"] !== 'undefined' && metadataEntry["plugin"].length !== 0) {
      plugin = metadataEntry["plugin"]
    }
    if (typeof metadataEntry["cluster"] !== 'undefined' && metadataEntry["cluster"].length !== 0) {
      cluster = metadataEntry["cluster"]
    }
    return {
      id: thisSpiffeid,
      spiffeid: thisSpiffeid,
      numEntries: this.SpiffeHelper.numberEntriesOfAgent(agent, globalEntries),
      status: status,
      platformType: plugin,
      clusterName: cluster,
    }
  }

  // workloadEntry takes in an entry metadata and workload attestor info for specified agents
  // returns entry metadata info for dashboard table
  workloadEntry(entry, workLoadAttestorInfo, globalAgents, globalEntries) {
    var thisSpiffeId = this.SpiffeHelper.getEntrySpiffeid(entry)
    var thisParentId = this.SpiffeHelper.getEntryParentid(entry)
    var canonicalParentId = thisParentId
    // get tornjak metadata
    var parentAgent = globalAgents.find(a => this.SpiffeHelper.getAgentSpiffeid(a) === thisParentId)
    if (parentAgent === undefined) {
        var agent = globalEntries.find(e => this.SpiffeHelper.getEntrySpiffeid(e) === thisParentId)
        if (agent !== undefined) {
            canonicalParentId = this.SpiffeHelper.getCanonicalAgentSpiffeid(agent, globalAgents)
        }
    }

    var plugin = "None"
    var cluster = "None"
    var metadataEntry = this.SpiffeHelper.getAgentMetadata(canonicalParentId, workLoadAttestorInfo);
    if (metadataEntry["plugin"].length !== 0) {
      plugin = metadataEntry["plugin"]
    }
    if (metadataEntry["cluster"].length !== 0) {
      cluster = metadataEntry["cluster"]
    }

    // get spire data
    var admin = this.SpiffeHelper.getEntryAdminFlag(entry)
    var expTime = "No Expiry Time"
    if (typeof entry.expires_at !== 'undefined') {
      var d = new Date(this.SpiffeHelper.getEntryExpiryMillisecondsFromEpoch(entry))
      expTime = d.toLocaleDateString("en-US", { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false })
    }
    return {
      id: entry.id,
      spiffeid: thisSpiffeId,
      parentId: thisParentId,
      adminFlag: admin,
      entryExpireTime: expTime,
      platformType: plugin,
      clusterName: cluster,
      canonicalAgentId: canonicalParentId,
    }
  }

}

export default TornjakHelper;


<|start_filename|>tornjak-frontend/src/components/is_manager.js<|end_filename|>
require('dotenv').config();

// API_SERVER_URL
console.log("IS_MANAGER:" + process.env.REACT_APP_TORNJAK_MANAGER)
const IsManager = process.env.REACT_APP_TORNJAK_MANAGER !== undefined && process.env.REACT_APP_TORNJAK_MANAGER.toUpperCase() !== "FALSE"

module.exports = IsManager


<|start_filename|>tornjak-frontend/src/components/dashboard/table/dashboard-table.js<|end_filename|>
import React from "react";
import { connect } from 'react-redux';
import { DataGrid, GridToolbar } from "@material-ui/data-grid";
import Title from '../title';
import {
  Button,
} from '@material-ui/core';
import {
  clickedDashboardTableFunc,
} from 'redux/actions';
import TornjakHelper from 'components/tornjak-helper';

class TableDashboard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedRows: [],
    };
    this.TornjakHelper = new TornjakHelper();
  }

  render() {
    const { numRows, data, columns, title } = this.props;
    return (
      <React.Fragment>
        <Title>
          <Button
            color="inherit"
            size="large"
            onClick={() => { this.props.clickedDashboardTableFunc(title.toLowerCase()); }}
          >
            {title}
          </Button>
        </Title>
        <Button //Selected Details Button
          href={this.TornjakHelper.detailsLink(this.state.selectedRows, title)}
          style={{ width: 160, marginLeft: 1040, marginBottom: 20 }}
          color="primary"
          size="small"
          variant="outlined"
          onClick={() => {
            if (this.state.selectedRows.length === 0) {
              window.alert("Please Select a Row to See Details.");
            } else {
              this.props.clickedDashboardTableFunc(title.toLowerCase() + "details")
            }
          }}
        >
          Selected Details
        </Button>
        <div style={{ width: "100%" }}>
          <DataGrid
            rows={data}
            columns={columns}
            pageSize={numRows}
            autoHeight={true}
            onRowSelected={(selectedRows) => {
              this.setState({ selectedRows: selectedRows.data })
            }}
            components={{
              Toolbar: GridToolbar,
            }}
          />
        </div>
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => ({
  globalClickedDashboardTable: state.tornjak.globalClickedDashboardTable,
})

export default connect(
  mapStateToProps,
  { clickedDashboardTableFunc }
)(TableDashboard);


<|start_filename|>tornjak-frontend/src/tables/table-body.js<|end_filename|>
import React from "react";
import { DataTable } from "carbon-components-react";
import WorkLoadAttestor from 'components/work-load-attestor-modal';
const {
    TableBody,
    TableRow,
    TableSelectRow,
    TableCell,
} = DataTable;

// Body take in 
// entityType: type of entity 
// rows: rows of data to be rendered on table body
// getSelectionProps: getSelectionProps func for selecting rows from DataTable
// returns the body of the table for the specified entity
class Body extends React.Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <TableBody>
                {this.props.rows.map((row, key) => (
                    <TableRow key={key}>
                        <TableSelectRow {...this.props.getSelectionProps({ row })} />
                        {row.cells.map((cell) => (
                            <TableCell key={cell.id}>
                                {cell.info.header === "info" ? (
                                    <div style={{ overflowX: 'auto', width: "400px" }}>
                                        <pre>{cell.value}</pre>
                                    </div>
                                ) : (
                                    cell.value)}
                            </TableCell>
                        ))}
                        {this.props.entityType === "Agent" &&
                            <TableCell>
                                <div>
                                    <WorkLoadAttestor
                                        spiffeid={row.cells[2].value}
                                        agentData={row}
                                    />
                                </div>
                            </TableCell>
                        }
                    </TableRow>
                ))}
            </TableBody>
        )
    }
};

export default (Body)


<|start_filename|>tornjak-backend/pkg/agent/types/clusterinfo.go<|end_filename|>
package types

// ClusterInfo contains the meta-information about clusters
// TODO include details field for extra info/tags in json format (probably a byte array)
type ClusterInfo struct {
	Name         string   `json:"name"`
	EditedName   string   `json:"editedName"`
	CreationTime string   `json:"creationTime"`
	DomainName   string   `json:"domainName"`
	ManagedBy    string   `json:"managedBy"`
	PlatformType string   `json:"platformType"`
	AgentsList   []string `json:"agentsList"`
}

type ClusterInput struct {
	ClusterInstance ClusterInfo `json:"cluster"`
}

// ClusterInfoList contains the meta-information about clusters
type ClusterInfoList struct {
	Clusters []ClusterInfo `json:"clusters"`
}


<|start_filename|>tornjak-frontend/src/components/cluster-list.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import IsManager from './is_manager';
import Table from "tables/clusters-list-table";
import TornjakApi from './tornjak-api-helpers';
import {
  serverSelectedFunc,
  agentsListUpdateFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc,
  selectorInfoFunc,
  tornjakMessageFunc,
  workloadSelectorInfoFunc,
  agentworkloadSelectorInfoFunc,
  clustersListUpdateFunc,
} from 'redux/actions';

const Cluster = props => (
  <tr>
    <td>{props.name}</td>
    <td>{props.platformType}</td>
    <td>{props.domainName}</td>
    <td>{props.managedBy}</td>
    <td><div style={{ overflowX: 'auto', width: "400px" }}>
      <pre>{JSON.stringify(props.agentsList, null, ' ')}</pre>
    </div></td>
  </tr>
)

class ClusterList extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.state = {
      message: "",
    };
  }

  componentDidMount() {
    if (IsManager) {
      if (this.props.globalServerSelected !== "") {
        this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      }
    } else {
      this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      if (this.props.globalTornjakServerInfo !== "") {
        this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
      }
    }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      }
    } else {
      if (prevProps.globalTornjakServerInfo !== this.props.globalTornjakServerInfo) {
        this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
      }
    }
  }

  clusterList() {
    if (typeof this.props.globalClustersList !== 'undefined') {
      return this.props.globalClustersList.map(currentCluster => {
        return <Cluster key={currentCluster.name}
          cluster={currentCluster} />;
      })
    } else {
      return ""
    }
  }

  render() {
    return (
      <div>
        <h3>Clusters List</h3>
        {this.props.globalErrorMessage !== "OK" &&
          <div className="alert-primary" role="alert">
            <pre>
              {this.props.globalErrorMessage}
            </pre>
          </div>
        }
        <br /><br />
        <div className="indvidual-list-table">
          <Table data={this.clusterList()} id="table-1" />
        </div>
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
  globalClustersList: state.clusters.globalClustersList,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
  mapStateToProps,
  { serverSelectedFunc, agentsListUpdateFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, selectorInfoFunc, tornjakMessageFunc, workloadSelectorInfoFunc, agentworkloadSelectorInfoFunc, clustersListUpdateFunc }
)(ClusterList)


<|start_filename|>tornjak-backend/pkg/agent/types/agentinfo.go<|end_filename|>
package types

// AgentInfo contains the information about agents workload attestor plugin
type AgentInfo struct {
	Spiffeid string `json:"spiffeid"`
	Plugin   string `json:"plugin"`
	Cluster  string `json:"cluster"`
}

// AgentInfoList contains the information about agents workload attestor plugin
type AgentInfoList struct {
	Agents []AgentInfo `json:"agents"`
}

// AgentEntries contains agent spiffeid and list of spiffeids of Entries
type AgentEntries struct {
	Spiffeid    string   `json:"spiffeid"`
	EntriesList []string `json:"entries_list"`
}

// AllAgentEntries contains a list of agent entry spiffeids
type AllAgentEntries struct {
	Agents []AgentEntries `json:"agents"`
}

// AgentMetadataRequest contains a list of spiffeids
type AgentMetadataRequest struct {
	Agents []string `json:"agents"`
}


<|start_filename|>tornjak-frontend/src/tables/clusters-list-table.js<|end_filename|>
import React from "react";
import { connect } from 'react-redux';
import IsManager from 'components/is_manager';
import GetApiServerUri from 'components/helpers';
import axios from 'axios';
import {
    clustersListUpdateFunc
} from 'redux/actions';
import Table from './list-table';

// ClusterListTable takes in 
// listTableData: clusters data to be rendered on table
// returns clusters data inside a carbon component table with specified functions
class ClustersListTable extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            listData: props.data,
            listTableData: [{ "id": "0" }],
        };
        this.prepareTableData = this.prepareTableData.bind(this);
        this.deleteCluster = this.deleteCluster.bind(this);
    }

    componentDidMount() {
        this.prepareTableData();
    }
    componentDidUpdate(prevProps, prevState) {
        if (prevProps !== this.props) {
            this.setState({
                listData: this.props.globalClustersList
            })
            this.prepareTableData();
        }
    }

    prepareTableData() {
        const { data } = this.props;
        let listData = [...data];
        let listtabledata = [];
        for (let i = 0; i < listData.length; i++) {
            listtabledata[i] = {};
            listtabledata[i]["id"] = (i + 1).toString();
            listtabledata[i]["clusterName"] = listData[i].props.cluster.name;
            listtabledata[i]["clusterType"] = listData[i].props.cluster.platformType;
            listtabledata[i]["clusterManagedBy"] = listData[i].props.cluster.managedBy;
            listtabledata[i]["clusterDomainName"] = listData[i].props.cluster.domainName;
            listtabledata[i]["clusterAssignedAgents"] = <pre>{JSON.stringify(listData[i].props.cluster.agentsList, null, ' ')}</pre>
        }
        this.setState({
            listTableData: listtabledata
        })
    }

    deleteCluster(selectedRows) {
        var cluster = [], endpoint = "";
        let promises = [];
        if (IsManager) {
            endpoint = GetApiServerUri('/manager-api/tornjak/clusters/delete') + "/" + this.props.globalServerSelected;
        } else {
            endpoint = GetApiServerUri('/api/tornjak/clusters/delete');
        }
        if (selectedRows.length !== 0) {
            for (let i = 0; i < selectedRows.length; i++) {
                cluster[i] = {}
                cluster[i]["name"] = selectedRows[i].cells[1].value;
                promises.push(axios.post(endpoint, {
                    "cluster": {
                        "name": cluster[i].name
                    }
                }))
            }
        } else {
            return ""
        }
        Promise.all(promises)
            .then(responses => {
                for (let i = 0; i < responses.length; i++) {
                    this.props.clustersListUpdateFunc(this.props.globalClustersList.filter(el =>
                        el.name !== cluster[i].name));
                }
            })
            .catch((error) => {
                console.log(error);
            })
    }

    render() {
        const { listTableData } = this.state;
        const headerData = [
            {
                header: '#No',
                key: 'id',
            },
            {
                header: 'Cluster Name',
                key: 'clusterName',
            },
            {
                header: 'Cluster Type',
                key: 'clusterType',
            },
            {
                header: 'Cluster Managed By',
                key: 'clusterManagedBy',
            },
            {
                header: 'Cluster Domain Name',
                key: 'clusterDomainName',
            },
            {
                header: 'Assigned Agents',
                key: 'clusterAssignedAgents',
            },
        ];
        return (
            <div>
                <Table
                    listTableData={listTableData}
                    headerData={headerData}
                    deleteEntity={this.deleteCluster}
                />
            </div>
        );
    }
}

const mapStateToProps = (state) => ({
    globalServerSelected: state.servers.globalServerSelected,
    globalClustersList: state.clusters.globalClustersList,
})

export default connect(
    mapStateToProps,
    { clustersListUpdateFunc }
)(ClustersListTable)


<|start_filename|>tornjak-frontend/src/components/dashboard/agents-dashboard-table.js<|end_filename|>
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import renderCellExpand from './render-cell-expand';
import TableDashboard from './table/dashboard-table';
import SpiffeHelper from '../spiffe-helper';
import TornjakHelper from 'components/tornjak-helper';

const columns = [
  { field: "spiffeid", headerName: "Name", flex: 1, renderCell: renderCellExpand },
  { field: "clusterName", headerName: "Cluster Name", width: 190 },
  { field: "numEntries", headerName: "Number of Entries", width: 200 },
  { field: "status", headerName: "Status", width: 120 },
  { field: "platformType", headerName: "Platform Type", width: 170 },
];

const styles = theme => ({
  seeMore: {
    marginTop: theme.spacing(3),
  },
});

class AgentDashboardTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
    };
    this.SpiffeHelper = new SpiffeHelper();
    this.TornjakHelper = new TornjakHelper();
  }

  agentList() {
    var filterByValue = [];
    const { filterByCluster, filterByAgentId } = this.props;
    let agentsList = [];
    if ((typeof this.props.globalEntries.globalEntriesList === 'undefined') ||
      (typeof this.props.globalAgents.globalAgentsList === 'undefined')) {
      return [];
    }
    agentsList = this.props.globalAgents.globalAgentsList.map(currentAgent => {
      return this.TornjakHelper.getDashboardAgentMetaData(currentAgent, this.props.globalEntries.globalEntriesList, this.props.globalAgents.globalAgentsList, this.props.globalAgents.globalAgentsWorkLoadAttestorInfo)
    })
    // For details page filtering data
    if (filterByCluster === undefined && filterByAgentId === undefined) {
      return agentsList;
    }
    for (let i = 0; i < agentsList.length; i++) {
      if ((filterByCluster !== undefined && agentsList[i].clusterName === filterByCluster) ||
        (filterByAgentId !== undefined && agentsList[i].id === filterByAgentId)) { // for filtering agents for a specific cluster or filtering agents for entries
        filterByValue.push(agentsList[i]);
      }
    }
    return filterByValue;
  }

  render() {
    const { numRows } = this.props;
    var data = this.agentList();
    return (
      <div>
        <TableDashboard
          title={"Agents"}
          numRows={numRows}
          columns={columns}
          data={data} />
      </div>
    );
  }

}

const mapStateToProps = (state) => ({
  globalAgents: state.agents,
  globalEntries: state.entries,
  globalClickedDashboardTable: state.tornjak.globalClickedDashboardTable,
})

export default withStyles(styles)(
  connect(mapStateToProps, {})(AgentDashboardTable)
)


<|start_filename|>tornjak-backend/api/manager/api.go<|end_filename|>
package managerapi

import (
	managertypes "github.com/spiffe/tornjak/tornjak-backend/pkg/manager/types"

	"github.com/pkg/errors"
	//types "github.com/spiffe/spire/proto/spire/types"
	//agent "github.com/spiffe/spire/proto/spire/api/server/agent/v1"
	//entry "github.com/spiffe/spire/proto/spire/api/server/entry/v1"
)

type ListServersRequest struct{}
type ListServersResponse managertypes.ServerInfoList

func (s *Server) ListServers(inp ListServersRequest) (*ListServersResponse, error) {

	resp, err := s.db.GetServers()
	if err != nil {
		return nil, err
	}
	for i := range resp.Servers {
		resp.Servers[i].Key = []byte{}
		resp.Servers[i].Cert = []byte{}
	}

	return (*ListServersResponse)(&resp), nil
}

type RegisterServerRequest managertypes.ServerInfo

func (s *Server) RegisterServer(inp RegisterServerRequest) error {
	sinfo := managertypes.ServerInfo(inp)
	if len(sinfo.Name) == 0 || len(sinfo.Address) == 0 {
		return errors.New("Server info missing mandatory fields")
	}

	return s.db.CreateServerEntry(sinfo)
}


<|start_filename|>tornjak-frontend/src/components/tornjak-server-info.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Accordion, AccordionItem, Tag } from "carbon-components-react";
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';
import {
  serverSelectedFunc,
  serverInfoUpdateFunc,
  tornjakServerInfoUpdateFunc,
  tornjakMessageFunc,
} from 'redux/actions';

const pluginTagColorMapper = {
  "NodeAttestor": "red",
  "WorkloadAttestor": "green",
  "KeyManager": "cyan",
  "NodeResolver": "blue",
  "Notifier": "teal",
  "DataStore": "purple",
}

const PluginTags = props => (
  <p>{props.names.map(v => <Tag type={pluginTagColorMapper[props.type]} >{props.type + ": " + v}</Tag>)}</p>
)
const TornjakServerInfoDisplay = props => (
  <Accordion>
    <AccordionItem title="Trust Domain" open>
      <p>
        {props.tornjakServerInfo.trustDomain}
      </p>
    </AccordionItem>
    <AccordionItem title="Plugins" open>
      <table>
        {
          Object.entries(props.tornjakServerInfo.plugins).map(([key, value]) =>
            <tr key={key + ":" + value}><PluginTags type={key} names={value} /></tr>)
        }
      </table>
    </AccordionItem>
    <AccordionItem title="Verbose Config (click to expand)">
      <pre>
        {props.tornjakServerInfo.verboseConfig}
      </pre>
    </AccordionItem>
  </Accordion>
)

class TornjakServerInfo extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.state = {};
  }

  componentDidMount() {
    if (IsManager) {
      if (this.props.globalServerSelected !== "") {
        this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
      }
    } else {
      this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
    }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc)
      }
    }
  }

  tornjakServerInfo() {
    if (this.props.globalTornjakServerInfo === "") {
      return ""
    } else {
      return <TornjakServerInfoDisplay tornjakServerInfo={this.props.globalTornjakServerInfo} />
    }
  }

  render() {
    return (
      <div>
        <h3>Server Info</h3>
        {this.props.globalErrorMessage !== "OK" &&
          <div className="alert-primary" role="alert">
            <pre>
              {this.props.globalErrorMessage}
            </pre>
          </div>
        }
        <br /><br />
        {this.tornjakServerInfo()}
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
  globalServerInfo: state.servers.globalServerInfo,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
  mapStateToProps,
  { serverSelectedFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, tornjakMessageFunc }
)(TornjakServerInfo)


<|start_filename|>tornjak-frontend/src/components/helpers.js<|end_filename|>
require('dotenv').config();
var urljoin = require('url-join');


// API_SERVER_URL
//console.log(process.env.API_SERVER_URI);
const ApiServerUri = process.env["REACT_APP_API_SERVER_URI"]

function GetApiServerUri (uri) {
    if (ApiServerUri) {
        return urljoin(ApiServerUri, uri)
    } else {
        return urljoin("/", uri)
    }
}

module.exports = GetApiServerUri;


// IS_MANAGER



<|start_filename|>tornjak-frontend/src/redux/reducers/serversReducer.js<|end_filename|>
import {
    GLOBAL_SERVER_SELECTED,
    GLOBAL_SERVER_INFO,
    GLOBAL_TORNJAK_SERVER_INFO,
    GLOBAL_SERVERS_LIST,
    GLOBAL_SELECTOR_INFO,
    GLOBAL_WORKLOAD_SELECTOR_INFO,
} from '../actions/types';

const initialState = {
    globalServerSelected: "",
    globalTornjakServerInfo: "",
    globalServerInfo: [],
    globalServersList: [],
    globalSelectorInfo:[],
    globalWorkloadSelectorInfo:[],
};

export default function serversReducer(state = initialState, action) {
    switch (action.type) {
        case GLOBAL_SERVER_SELECTED:
            return {
                ...state,
                globalServerSelected: action.payload
            };
        case GLOBAL_SERVER_INFO:
            return {
                ...state,
                globalServerInfo: action.payload
            };
        case GLOBAL_TORNJAK_SERVER_INFO:
            return {
                ...state,
                globalTornjakServerInfo: action.payload
            };
        case GLOBAL_SERVERS_LIST:
            return {
                ...state,
                globalServersList: action.payload
            };
        case GLOBAL_SELECTOR_INFO:
            return {
                ...state,
                globalSelectorInfo: action.payload
            };
        case GLOBAL_WORKLOAD_SELECTOR_INFO:
            return {
                ...state,
                globalWorkloadSelectorInfo: action.payload
            };
        default:
            return state;
    }
}


<|start_filename|>tornjak-frontend/src/components/tornjak-api-helpers.js<|end_filename|>
import { Component } from 'react';
import axios from 'axios';
import GetApiServerUri from './helpers';

class TornjakApi extends Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.registerSelectors = this.registerSelectors.bind(this);
    this.registerLocalSelectors = this.registerLocalSelectors.bind(this);
    this.refreshSelectorsState = this.refreshSelectorsState.bind(this);
    this.refreshLocalSelectorsState = this.refreshLocalSelectorsState.bind(this);
    this.populateTornjakServerInfo = this.populateTornjakServerInfo.bind(this);
    this.populateLocalTornjakServerInfo = this.populateLocalTornjakServerInfo.bind(this);
    this.populateServerInfo = this.populateServerInfo.bind(this);
    this.populateAgentsUpdate = this.populateAgentsUpdate.bind(this);
    this.populateLocalAgentsUpdate = this.populateLocalAgentsUpdate.bind(this);
    this.populateClustersUpdate = this.populateClustersUpdate.bind(this);
    this.populateLocalClustersUpdate = this.populateLocalClustersUpdate.bind(this);
  }

  registerSelectors = (serverName, wLoadAttdata, refreshSelectorsState, agentworkloadSelectorInfoFunc) => {
    axios.post(GetApiServerUri('/manager-api/tornjak/selectors/register/') + serverName, wLoadAttdata)
      .then(res => {
        //console.log(JSON.stringify(wLoadAttdata, null, ' ') + "\n\nSuccess:" + JSON.stringify(res.data, null, ' '));
        refreshSelectorsState(serverName, agentworkloadSelectorInfoFunc);
      }
      )
      .catch((error) => {
        console.log(error);
      })
  }

  registerLocalSelectors = (wLoadAttdata, refreshLocalSelectorsState, agentworkloadSelectorInfoFunc) => {
    //console.log(wLoadAttdata)
    axios.post(GetApiServerUri('/api/tornjak/selectors/register'), wLoadAttdata)
      .then(res => {
        //console.log(JSON.stringify(wLoadAttdata, null, ' ') + "\n\nSuccess:" + JSON.stringify(res.data, null, ' '));
        refreshLocalSelectorsState(agentworkloadSelectorInfoFunc);
      }
      )
      .catch((error) => {
        console.log(error);
      })
  }
  // refreshSelectorsState returns the list agent's with their workload plugin info for the selected server in manager mode
  // [
  //  "agent1workloadselectorinfo": [
  //      {
  //        "id": "agentid",
  //        "spiffeid": "agentspiffeeid",  
  //        "selectors": "agentworkloadselectors"
  //      }
  //    ],
  //    "agent2workloadselectorinfo": [
  //      {
  //        "id": "agentid",
  //        "spiffeid": "agentspiffeeid",  
  //        "selectors": "agentworkloadselectors"  
  //      }
  //    ]
  // ]
  refreshSelectorsState = (serverName, agentworkloadSelectorInfoFunc) => {
    axios.get(GetApiServerUri("/manager-api/tornjak/selectors/list/") + serverName, { crossdomain: true })
      .then(response => {
        //console.log(response.data);
        agentworkloadSelectorInfoFunc(response.data["agents"]);
      })
      .catch((error) => {
        console.log(error);
      })
  }
  // refreshLocalSelectorsState returns the list agent's with their workload plugin info for the local server
  // [
  //  "agent1workloadselectorinfo": [
  //      {
  //        "id": "agentid",
  //        "spiffeid": "agentspiffeeid",  
  //        "selectors": "agentworkloadselectors"
  //      }
  //    ],
  //    "agent2workloadselectorinfo": [
  //      {
  //        "id": "agentid",
  //        "spiffeid": "agentspiffeeid",  
  //        "selectors": "agentworkloadselectors"  
  //      }
  //    ]
  // ]
  refreshLocalSelectorsState = (agentworkloadSelectorInfoFunc) => {
    axios.get(GetApiServerUri("/api/tornjak/selectors/list"), { crossdomain: true })
      .then(response => {
        //console.log(response.data);
        agentworkloadSelectorInfoFunc(response.data["agents"]);
      })
      .catch((error) => {
        console.log(error);
      })
  }

  // populateTornjakAgentInfo returns tornjak info of requested agents including cluster name and selector
  populateTornjakAgentInfo = (serverName, agentworkloadSelectorInfoFunc, inputData) => {
    axios.post(GetApiServerUri("/manager-api/tornjak/agents/list/") + serverName, inputData,
              { crossdomain: true,
              })
      .then(response => {
        agentworkloadSelectorInfoFunc(response.data["agents"]);
      })
      .catch((error) => {
        console.log(error);
      })
  }

  // populateLocalTornjakAgentInfo returns tornjak info of requested agents including cluster name and selector
  populateLocalTornjakAgentInfo = (agentworkloadSelectorInfoFunc, inputData) => {
    axios.post(GetApiServerUri("/api/tornjak/agents/list"), inputData,
              { crossdomain: true,
              })
      .then(response => {
        agentworkloadSelectorInfoFunc(response.data["agents"])
      })
      .catch((error) => {
        console.log(error);
      })
  }

  // populateTornjakServerInfo returns the tornjak server info of the selected server in manager mode
  populateTornjakServerInfo = (serverName, tornjakServerInfoUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/manager-api/tornjak/serverinfo/') + serverName, { crossdomain: true })
      .then(response => {
        tornjakServerInfoUpdateFunc(response.data);
        tornjakMessageFunc(response.statusText);
      }).catch(error => {
        tornjakServerInfoUpdateFunc([]);
        tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message);
      });
  }

  // populateLocalTornjakServerInfo returns the torjak server info of the server in local mode
  populateLocalTornjakServerInfo = (tornjakServerInfoUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/api/tornjak/serverinfo'), { crossdomain: true })
      .then(response => {
        tornjakServerInfoUpdateFunc(response.data);
        tornjakMessageFunc(response.statusText);
      })
      .catch((error) => {
        tornjakMessageFunc("Error retrieving: " + error.message);
      })
  }

  // populateServerInfo returns the server trust domain and nodeAttestorPlugin
  populateServerInfo = (serverInfo, serverInfoUpdateFunc) => {
    //node attestor plugin
    if (serverInfo === "" || serverInfo === undefined) {
      return
    }
    if (serverInfo.plugins["NodeAttestor"].length === 0) {
      return
    }
    let nodeAtt = serverInfo.plugins["NodeAttestor"][0];
    let trustDomain = serverInfo.trustDomain;
    var reqInfo =
    {
      "data":
      {
        "trustDomain": trustDomain,
        "nodeAttestorPlugin": nodeAtt
      }
    }
    serverInfoUpdateFunc(reqInfo);
  }

  populateEntriesUpdate = (serverName, entriesListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/manager-api/entry/list/') + serverName, {crossdomain: true})
    .then(response => {
      if(!response.data["entries"]) {
        entriesListUpdateFunc([]);
      } else {entriesListUpdateFunc(response.data["entries"]);}
      tornjakMessageFunc(response.statusText);
    }).catch(error => {
      entriesListUpdateFunc([]);
      tornjakMessageFunc("Error retrieving " + serverName + " : " + error + (typeof (error.response) !== "undefined" ? ":" + error.response.data : ""));
    })
  }

  populateLocalEntriesUpdate = (entriesListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/api/entry/list'), {crossdomain: true})
    .then(response => {
      if(!response.data["entries"]) {
        entriesListUpdateFunc([]);
      } else {entriesListUpdateFunc(response.data["entries"]);}
      tornjakMessageFunc(response.statusText);
    }).catch(error => {
      console.log(error);
      entriesListUpdateFunc([]);
      tornjakMessageFunc(error.message);
    })
  }

  // populateAgentsUpdate returns the list of agents with their info in manager mode for the selected server
  populateAgentsUpdate = (serverName, agentsListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/manager-api/agent/list/') + serverName, { crossdomain: true })
      .then(response => {
        if(!response.data["agents"]) {
          agentsListUpdateFunc([]);
        } else {agentsListUpdateFunc(response.data["agents"]);}
        tornjakMessageFunc(response.statusText);
      }).catch(error => {
        agentsListUpdateFunc([]);
        tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message);
      });

  }

  // populateLocalAgentsUpdate - returns the list of agents with their info in Local mode for the server
  populateLocalAgentsUpdate = (agentsListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/api/agent/list'), { crossdomain: true })
      .then(response => {
        if(!response.data["agents"]) {
          agentsListUpdateFunc([]);
        } else {agentsListUpdateFunc(response.data["agents"]);}
        tornjakMessageFunc(response.statusText);
      })
      .catch((error) => {
        agentsListUpdateFunc([]);
        tornjakMessageFunc("Error retrieving: " + error.message);
      })
  }

  // populateClustersUpdate returns the list of clusters with their info in manager mode for the selected server
  populateClustersUpdate = (serverName, clustersListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/manager-api/tornjak/clusters/list/') + serverName, { crossdomain: true })
      .then(response => {
        clustersListUpdateFunc(response.data["clusters"]);
        tornjakMessageFunc(response.statusText);
      }).catch(error => {
        clustersListUpdateFunc([]);
        tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message);
      });
  }

  // populateLocalClustersUpdate - returns the list of clusters with their info in Local mode for the server
  populateLocalClustersUpdate = (clustersListUpdateFunc, tornjakMessageFunc) => {
    axios.get(GetApiServerUri('/api/tornjak/clusters/list'), { crossdomain: true })
      .then(response => {
        clustersListUpdateFunc(response.data["clusters"]);
        tornjakMessageFunc(response.statusText);
      })
      .catch((error) => {
        clustersListUpdateFunc([]);
        tornjakMessageFunc("Error retrieving: " + error.message);
      })
  }

}

export default TornjakApi;

<|start_filename|>tornjak-frontend/src/components/agent-create-join-token.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import GetApiServerUri from './helpers';
import IsManager from './is_manager';
import {
  serverSelectedFunc
} from 'redux/actions';

class CreateJoinToken extends Component {
  constructor(props) {
    super(props);

    this.onChangeName = this.onChangeName.bind(this);
    this.onChangeTtl = this.onChangeTtl.bind(this);
    this.onChangeToken = this.onChangeToken.bind(this);
    this.onChangeSpiffeId = this.onChangeSpiffeId.bind(this);
    this.onSubmit = this.onSubmit.bind(this);

    this.state = {
      name: "",
      ttl: 500,
      token: "",
      spiffeId: "",
      trustDomain: "",
      path: "",
      message: "",
      servers: [],
      selectedServer: "",
    }
  }

  
  componentDidMount() {
      if (IsManager) {
        if(this.props.globalServerSelected !== ""){
          this.setState({selectedServer: this.props.globalServerSelected});
        }
      } else {
        // agent doesnt need to do anything
        this.setState({})
      }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if(prevProps.globalServerSelected !== this.props.globalServerSelected){
        this.setState({selectedServer: this.props.globalServerSelected});
      }
    }
  }


  onChangeName(e) {
    this.setState({
      name: e.target.value
    });
  }

  onChangeTtl(e) {
    this.setState({
      ttl: Number(e.target.value)
    });
  }

  onChangeToken(e) {
    this.setState({
      token: e.target.value
    });
  }

  parseSpiffeId(sid) {
    if (sid.startsWith('spiffe://')) {
        var sub = sid.substr("spiffe://".length)
        var sp = sub.indexOf("/")
        if (sp > 0 && sp !== sub.length-1) {
            var trustDomain = sub.substr(0,sp);
            var path = sub.substr(sp);
            return [true, trustDomain, path];
        }
    }
    return [ false, "", "" ];
  }

  onChangeSpiffeId(e) {
    var sid = e.target.value;
    if (sid.length === 0) {
        this.setState({
            spiffeId: sid,
            trustDomain: "",
            path: "",
            message: "",
        });
        return
    }

    const [ validSpiffeId, trustDomain, path ] = this.parseSpiffeId(sid)
    if (validSpiffeId) {
        this.setState({
            message: "",
            spiffeId: sid,
            trustDomain: trustDomain,
            path: path,
        });
        return
    }
    // else invalid spiffe ID
    this.setState({
        spiffeId: sid,
        message: "Invalid Spiffe ID",
        trustDomain: "",
        path: "",
    });
    return
  }


  // Tag related things

  handleTagDelete(i) {
    const { tags } = this.state;
    this.setState({
      tags: tags.filter((tag, index) => index !== i),
    });
  }

  handleTagAddition(tag) {
    this.setState(state => ({ tags: [...state.tags, tag] }));
  }

  handleTagDrag(tag, currPos, newPos) {
    const tags = [...this.state.tags];
    const newTags = tags.slice();

    newTags.splice(currPos, 1);
    newTags.splice(newPos, 0, tag);

    // re-render
    this.setState({ tags: newTags });
  }

  getApiTokenEndpoint() {
      if (!IsManager) {
          return GetApiServerUri('/api/agent/createjointoken')
      } else if (IsManager && this.state.selectedServer !== "") {
          return GetApiServerUri('/manager-api/agent/createjointoken') + "/" + this.state.selectedServer
      } else {
          this.setState({message:"Error: No server selected"})
          return ""
      }


  }
  onSubmit(e) {
    e.preventDefault();

    if (this.state.spiffeId !== "") {
        const validSpiffeId = (this.parseSpiffeId(this.state.spiffeId))[0];
        if (!validSpiffeId) {
            this.setState({ message: "ERROR: invalid spiffe ID specified"});
            return
        }
    }
    var cjtData = {
        "ttl": this.state.ttl,
    };
    if (this.state.trustDomain !== "" && this.state.path !== "") {
        cjtData["trust_domain"] = this.state.trustDomain;
        cjtData["path"] = this.state.path;
    }
    if (this.state.token !== "") {
        cjtData["token"] = this.state.token;
    }
    let endpoint = this.getApiTokenEndpoint();
    if (endpoint === "") {
        return
    }
    axios.post(endpoint, cjtData)
      .then(res => this.setState({ message: "Request:" + JSON.stringify(cjtData,null, ' ')+ "\n\nSuccess:" + JSON.stringify(res.data, null, ' ')}))
      .catch(err => this.setState({ message: "ERROR:" + err + (typeof (err.response) !== "undefined" ? err.response.data : "")}))
    //window.location = '/';
  }

  render() {
    return (
      <div>
        <h3>Create New Agent Join Token</h3>
        <form onSubmit={this.onSubmit}>
          <div className="alert-primary" role="alert">
          <pre>
            {this.state.message}
          </pre>
          </div>
          {IsManager}
          <br/><br/>

          <div className="form-group">
            <label>Time to live (TTL): </label>
            <input type="number"
              required
              className="form-control"
              value={this.state.ttl}
              onChange={this.onChangeTtl}
            />
          </div>


          <div className="form-group">
            <label>Token (optional): i.e. <PASSWORD>-<PASSWORD></label>
            <input type="text"
              className="form-control"
              value={this.state.token}
              onChange={this.onChangeToken}
            />
          </div>

          <div className="form-group">
            <label>SPIFFE ID (optional): i.e. spiffe://example.org/sample/spiffe/id</label>
            <input type="text"
              className="form-control"
              value={this.state.spiffeId}
              onChange={this.onChangeSpiffeId}
            />
          </div>



          <div className="form-group">
            <input type="submit" value="Create Join Token" className="btn btn-primary" />
          </div>
        </form>
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
})

export default connect(
  mapStateToProps,
  { serverSelectedFunc }
)(CreateJoinToken)

<|start_filename|>tornjak-backend/pkg/manager/db/sqlite_test.go<|end_filename|>
package db

import (
	"os"
	"testing"

	"github.com/spiffe/tornjak/tornjak-backend/pkg/manager/types"
)

func cleanup() {
	os.Remove("./local-test-db")
}

func TestServerCreate(t *testing.T) {
	defer cleanup()
	db, err := NewLocalSqliteDB("./local-test-db")
	if err != nil {
		t.Fatal(err)
	}

	sList, err := db.GetServers()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Servers) > 0 {
		t.Fatal("Server list should initially be empty")
	}

	sinfo := types.ServerInfo{
		Name:    "my-server",
		Address: "http://localhost:10000",
	}

	err = db.CreateServerEntry(types.ServerInfo{
		Name:    "my-server",
		Address: "http://localhost:10000",
	})
	if err != nil {
		t.Fatal(err)
	}

	sList, err = db.GetServers()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Servers) != 1 || sList.Servers[0].Name != sinfo.Name {
		t.Fatal("Server list should initially be empty")
	}
}


<|start_filename|>tornjak-backend/api/agent/types.go<|end_filename|>
package api

// TornjakServerInfo provides insight into the configuration of the SPIRE server
// where the Tornjak Agent resides
type TornjakServerInfo struct {
	// Plugins is a map from plugin types to respective names of plugins configured
	Plugins map[string][]string `json:"plugins"`
	// TrustDomain specifies the trust domain of the SPIRE server configured with tornjak
	TrustDomain string `json:"trustDomain"`
	// Verbose config contains unstructure information on the config on the agent
	VerboseConfig string `json:"verboseConfig"`
}


<|start_filename|>tornjak-frontend/src/redux/reducers/clustersReducer.js<|end_filename|>
import {
    GLOBAL_CLUSTERS_LIST,
    GLOBAL_CLUSTER_TYPE_INFO,
} from '../actions/types';

const initialState = {
    globalClustersList: [],
    globalClusterTypeInfo: [],
};

export default function clustersReducer(state = initialState, action) {
    switch (action.type) {
        case GLOBAL_CLUSTERS_LIST:
            return {
                ...state,
                globalClustersList: action.payload
            };
        case GLOBAL_CLUSTER_TYPE_INFO:
            return {
                ...state,
                globalClusterTypeInfo: action.payload
            };
        default:
            return state;
    }
}

<|start_filename|>tornjak-frontend/src/redux/actions/types.js<|end_filename|>
//tornjak
export const GLOBAL_MESSAGE = 'GLOBAL_MESSAGE';
export const GLOBAL_CLICKED_DASHBOARD_TABLE = 'GLOBAL_CLICKED_DASHBOARD_TABLE';

//servers
export const GLOBAL_SERVER_SELECTED = 'GLOBAL_SERVER_SELECTED';
export const GLOBAL_SERVER_INFO = 'GLOBAL_SERVER_INFO';
export const GLOBAL_TORNJAK_SERVER_INFO = 'GLOBAL_TORNJAK_SERVER_INFO';
export const GLOBAL_SERVERS_LIST = 'GLOBAL_SERVERS_LIST';
export const GLOBAL_SELECTOR_INFO = 'GLOBAL_SELECTOR_INFO';
export const GLOBAL_WORKLOAD_SELECTOR_INFO = 'GLOBAL_WORKLOAD_SELECTOR_INFO';

//entries
export const GLOBAL_ENTRIES_LIST = 'GLOBAL_ENTRIES_LIST';
//agents
export const GLOBAL_AGENTS_LIST = 'GLOBAL_AGENTS_LIST';
export const GLOBAL_AGENTS_WORKLOADATTESTOR_INFO = 'GLOBAL_AGENTS_WORKLOADATTESTOR_INFO'

//clusters
export const GLOBAL_CLUSTERS_LIST = 'GLOBAL_CLUSTERS_LIST';
export const GLOBAL_CLUSTER_TYPE_INFO = 'GLOBAL_CLUSTER_TYPE_INFO';


<|start_filename|>tornjak-backend/api/manager/server.go<|end_filename|>
package managerapi

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gorilla/mux"
	managerdb "github.com/spiffe/tornjak/tornjak-backend/pkg/manager/db"
)

var (
	jsonContentType string = "application/json"
)

type Server struct {
	listenAddr string
	db         managerdb.ManagerDB
}

// Handle preflight checks
func corsHandler(f func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "OPTIONS" {
			cors(w, r)
			return
		} else {
			f(w, r)
		}
	}
}

func cors(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=ascii")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type,access-control-allow-origin, access-control-allow-headers")
	w.WriteHeader(http.StatusOK)
}

func retError(w http.ResponseWriter, emsg string, status int) {
	w.Header().Set("Content-Type", "text/html; charset=ascii")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type,access-control-allow-origin, access-control-allow-headers")
	http.Error(w, emsg, status)
}

func copyHeader(dst, src http.Header) {
	for k, vv := range src {
		for _, v := range vv {
			dst.Add(k, v)
		}
	}
}

// Returns a post proxy function for tornjak api, where path is the path from the base URL, i.e. "/api/entry/delete"
func (s *Server) apiServerProxyFunc(apiPath string) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		serverName := vars["server"]

		fmt.Println(serverName)

		// Get server info
		sinfo, err := s.db.GetServer(serverName)
		if err != nil {
			emsg := fmt.Sprintf("Error getting server info: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
		fmt.Printf("%+v\n", sinfo)

		client, err := sinfo.HttpClient()
		if err != nil {
			emsg := fmt.Sprintf("Error initializing server client: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
		resp, err := client.Post(strings.TrimSuffix(sinfo.Address, "/")+apiPath, jsonContentType, r.Body)
		if err != nil {
			emsg := fmt.Sprintf("Error making api call to server: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
		defer resp.Body.Close()
		copyHeader(w.Header(), resp.Header)
		w.WriteHeader(resp.StatusCode)
		_, err = io.Copy(w, resp.Body)
		if err != nil {
			emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
	}
}

// spaHandler implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type spaHandler struct {
	staticPath string
	indexPath  string
}

// ServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// get the absolute path to prevent directory traversal
	path, err := filepath.Abs(r.URL.Path)
	if err != nil {
		// if we failed to get the absolute path respond with a 400 bad request
		// and stop
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// prepend the path with the path to the static directory
	path = filepath.Join(h.staticPath, path)

	// check whether a file exists at the given path
	_, err = os.Stat(path)
	if os.IsNotExist(err) {
		// file does not exist, serve index.html
		http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
		return
	} else if err != nil {
		// if we got an error (that wasn't that the file doesn't exist) stating the
		// file, return a 500 internal server error and stop
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	// otherwise, use http.FileServer to serve the static dir
	http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

func (s *Server) HandleRequests() {
	// TO implement
	rtr := mux.NewRouter()

	// Manger-specific
	rtr.HandleFunc("/manager-api/server/list", corsHandler(s.serverList))
	rtr.HandleFunc("/manager-api/server/register", corsHandler(s.serverRegister))

	// Entries
	rtr.HandleFunc("/manager-api/entry/list/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/entry/list")))
	rtr.HandleFunc("/manager-api/entry/delete/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/entry/delete")))
	rtr.HandleFunc("/manager-api/entry/create/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/entry/create")))

	// Agents
	rtr.HandleFunc("/manager-api/agent/list/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/agent/list")))
	rtr.HandleFunc("/manager-api/agent/delete/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/agent/delete")))
	rtr.HandleFunc("/manager-api/agent/ban/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/agent/ban")))
	rtr.HandleFunc("/manager-api/agent/createjointoken/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/agent/createjointoken")))

	// Tornjak-specific
	rtr.HandleFunc("/manager-api/tornjak/serverinfo/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/serverinfo")))
	// Agents Selectors
	rtr.HandleFunc("/manager-api/tornjak/selectors/register/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/selectors/register")))
	rtr.HandleFunc("/manager-api/tornjak/selectors/list/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/selectors/list")))
	rtr.HandleFunc("/manager-api/tornjak/agents/list/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/agents/list")))
	// Agents Clusters
	rtr.HandleFunc("/manager-api/tornjak/clusters/create/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/clusters/create")))
	rtr.HandleFunc("/manager-api/tornjak/clusters/edit/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/clusters/edit")))
	rtr.HandleFunc("/manager-api/tornjak/clusters/list/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/clusters/list")))
	rtr.HandleFunc("/manager-api/tornjak/clusters/delete/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/tornjak/clusters/delete")))

	//http.HandleFunc("/manager-api/get-server-info", s.agentList)
	//http.HandleFunc("/manager-api/agent/list/:id", s.agentList)

	spa := spaHandler{staticPath: "ui-manager", indexPath: "index.html"}
	rtr.PathPrefix("/").Handler(spa)

	fmt.Println("Starting to listen...")
	log.Fatal(http.ListenAndServe(s.listenAddr, rtr))
}

/*

func main() {
  rtr := mux.NewRouter()
  rtr.HandleFunc("/number/{id:[0-9]+}", pageHandler)
  http.Handle("/", rtr)
  http.ListenAndServe(PORT, nil)
}
*/

// NewManagerServer returns a new manager server, given a listening address for the
// server, and a DB connection string
func NewManagerServer(listenAddr, dbString string) (*Server, error) {
	db, err := managerdb.NewLocalSqliteDB(dbString)
	if err != nil {
		return nil, err
	}
	return &Server{
		listenAddr: listenAddr,
		db:         db,
	}, nil
}

func (s *Server) serverList(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Endpoint Hit: Server List")

	buf := new(strings.Builder)

	n, err := io.Copy(buf, r.Body)
	if err != nil {
		emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}
	data := buf.String()

	var input ListServersRequest
	if n == 0 {
		input = ListServersRequest{}
	} else {
		err := json.Unmarshal([]byte(data), &input)
		if err != nil {
			emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
	}

	ret, err := s.ListServers(input)
	if err != nil {
		emsg := fmt.Sprintf("Error: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}
	cors(w, r)

	je := json.NewEncoder(w)
	err = je.Encode(ret)

	if err != nil {
		emsg := fmt.Sprintf("Error: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}
}

func (s *Server) serverRegister(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Endpoint Hit: Server Create")

	buf := new(strings.Builder)

	n, err := io.Copy(buf, r.Body)
	if err != nil {
		emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}
	data := buf.String()

	var input RegisterServerRequest
	if n == 0 {
		input = RegisterServerRequest{}
	} else {
		err := json.Unmarshal([]byte(data), &input)
		if err != nil {
			emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
			retError(w, emsg, http.StatusBadRequest)
			return
		}
	}

	err = s.RegisterServer(input)
	if err != nil {
		emsg := fmt.Sprintf("Error: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}

	cors(w, r)
	_, err = w.Write([]byte("SUCCESS"))

	if err != nil {
		emsg := fmt.Sprintf("Error: %v", err.Error())
		retError(w, emsg, http.StatusBadRequest)
		return
	}
}


<|start_filename|>tornjak-frontend/src/components/dashboard/entries-dashboard-table.js<|end_filename|>
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import renderCellExpand from './render-cell-expand';
import TableDashboard from './table/dashboard-table';
import SpiffeHelper from '../spiffe-helper';
import TornjakHelper from 'components/tornjak-helper';

const columns = [
  { field: "id", headerName: "ID", width: 170, renderCell: renderCellExpand },
  { field: "spiffeid", headerName: "Name", width: 170, renderCell: renderCellExpand },
  { field: "parentId", headerName: "Parent ID", width: 170, renderCell: renderCellExpand },
  { field: "clusterName", headerName: "Cluster Name", width: 170 },
  { field: "entryExpireTime", headerName: "Entry Expire Time", width: 190 },
  { field: "platformType", headerName: "Platform Type", width: 170 },
  { field: "adminFlag", headerName: "Admin Flag", width: 150, type: 'boolean'},
];

const styles = (theme => ({
  seeMore: {
    marginTop: theme.spacing(3),
  },
}));

class EntriesDashBoardTable extends React.Component {
  constructor(props) {
    super(props)
    this.SpiffeHelper = new SpiffeHelper();
    this.TornjakHelper = new TornjakHelper();
  }

  entryList() {
    var filterByValue = [];
    const { filterByCluster, filterByAgentId } = this.props;
    let entriesList = [];
    if ((typeof this.props.globalEntries.globalEntriesList === 'undefined') ||
      (typeof this.props.globalAgents.globalAgentsList === 'undefined')) {
      return [];
    }

    entriesList = this.props.globalEntries.globalEntriesList.map(currentEntry => {
      return this.TornjakHelper.workloadEntry(currentEntry, this.props.globalAgents.globalAgentsWorkLoadAttestorInfo, this.props.globalAgents.globalAgentsList, this.props.globalEntries.globalEntriesList);
    })

    // For details page filtering data
    if (filterByCluster === undefined && filterByAgentId === undefined) {
      return entriesList;
    }

    for (let i = 0; i < entriesList.length; i++) {
      if ((filterByCluster !== undefined && entriesList[i].clusterName === filterByCluster) ||
        (filterByAgentId !== undefined && entriesList[i].parentId === filterByAgentId) ||
        (filterByAgentId !== undefined && entriesList[i].canonicalAgentId=== filterByAgentId)
      ) {
        filterByValue.push(entriesList[i]);
      }
    }
    return filterByValue;
  }

  render() {
    const { numRows } = this.props;
    var data = this.entryList();
    return (
      <div>
        <TableDashboard
          title={"Entries"}
          numRows={numRows}
          columns={columns}
          data={data} />
      </div>
    );
  }
}

const mapStateToProps = state => ({
  globalAgents: state.agents,
  globalEntries: state.entries,
  globalClickedDashboardTable: state.tornjak.globalClickedDashboardTable,
})

export default withStyles(styles)(
  connect(mapStateToProps, {})(EntriesDashBoardTable)
)


<|start_filename|>tornjak-backend/pkg/agent/db/sqlite_errors.go<|end_filename|>
package db

import (
	"fmt"
)

// SQLError is an error where the input appears correct but the database acts up
type SQLError struct {
	Cmd string
	Err error
}

func (e SQLError) Error() string {
	return fmt.Sprintf("Unable to execute SQL query %v: %v", e.Cmd, e.Err.Error())
}

// GetError is an error intended to signify something wrong with a get request
// For example, non-existence
type GetError struct {
	Message string
}

func (e GetError) Error() string {
	return e.Message
}

// PostFailure is meant to signify when the state of the database has not changed
type PostFailure struct {
	Message string
}

func (e PostFailure) Error() string {
	return e.Message
}


<|start_filename|>tornjak-backend/pkg/agent/db/sqlite_test.go<|end_filename|>
package db

import (
	"fmt"
	"github.com/pkg/errors"
	"os"
	"testing"
	"time"

	"github.com/cenkalti/backoff/v4"

	"github.com/spiffe/tornjak/tornjak-backend/pkg/agent/types"
)

func cleanup() {
	os.Remove("./local-agentstest-db")
}

// TestSelectorDB checks correctness of functions dealing with Agent Selector table
// Uses functions NewLocalSqliteDB, db.CreateAgentsEntry, db.GetAgentSelectors, db.GetAgentPluginInfo
func TestSelectorDB(t *testing.T) {
	defer cleanup()
	expBackoff := backoff.NewExponentialBackOff()
	expBackoff.MaxElapsedTime = time.Second
	db, err := NewLocalSqliteDB("./local-agentstest-db", expBackoff)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK initial emptiness [GetAgentSelectors]
	sList, err := db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) > 0 {
		t.Fatal("Agents list should initially be empty")
	}

	spiffeid := "spiffe://example.org/spire/agent/"
	spiffeidA := "spiffeA"
	sinfo := types.AgentInfo{
		Spiffeid: spiffeid,
		Plugin:   "Docker",
	}
	sinfoNew := types.AgentInfo{
		Spiffeid: spiffeid,
		Plugin:   "K8s",
	}
	sinfoANull := types.AgentInfo{
		Spiffeid: spiffeidA,
	}
	sinfoANotNull := types.AgentInfo{
		Spiffeid: spiffeidA,
		Plugin:   "K8s",
	}

	// ATTEMPT registration of agent plugin [CreateAgentEntry]]
	err = db.CreateAgentEntry(sinfo)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK new agent plugin [GetAgentSelectors]
	sList, err = db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 || !agentInfoCmp(sList.Agents[0], sinfo) {
		t.Fatal("Agents list should initially be empty")
	}

	// CHECK get agent plugin of existing agent [GetAgentPluginInfo]
	info, err := db.GetAgentPluginInfo(spiffeid)
	if err != nil {
		t.Fatal(err)
	}
	if !agentInfoCmp(info, sinfo) {
		t.Fatal("Wrong info obtained from GetAgentPluginInfo")
	}

	// CHECK get agent plugin of nonexisting agent [GetAgentPluginInfo]
	_, err = db.GetAgentPluginInfo("super secret agent")
	if err == nil {
		t.Fatal("Failed to report non-existing agent in GetAgentPluginInfo")
	}

	// ATTEMPT editing registration of agent plugin [CreateAgentEntry]
	err = db.CreateAgentEntry(sinfoNew)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK new agent plugin [GetAgentSelectors]
	sList, err = db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 {
		t.Fatal("There should only be one agent")
	}
	if !agentInfoCmp(sList.Agents[0], sinfoNew) {
		t.Fatal(fmt.Sprintf("Wrong agent info stored after edit: wanted %v, got %v", sinfoNew, sList.Agents[0]))
	}

	// ATTEMPT adding new agent with no plugin [CreateAgentEntry]
	err = db.CreateAgentEntry(sinfoANull)
	if err != nil {
		t.Fatal(fmt.Sprintf("Cannot add agent with no plugin, got error: %v", err))
	}

	// CHECK all agents with plugins; should only have 1 [GetAgentSelectors]
	sList, err = db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 {
		t.Fatal(fmt.Sprintf("There should only be one agent %v", sList.Agents))
	}
	if !agentInfoCmp(sList.Agents[0], sinfoNew) {
		t.Fatal("Wrong agent info stored after edit")
	}

	// ATTEMPT updating agent from NULL to new plugin [CreateAgentEntry]
	err = db.CreateAgentEntry(sinfoANotNull)
	if err != nil {
		t.Fatal("Cannot add agent with no plugin")
	}

	// CHECK all agents with plugins; should have 2 [GetAgentSelectors]
	sList, err = db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 2 {
		t.Fatal("There should only be one agent")
	}

	// ATTEMPT updating agent from NOT NULL to NULL plugin [CreateAgentEntry]
	err = db.CreateAgentEntry(sinfoANull)
	if err != nil {
		t.Fatal("Cannot add agent with no plugin")
	}

	// CHECK all agents with plugins; should only have 1 [GetAgentSelectors]
	sList, err = db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 {
		t.Fatal("There should only be one agent")
	}
	if !agentInfoCmp(sList.Agents[0], sinfoNew) {
		t.Fatal("Wrong agent info stored after edit")
	}

	// CHECK agent metadata [GetAgentsMetadata]
	req1 := types.AgentMetadataRequest{
		Agents: []string{"spiffeA"},
	}
	req2 := types.AgentMetadataRequest{
		Agents: []string{},
	}
	req3 := types.AgentMetadataRequest{
		Agents: []string{"nonexistent spiffe"},
	}
	sList, err = db.GetAgentsMetadata(req1)
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 {
		t.Fatal(fmt.Sprintf("We requested one agent, got: %v", sList))
	}
	sList, err = db.GetAgentsMetadata(req2)
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 2 {
		t.Fatal(fmt.Sprintf("We requested all agents, got: %v", sList))
	}
	sList, err = db.GetAgentsMetadata(req3)
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 0 {
		t.Fatal(fmt.Sprintf("We requested nonexistent agent, got: %v", sList))
	}
}

// TestClusterCreate checks edge cases involving CreateClusterEntry
// Uses functions NewLocalSqliteDB, db.GetClusters, db.CreateClusterEntry,
//                db.GetAgentClusterName, db.GetClusterAgents
func TestClusterCreate(t *testing.T) {
	cleanup()
	defer cleanup()
	expBackoff := backoff.NewExponentialBackOff()
	expBackoff.MaxElapsedTime = time.Second
	db, err := NewLocalSqliteDB("./local-agentstest-db", expBackoff)
	if err != nil {
		t.Fatal(err)
	}

	// CHECKS no clusters initially present [GetClusters]
	cListObject, err := db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList := cListObject.Clusters
	if len(cList) > 0 {
		t.Fatal("Clusters list should initially be empty")
	}

	cluster1 := "cluster1"
	cluster2 := "cluster2"
	cluster3 := "cluster3"
	vms := "VMs"
	k8s := "Kubernetes"
	agent1 := "agent1"
	agent2 := "agent2"
	agent3 := "agent3"
	agent4 := "agent4"

	sinfo := types.AgentInfo{
		Spiffeid: agent1,
		Plugin:   "Docker",
	}

	cinfo1 := types.ClusterInfo{
		Name:         cluster1,
		PlatformType: vms,
		AgentsList:   []string{agent1, agent2},
	}
	cinfo1a := types.ClusterInfo{
		Name:         cluster1,
		PlatformType: k8s,
		AgentsList:   []string{agent1},
	}
	cinfo2 := types.ClusterInfo{
		Name:         cluster2,
		PlatformType: vms,
		AgentsList:   []string{agent2, agent4},
	}
	cinfo3 := types.ClusterInfo{
		Name:         cluster3,
		PlatformType: k8s,
		AgentsList:   []string{agent3},
	}

	// CHECK GetClusterAgents with nonexistent cluster [GetClusterAgents]
	_, err = db.GetClusterAgents(cluster1)
	if err == nil {
		t.Fatal("Cannot get agents from nonexistent cluster")
	}
	_, ok := err.(GetError)
	if !ok {
		t.Fatal("Non-get error")
	}

	// ATTEMPT Creating cluster [CreateClusterEntry, GetClusters]
	err = db.CreateClusterEntry(cinfo1)
	if err != nil {
		t.Fatal(err)
	}

	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList = cListObject.Clusters
	if len(cList) != 1 || !clusterEquality(cList[0], cinfo1) {
		t.Fatal("Clusters list after 1 insertion has incorrect cluster")
	}

	// ATTEMPT Create with already existing agent; should fail [CreateClusterEntry]
	err = db.CreateClusterEntry(cinfo1a)
	if err == nil {
		t.Fatal("Failure to report error on cluster create of existing cluster")
	}
	_, ok = err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error on cluster create of existing cluster: %v", err.Error()))
	}

	// ATTEMPT Create with no conflicting agent assignment [CreateClusterEntry, GetClusters]
	err = db.CreateClusterEntry(cinfo3)
	if err != nil {
		t.Fatal(err)
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo1, cinfo3}}) != nil {
		t.Fatal("Clusters list after 2 good insertions does not have 2 correct clusters")
	}

	// ATTEMPT Create with conflicting agent assignment; should fail [CreateClusterEntry]
	err = db.CreateClusterEntry(cinfo2)
	if err == nil {
		t.Fatal("Failure to report failure to assign already assigned agent")
	}
	_, ok = err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error on agent assignment: %v", err.Error()))
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo1, cinfo3}}) != nil {
		t.Fatal("Clusters list after 2 good insertions does not have 2 correct clusters")
	}

	// FINAL CHECK agent memberships; want 2 in cluster 1, 1 in cluster 2, 1 in cluster 3
	// [GetClusterAgents]
	agents1, err := db.GetClusterAgents(cluster1)
	if err != nil {
		t.Fatal(err)
	}
	_, err = db.GetClusterAgents(cluster2)
	if err == nil {
		t.Fatal("should not be able to get cluster agents of unsuccessfully assigned cluster")
	}
	agents3, err := db.GetClusterAgents(cluster3)
	if err != nil {
		t.Fatal(err)
	}
	err = agentListComp(agents1, []string{agent1, agent2})
	if err != nil {
		t.Fatal(fmt.Sprintf("Error on basic registration of agents to cluster: %v", err))
	}
	err = agentListComp(agents3, []string{agent3})
	if err != nil {
		t.Fatal(fmt.Sprintf("Error on basic registration of agents to cluster: %v", err))
	}

	// ATTEMPT editing registration of agent plugin [CreateAgentEntry]
	err = db.CreateAgentEntry(sinfo)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK new agent plugin [GetAgentSelectors]
	sList, err := db.GetAgentSelectors()
	if err != nil {
		t.Fatal(err)
	}
	if len(sList.Agents) != 1 {
		t.Fatal("There should only be one agent")
	}
	if !agentInfoCmp(sList.Agents[0], sinfo) {
		t.Fatal(fmt.Sprintf("Wrong agent info stored after edit: wanted %v, got %v", sinfo, sList.Agents[0]))
	}

	// FINAL CHECK agent memberships [GetAgentClusterName]
	agent1Cluster, err := db.GetAgentClusterName(agent1)
	if err != nil {
		t.Fatal(err)
	}
	agent2Cluster, err := db.GetAgentClusterName(agent2)
	if err != nil {
		t.Fatal(err)
	}
	agent3Cluster, err := db.GetAgentClusterName(agent3)
	if err != nil {
		t.Fatal(err)
	}
	agent4Cluster, err := db.GetAgentClusterName(agent4)
	if err == nil {
		t.Fatal("agent4 should not be assigned")
	}
	if agent1Cluster != cluster1 {
		t.Fatal("agent1 not in cluster1")
	}
	if agent2Cluster != cluster1 {
		t.Fatal("agent2 not in cluster1")
	}
	if agent3Cluster != cluster3 {
		t.Fatal("agent3 not in cluster3")
	}
	if agent4Cluster == cluster2 {
		t.Fatal("agent4 in cluster2")
	}

}

// TestClusterEdit checks edge cases involving EditClusterEntry
// uses NewLocalSqliteDB, db.CreateClusterEntry, db.EditClusterEntry,
//      db.GetAgentClusterName, db.GetClusterAgents
func TestClusterEdit(t *testing.T) {
	defer cleanup()
	expBackoff := backoff.NewExponentialBackOff()
	expBackoff.MaxElapsedTime = time.Second
	db, err := NewLocalSqliteDB("./local-agentstest-db", expBackoff)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK initial emptiness of cluster list
	cListObject, err := db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList := cListObject.Clusters
	if len(cList) > 0 {
		t.Fatal("Clusters list should initially be empty")
	}

	cluster1 := "cluster1"
	cluster2 := "cluster2"
	cluster3 := "cluster3"
	cluster4 := "cluster4"
	vms := "VMs"
	k8s := "Kubernetes"
	agent1 := "agent1"
	agent2 := "agent2"
	agent3 := "agent3"
	agent4 := "agent4"

	cinfo1 := types.ClusterInfo{
		Name:         cluster1,
		EditedName:   cluster1,
		PlatformType: vms,
		AgentsList:   []string{agent1, agent2},
	}
	cinfo1New := types.ClusterInfo{
		Name:         cluster1,
		EditedName:   cluster1,
		PlatformType: k8s,
		ManagedBy:    "MaiaIyer",
		AgentsList:   []string{agent1, agent3},
	}
	cinfo2 := types.ClusterInfo{
		Name:         cluster2,
		EditedName:   cluster2,
		PlatformType: vms,
		AgentsList:   []string{agent2, agent4},
	}
	cinfo3to4 := types.ClusterInfo{
		Name:         cluster3,
		EditedName:   cluster4,
		PlatformType: k8s,
		AgentsList:   []string{agent2},
	}
	cinfo1to3 := types.ClusterInfo{
		Name:         cluster1,
		EditedName:   cluster3,
		PlatformType: k8s,
		AgentsList:   []string{agent1},
	}
	cinfo3to2 := types.ClusterInfo{
		Name:         cluster3,
		EditedName:   cluster2,
		PlatformType: k8s,
		AgentsList:   []string{agent1},
	}
	cinfo3 := types.ClusterInfo{
		Name:         cluster3,
		EditedName:   cluster3,
		PlatformType: k8s,
		AgentsList:   []string{agent1},
	}

	// ATTEMPT CreateClusterEntry [CreateClusterEntry, GetClusters]
	err = db.CreateClusterEntry(cinfo1)
	if err != nil {
		t.Fatal(err)
	}

	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList = cListObject.Clusters
	if len(cList) != 1 || !clusterEquality(cList[0], cinfo1) {
		t.Fatal("Clusters list after 1 insertion has incorrect cluster")
	}

	// CHECK [GetClusterAgents]
	agents, err := db.GetClusterAgents("cluster1")
	if err != nil {
		t.Fatal(err)
	}
	err = agentListComp(agents, []string{agent1, agent2})
	if err != nil {
		t.Fatal(fmt.Sprintf("Error on basic registration of agents to cluster: %v", err))
	}

	// ATTEMPT normal EditClusterEntry [EditClusterEntry, GetClusters, GetClusterAgents]
	err = db.EditClusterEntry(cinfo1New)
	if err != nil {
		t.Fatal(err)
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo1New}}) != nil {
		t.Fatal("Clusters list after 1 good insertion and edit does not have 1 correct cluster")
	}

	// ATTEMPT EditClusterEntry on non-existent cluster; should fail [EditClusterEntry]
	err = db.EditClusterEntry(cinfo2)
	if err == nil {
		t.Fatal("Failed to report edit of nonexisting cluster")
	}
	_, ok := err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error returned on editing nonexisting cluster: %v", err.Error()))
	}

	// ATTEMPT EditClusterEntry with already assigned agent; should fail [CreateClusterEntry, EditClusterEntry]
	err = db.CreateClusterEntry(cinfo2)
	if err != nil {
		t.Fatal(err)
	}
	err = db.EditClusterEntry(cinfo1)
	if err == nil {
		t.Fatal("Failed to report failure of agent assignment already taken")
	}
	_, ok = err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error on assignment of already assigned agent: %v", err.Error()))
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo2, cinfo1New}}) != nil {
		t.Fatal("Clusters list does not have correct clusters")
	}

	// CHECK agent memberships; want cluster1 to have agents 1 and 3 and cluster2 to have agents 2 and 4
	// [GetAgentClusterName]
	agent1Cluster, err := db.GetAgentClusterName(agent1)
	if err != nil {
		t.Fatal(err)
	}
	agent2Cluster, err := db.GetAgentClusterName(agent2)
	if err != nil {
		t.Fatal(err)
	}
	agent3Cluster, err := db.GetAgentClusterName(agent3)
	if err != nil {
		t.Fatal(err)
	}
	agent4Cluster, err := db.GetAgentClusterName(agent4)
	if err != nil {
		t.Fatal(err)
	}
	if agent1Cluster != cluster1 {
		t.Fatal("agent1 not in cluster1")
	}
	if agent2Cluster != cluster2 {
		t.Fatal("agent2 not in cluster1")
	}
	if agent3Cluster != cluster1 {
		t.Fatal("agent3 not in cluster1")
	}
	if agent4Cluster != cluster2 {
		t.Fatal("agent4 not in cluster2")
	}

	// TEST Renaming of cluster that exists to cluster that does not exist; should succeed
	err = db.EditClusterEntry(cinfo1to3)
	if err != nil {
		t.Fatal(err)
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo2, cinfo3}}) != nil {
		t.Fatal("Clusters list does not have correct clusters")
	}

	// TEST Renaming of cluster that exists with conflicting new agents; should fail
	err = db.EditClusterEntry(cinfo3to4)
	if err == nil {
		t.Fatal("Renamed edit cluster should throw error with conflicting new agents")
	}
	_, ok = err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error on assignment of already assigned agent: %v", err.Error()))
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo2, cinfo3}}) != nil {
		t.Fatal("Clusters list does not have correct clusters")
	}

	// TEST Renaming of cluster that exists to cluster that exists; should fail
	err = db.EditClusterEntry(cinfo3to2)
	if err == nil {
		t.Fatal("Renamed edit cluster should throw error when renaming to cluster that exists")
	}
	_, ok = err.(PostFailure)
	if !ok {
		t.Fatal(fmt.Sprintf("Wrong error on renaming to existing cluster: %v", err.Error()))
	}
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	if clustersComp(cListObject, types.ClusterInfoList{Clusters: []types.ClusterInfo{cinfo2, cinfo3}}) != nil {
		t.Fatal("Clusters list does not have correct clusters")
	}

}

// TestClusterDelete checks edge cases on DeleteClusterEntry
// uses NewLocalSqliteDB, db.GetClusters, db.CreateClusterEntry, db.EditClusterEntry
//      db.DeleteClusterEntry, db.GetAgentClusterName, db.GetClusterAgents
func TestClusterDelete(t *testing.T) {
	defer cleanup()
	expBackoff := backoff.NewExponentialBackOff()
	expBackoff.MaxElapsedTime = time.Second
	db, err := NewLocalSqliteDB("./local-agentstest-db", expBackoff)
	if err != nil {
		t.Fatal(err)
	}

	// CHECK initial emptiness of cluster list
	cListObject, err := db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList := cListObject.Clusters
	if len(cList) > 0 {
		t.Fatal("Clusters list should initially be empty")
	}

	cluster1 := "cluster1"
	cluster2 := "cluster2"
	vms := "VMs"
	k8s := "Kubernetes"
	agent1 := "agent1"
	agent2 := "agent2"
	agent3 := "agent3"
	agent4 := "agent4"

	cinfo1 := types.ClusterInfo{
		Name:         cluster1,
		EditedName:   cluster1,
		PlatformType: vms,
		AgentsList:   []string{agent1, agent2},
	}
	cinfo1New := types.ClusterInfo{
		Name:         cluster1,
		EditedName:   cluster1,
		PlatformType: vms,
		AgentsList:   []string{},
	}
	cinfo2 := types.ClusterInfo{
		Name:         cluster2,
		EditedName:   cluster2,
		PlatformType: k8s,
		AgentsList:   []string{agent3, agent4},
	}

	// ATTEMPT basic CreateClusterEntry [CreateClusterEntry, GetClusters]
	err = db.CreateClusterEntry(cinfo1)
	if err != nil {
		t.Fatal(err)
	}

	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList = cListObject.Clusters
	if len(cList) != 1 || cList[0].Name != cinfo1.Name {
		t.Fatal("Clusters list after 1 insertion should have 1 cluster")
	}

	// CHECK GetClusterAgents [GetClusterAgents]
	agents, err := db.GetClusterAgents(cluster1)
	if err != nil {
		t.Fatal(err)
	}
	err = agentListComp(agents, []string{agent1, agent2})
	if err != nil {
		t.Fatal(fmt.Sprintf("Error on basic registration of agents to cluster: %v", err))
	}

	// TEST Edit with Removing Entries [EditClusterEntry, GetClusterAgents]
	err = db.EditClusterEntry(cinfo1New)
	if err != nil {
		t.Fatal(err)
	}
	agents, err = db.GetClusterAgents(cluster1)
	if err != nil {
		t.Fatal(err)
	}
	if len(agents) != 0 {
		t.Fatal("EditClusterEntry cannot remove all agents")
	}
	_, err = db.GetAgentClusterName(agent1)
	if err == nil {
		t.Fatal("Agent1 not successfully unassigned")
	}

	// TEST DeleteClusterEntry on nonexistent cluster [DeleteClusterEntry]
	err = db.DeleteClusterEntry(cluster2)
	if err == nil {
		t.Fatal("Failure to report cluster does not exist")
	}

	// SETUP cluster with agents [CreateClusterEntry]
	err = db.CreateClusterEntry(cinfo2)
	if err != nil {
		t.Fatal(err)
	}

	// TEST DeleteClusterEntry on existing cluster with no agents [DeleteClusterEntry, GetClusterAgents]
	err = db.DeleteClusterEntry(cluster1)
	if err != nil {
		t.Fatal(err)
	}
	_, err = db.GetClusterAgents(cluster1)
	if err == nil {
		t.Fatal("Failure to report cluster does not exist")
	}

	// TEST DeleteClusterEntry on existing cluster with agents [DeleteClusterEngry]
	err = db.DeleteClusterEntry(cluster2)
	if err != nil {
		t.Fatal(err)
	}
	// CHECK agent previously assigned is unassigned with GetError [GetAgentClusterName]
	agent3Cluster, err := db.GetAgentClusterName(agent3)
	if err == nil {
		t.Fatal("Failure to report cluster does not exist")
	}
	_, ok := err.(GetError)
	if !ok {
		t.Fatal("incorrect failure")
	}
	if agent3Cluster != "" {
		t.Fatal("Agent3 not successfully unassigned")
	}
	// FINAL CHECK should have no clusters [GetClusters]
	cListObject, err = db.GetClusters()
	if err != nil {
		t.Fatal(err)
	}
	cList = cListObject.Clusters
	if len(cList) != 0 {
		t.Fatal("Clusters list should be empty")
	}

}

/**** HELPER SECTION ****/

func agentInfoCmp(agentInfo1 types.AgentInfo, agentInfo2 types.AgentInfo) bool {
	return agentInfo1.Spiffeid == agentInfo2.Spiffeid && agentInfo1.Plugin == agentInfo2.Plugin
}

func inList(elem string, list []string) bool {
	for i := 0; i < len(list); i++ {
		if elem == list[i] {
			return true
		}
	}
	return false
}

func agentListComp(correctList []string, resultList []string) error {
	if len(resultList) != len(correctList) {
		return errors.Errorf("Lists not the same: expected %v, got %v", correctList, resultList)
	} else {
		for i := 0; i < len(correctList); i++ {
			if !inList(correctList[i], resultList) {
				return errors.Errorf("Agent %v not in resulting agents list %v", correctList[i], resultList)
			}
		}
		return nil
	}
}

func clusterEquality(c1 types.ClusterInfo, c2 types.ClusterInfo) bool {
	if c1.Name != c2.Name || c1.DomainName != c2.DomainName ||
		c1.ManagedBy != c2.ManagedBy || c1.PlatformType != c2.PlatformType {
		return false
	}
	return agentListComp(c1.AgentsList, c2.AgentsList) == nil
}

func inClusterList(cluster types.ClusterInfo, list []types.ClusterInfo) bool {
	for i := 0; i < len(list); i++ {
		if clusterEquality(cluster, list[i]) {
			return true
		}
	}
	return false
}

func clustersComp(c1 types.ClusterInfoList, c2 types.ClusterInfoList) error {
	c1info := c1.Clusters
	c2info := c2.Clusters
	if len(c1info) != len(c2info) {
		return errors.New("Number of clusters incorrect")
	}
	for i := 0; i < len(c1info); i++ {
		if !inClusterList(c1info[i], c2info) {
			cname := c1info[i].Name
			return errors.Errorf("Error: first list contains cluster %v not in second list", cname)
		}
	}
	return nil
}

/**** END HELPER SECTION ****/


<|start_filename|>tornjak-frontend/src/redux/reducers/entriesReducer.js<|end_filename|>
import {
    GLOBAL_ENTRIES_LIST,
} from '../actions/types';

const initialState = {
    globalEntriesList: [],
};

export default function entriesReducer(state = initialState, action) {
    switch (action.type) {
        case GLOBAL_ENTRIES_LIST:
            return {
                ...state,
                globalEntriesList: action.payload
            };
        default:
            return state;
    }
}

<|start_filename|>tornjak-frontend/src/components/dashboard/tornjak-dashboard.js<|end_filename|>
import React from 'react';
import { connect } from 'react-redux';
import clsx from 'clsx';
import { withStyles } from '@material-ui/core/styles';
// Components
import {
  Container,
  Grid,
  Paper,
} from '@material-ui/core';
// Pie Charts
import AgentsPieChart from './agents-pie-chart';
import ClustersPieChart from './clusters-pie-chart';
// Tables
import ClustersTable from './clusters-dashboard-table';
import AgentsTable from './agents-dashboard-table';
import EntriesTable from './entries-dashboard-table';
import IsManager from '../is_manager';
import TornjakApi from '../tornjak-api-helpers';
import {
  entriesListUpdateFunc,
  serverSelectedFunc,
  agentsListUpdateFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc,
  selectorInfoFunc,
  tornjakMessageFunc,
  agentworkloadSelectorInfoFunc,
  clustersListUpdateFunc,
  clickedDashboardTableFunc,
} from 'redux/actions';
import SpiffeHelper from '../spiffe-helper';
import DashboardDrawer from './dashboard-drawer';

const styles = theme => ({
  root: {
    marginTop: -25,
    marginLeft: -20,
    display: 'flex',
  },
  appBarSpacer: theme.mixins.toolbar,
  content: {
    flexGrow: 1,
  },
  container: { //container for root
    paddingTop: theme.spacing(4),
    paddingBottom: theme.spacing(4),
    marginLeft: 0
  },
  paper: { //container for all grids
    padding: theme.spacing(2),
    display: 'flex',
    overflow: 'auto',
    flexDirection: 'column',
    marginBottom: 20
  },
  fixedHeight: {
    height: 370, //height of piechart container
  },
});


class TornjakDashboard extends React.Component {
  constructor(props) {
    super(props);
    const { classes } = this.props;
    this.state = {
    };
    this.fixedHeightPaper = clsx(classes.paper, classes.fixedHeight)
    this.TornjakApi = new TornjakApi();
    this.SpiffeHelper = new SpiffeHelper();
  }

  agentSpiffeids() {
    if (typeof this.props.globalAgents.globalAgentsList !== undefined) {
      return this.props.globalAgents.globalAgentsList.map(currentAgent => {
        return this.SpiffeHelper.getAgentSpiffeid(currentAgent)
      })
    } else {
      return ""
    }
  }

  componentDidMount() {
    if (IsManager) {
      if (this.props.globalServerSelected !== "") {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateEntriesUpdate(this.props.globalServerSelected, this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
        this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateTornjakAgentInfo(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc, { "agents": this.agentSpiffeids() });
      }
    } else {
      this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateLocalEntriesUpdate(this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
      this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateLocalTornjakAgentInfo(this.props.agentworkloadSelectorInfoFunc, "");
      if (this.props.globalTornjakServerInfo !== "") {
        this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
      }
    }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateEntriesUpdate(this.props.globalServerSelected, this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
        this.TornjakApi.populateClustersUpdate(this.props.globalServerSelected, this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateTornjakAgentInfo(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc, { "agents": this.agentSpiffeids() });
      }
    } else {
      if (prevProps.globalTornjakServerInfo !== this.props.globalTornjakServerInfo) {
        this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateLocalEntriesUpdate(this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
        this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc)
        this.TornjakApi.populateLocalTornjakAgentInfo(this.props.agentworkloadSelectorInfoFunc, "");
        this.TornjakApi.populateLocalClustersUpdate(this.props.clustersListUpdateFunc, this.props.tornjakMessageFunc);
      }
    }
  }

  render() {
    const { classes } = this.props;
    return (
      <div className={classes.root}>
        <DashboardDrawer />
        <main className={classes.content}>
          <div className={classes.appBarSpacer} />
          {(this.props.globalClickedDashboardTable === "" || this.props.globalClickedDashboardTable === "dashboard") &&
            <Container maxWidth="lg" className={classes.container}>
              <Grid container spacing={3}>
                {/* Pie Chart Clusters */}
                <Grid item xs={6}>
                  <Paper className={this.fixedHeightPaper}>
                    <ClustersPieChart />
                  </Paper>
                </Grid>
                {/* Pie Chart Agents*/}
                <Grid item xs={6}>
                  <Paper className={this.fixedHeightPaper}>
                    <AgentsPieChart />
                  </Paper>
                </Grid>
                {/* Clusters Table */}
                <Grid item xs={12}>
                  <Paper className={classes.paper}>
                    <ClustersTable
                      numRows={5} />
                  </Paper>
                </Grid>
                {/* Agents Table */}
                <Grid item xs={12}>
                  <Paper className={classes.paper}>
                    <AgentsTable
                      numRows={5} />
                  </Paper>
                </Grid>
                {/* Entries Table */}
                <Grid item xs={12}>
                  <Paper className={classes.paper}>
                    <EntriesTable
                      numRows={5} />
                  </Paper>
                </Grid>
              </Grid>
            </Container>
          }
          {(this.props.globalClickedDashboardTable === "clusters") &&
            <Container maxWidth="lg" className={classes.container}>
              {/* Clusters Table */}
              <Grid item xs={12}>
                <Paper className={classes.paper}>
                  <ClustersTable
                    numRows={100} />
                </Paper>
              </Grid>
            </Container>
          }
          {(this.props.globalClickedDashboardTable === "agents") &&
            <Container maxWidth="lg" className={classes.container}>
              {/* Agents Table */}
              <Grid item xs={12}>
                <Paper className={classes.paper}>
                  <AgentsTable
                    numRows={100} />
                </Paper>
              </Grid>
            </Container>
          }
          {(this.props.globalClickedDashboardTable === "entries") &&
            <Container maxWidth="lg" className={classes.container}>
              {/* Entries Table */}
              <Grid item xs={12}>
                <Paper className={classes.paper}>
                  <EntriesTable
                    numRows={100} />
                </Paper>
              </Grid>
            </Container>
          }
        </main>
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
  globalClustersList: state.clusters.globalClustersList,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
  globalAgents: state.agents,
  globalEntries: state.entries.globalEntriesList,
  globalClickedDashboardTable: state.tornjak.globalClickedDashboardTable,
})

export default withStyles(styles)(connect(
  mapStateToProps,
  { entriesListUpdateFunc, agentsListUpdateFunc, agentworkloadSelectorInfoFunc, clustersListUpdateFunc, tornjakMessageFunc, serverInfoUpdateFunc, serverSelectedFunc, tornjakServerInfoUpdateFunc, selectorInfoFunc, clickedDashboardTableFunc }
)(TornjakDashboard))


<|start_filename|>tornjak-backend/pkg/manager/types/types.go<|end_filename|>
package types

// ServerInfo contains the information about servers
type ServerInfo struct {
	Name    string `json:"name"`
	Address string `json:"address"`
	TLS     bool   `json:"tls"`
	MTLS    bool   `json:"mtls"`
	CA      []byte `json:"ca,omitempty"`
	Cert    []byte `json:"cert,omitempty"`
	Key     []byte `json:"key,omitempty"`
}

// ServerInfo contains the information about servers
type ServerInfoList struct {
	Servers []ServerInfo `json:"servers"`
}


<|start_filename|>tornjak-frontend/src/tables/table-toolbar.js<|end_filename|>
import React from "react";
import { DataTable } from "carbon-components-react";
import {
    Delete16 as Delete,
} from '@carbon/icons-react';
import ResetIcon from "@carbon/icons-react/es/reset--alt/20";
const {
    TableToolbar,
    TableToolbarSearch,
    TableToolbarContent,
    TableBatchActions,
    TableBatchAction,
} = DataTable;

// TableToolBar takes in 
// onInputChange: onInputChange function for the search functionality from DataTable
// getBatchActionProps: getBatchActionProps function for functions on toolbar from DataTable
// deleteEntity: delete row function specified entity if applicable
// banEntity: ban row function for specified entity if applicable
// selectedRows: selectedRows from DataTable
// returns the toolbar of the table for the specified entity
class TableToolBar extends React.Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <TableToolbar>
                <TableToolbarContent>
                    <TableToolbarSearch onChange={(e) => this.props.onInputChange(e)} />
                </TableToolbarContent>
                <TableBatchActions {...this.props.getBatchActionProps()}>
                    {this.props.deleteEntity !== undefined &&
                        <TableBatchAction
                            renderIcon={Delete}
                            iconDescription="Delete"
                            onClick={() => {
                                this.props.deleteEntity(this.props.selectedRows);
                                this.props.getBatchActionProps().onCancel();
                            }}
                        >
                            Delete
                        </TableBatchAction>
                    }
                    {this.props.banEntity !== undefined &&
                        <TableBatchAction
                            renderIcon={ResetIcon}
                            iconDescription="Ban"
                            onClick={() => {
                                this.props.banEntity(this.props.selectedRows);
                                this.props.getBatchActionProps().onCancel();
                            }}
                        >
                            Ban
                        </TableBatchAction>
                    }
                </TableBatchActions>
            </TableToolbar>
        )
    }
};

export default (TableToolBar)


<|start_filename|>tornjak-frontend/src/components/dashboard/dashboard-details.js<|end_filename|>
import React from "react";
import { connect } from "react-redux";
import { withStyles } from "@material-ui/core/styles";
// Components
import { Container, Grid, Paper } from "@material-ui/core";
// Tables
import ClustersTable from "./clusters-dashboard-table";
import AgentsTable from "./agents-dashboard-table";
import EntriesTable from "./entries-dashboard-table";
import DashboardDrawer from "./dashboard-drawer";

const styles = (theme) => ({
  root: {
    marginTop: -25,
    marginLeft: -20,
    display: "flex",
  },
  appBarSpacer: theme.mixins.toolbar,
  content: {
    flexGrow: 1,
  },
  container: {
    //container for root
    paddingTop: theme.spacing(4),
    paddingBottom: theme.spacing(4),
    marginLeft: 0,
  },
  paper: {
    //container for all grids
    padding: theme.spacing(2),
    display: "flex",
    overflow: "auto",
    flexDirection: "column",
    marginBottom: 20,
  },
  detailsTitle: {
    fontSize: 20,
    color: "rgb(89, 103, 185)",
  },
  metadataTag: {
    fontWeight: "bold",
    fontSize: 17,
    marginTop: 10,
  },

  dashboardDetalsLine: {
    marginLeft: 0,
    width: 1200,
  },
  metadataDetails: {
    fontSize: 17,
  },
});

// DashboardDetails takes in the selected data and props from dashboard table for a specific entity
// returns details page for dashboard
class DashboardDetails extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedDataKey: this.selectedDataKey(),
    };
  }

  componentDidUpdate(prevProps) {
    if (prevProps.selectedData !== this.props.selectedData) {
      this.setState({
        selectedDataKey: this.selectedDataKey(),
      });
    }
  }

  selectedDataKey() {
    var { selectedData } = this.props;
    var clickedDashboardTable = this.props.globalClickedDashboardTable,
      selectedDataKey = [];
    if (selectedData !== undefined) {
      if (clickedDashboardTable === "clustersdetails") {
        //to filter agents in clustersdetails
        selectedDataKey["agentsFilter"] = selectedData.name;
        //to filter entries in clustersdetails
        selectedDataKey["entriesFilter"] = selectedData.name;
      } else if (clickedDashboardTable === "entriesdetails") {
        //to filter agents in entriesdetails
        selectedDataKey["agentsFilter"] = selectedData.parentId;
        //to filter clusters in entriesdetails
        selectedDataKey["clustersFilter"] = selectedData.clusterName;
      } else if (clickedDashboardTable === "agentsdetails") {
        //to filter clusters in agentsdetails
        selectedDataKey["clustersFilter"] = selectedData.clusterName;
        //to filter entries in agentsdetails
        selectedDataKey["entriesFilter"] = selectedData.spiffeid;
      }
    }
    return selectedDataKey;
  }

  render() {
    const { selectedDataKey } = this.state;
    const { classes, selectedData } = this.props;
    return (
      <div className={classes.root}>
        <DashboardDrawer />
        <main className={classes.content}>
          <div className={classes.appBarSpacer} />
          {selectedData.length !== 0 && !selectedData[0] && (
            <div>
              {
                this.props.globalClickedDashboardTable ===
                  "clustersdetails" && (
                  <div className="clustersdetails">
                    <Container maxWidth="lg" className={classes.container}>
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <p className={classes.detailsTitle}>
                            Cluster Name : <b>{selectedData.name}</b>
                          </p>
                          <p className={classes.metadataTag}>Metadata</p>
                          <hr className={classes.dashboardDetalsLine}></hr>
                          <p className={classes.metadataDetails}>
                            Created : <b>{selectedData.created} </b>
                          </p>
                          <p className={classes.metadataDetails}>
                            Number of Nodes : <b>{selectedData.numNodes} </b>
                          </p>
                          <p className={classes.metadataDetails}>
                            Number of Entries: <b>{selectedData.numEntries}</b>{" "}
                          </p>
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Agents Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <AgentsTable
                            numRows={100}
                            filterByCluster={selectedDataKey["agentsFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Entries Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <EntriesTable
                            numRows={100}
                            filterByCluster={selectedDataKey["entriesFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                  </div>
                ) // End clustersdetails
              }
              {
                this.props.globalClickedDashboardTable === "agentsdetails" && (
                  <div className="agentsdetails">
                    <Container maxWidth="lg" className={classes.container}>
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <p className={classes.detailsTitle}>
                            Agent Name : <b>{selectedData.spiffeid}</b>
                          </p>
                          <p className={classes.metadataTag}>Metadata</p>
                          <hr className={classes.dashboardDetalsLine}></hr>
                          <p className={classes.metadataDetails}>
                            Belongs to Cluster :{" "}
                            <b>{selectedData.clusterName}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Status : <b>{selectedData.status}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Platform Type : <b>{selectedData.platformType}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Number of Entries: <b>{selectedData.numEntries}</b>{" "}
                          </p>
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Clusters Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <ClustersTable
                            numRows={100}
                            filterByCluster={selectedDataKey["clustersFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Entries Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <EntriesTable
                            numRows={100}
                            filterByAgentId={selectedDataKey["entriesFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                  </div>
                ) // End agentsdetails
              }
              {
                this.props.globalClickedDashboardTable === "entriesdetails" && (
                  <div className="entriesdetails">
                    <Container maxWidth="lg" className={classes.container}>
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <p className={classes.detailsTitle}>
                            Entry ID : <b>{selectedData.id}</b>
                          </p>
                          <p className={classes.metadataTag}>Metadata</p>
                          <hr className={classes.dashboardDetalsLine}></hr>
                          <p className={classes.metadataDetails}>
                            Entry Name : <b>{selectedData.spiffeid}</b>
                          </p>
                          <p className={classes.metadataDetails}>
                            Parent ID : <b>{selectedData.parentId}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Belongs to Cluster :{" "}
                            <b>{selectedData.clusterName}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Platform Type : <b>{selectedData.platformType}</b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Is Admin :{" "}
                            <b>
                              {selectedData.adminFlag.toString().toUpperCase()}
                            </b>{" "}
                          </p>
                          <p className={classes.metadataDetails}>
                            Entry Expire Time:{" "}
                            <b>{selectedData.entryExpireTime}</b>{" "}
                          </p>
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Clusters Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <ClustersTable
                            numRows={100}
                            filterByCluster={selectedDataKey["clustersFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                    <Container maxWidth="lg" className={classes.container}>
                      {/* Agents Table */}
                      <Grid item xs={12}>
                        <Paper className={classes.paper}>
                          <AgentsTable
                            numRows={100}
                            filterByAgentId={selectedDataKey["agentsFilter"]}
                          />
                        </Paper>
                      </Grid>
                    </Container>
                  </div>
                ) // End entriesdetails
              }
              {this.props.globalClickedDashboardTable === "clusters" && (
                <Container maxWidth="lg" className={classes.container}>
                  {/* Clusters Table */}
                  <Grid item xs={12}>
                    <Paper className={classes.paper}>
                      <ClustersTable 
                        numRows={100}
                        filterByCluster={selectedDataKey["clustersFilter"]} 
                      />
                    </Paper>
                  </Grid>
                </Container>
              )}
              {this.props.globalClickedDashboardTable === "agents" && (
                <Container maxWidth="lg" className={classes.container}>
                  {/* Agents Table */}
                  <Grid item xs={12}>
                    <Paper className={classes.paper}>
                      <AgentsTable 
                        numRows={100} 
                        filterByAgentId={selectedDataKey["agentsFilter"]}
                        filterByCluster={selectedDataKey["agentsFilter"]}
                      />
                    </Paper>
                  </Grid>
                </Container>
              )}
              {this.props.globalClickedDashboardTable === "entries" && (
                <Container maxWidth="lg" className={classes.container}>
                  {/* Entries Table */}
                  <Grid item xs={12}>
                    <Paper className={classes.paper}>
                      <EntriesTable 
                        numRows={100}
                        filterByAgentId={selectedDataKey["entriesFilter"]}
                        filterByCluster={selectedDataKey["entriesFilter"]}
                      />
                    </Paper>
                  </Grid>
                </Container>
              )}
            </div>
          )}
        </main>
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  globalClickedDashboardTable: state.tornjak.globalClickedDashboardTable,
});

export default withStyles(styles)(
  connect(mapStateToProps, {})(DashboardDetails)
);


<|start_filename|>tornjak-frontend/src/App.js<|end_filename|>
import React from 'react';
import "bootstrap/dist/css/bootstrap.min.css";
import store from 'redux/store';
import IsManager from './components/is_manager';
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from 'react-redux'; //enables all components to have access to everything inside our react app
import NavigationBar from "./components/navbar";
import SelectServer from "./components/select-server";
import ClusterList from "./components/cluster-list";
import ClusterManagement from "./components/cluster-management";
import AgentList from "./components/agent-list";
import CreateJoinToken from "./components/agent-create-join-token";
import EntryList from "./components/entry-list";
import EntryCreate from "./components/entry-create";
import ServerManagement from "./components/server-management";
import TornjakServerInfo from "./components/tornjak-server-info";
import TornjakDashBoard from "./components/dashboard/tornjak-dashboard";
import DashboardDetailsRender from 'components/dashboard/dashboard-details-render';
import './App.css';

function App() {
    return (
        <Provider store={store}>
            <Router>
                <div>
                    <div className="nav-comp">
                        <NavigationBar />
                    </div>
                    <div className="rest-body">
                        <SelectServer />
                        <br />
                        {IsManager && <br />}
                        <Route path="/" exact component={AgentList} />
                        <Route path="/clusters" exact component={ClusterList} />
                        <Route path="/agents" exact component={AgentList} />
                        <Route path="/entries" exact component={EntryList} />
                        <Route path="/entry/create" exact component={EntryCreate} />
                        <Route path="/agent/createjointoken" exact component={CreateJoinToken} />
                        <Route path="/cluster/clustermanagement" exact component={ClusterManagement} />
                        <Route path="/tornjak/serverinfo" exact component={TornjakServerInfo} />
                        <Route path="/tornjak/dashboard" exact component={TornjakDashBoard} />
                        <Route
                            path="/tornjak/dashboard/details/:entity"
                            render={(props) => (<DashboardDetailsRender {...props} params={props.match.params} />)}
                        />
                        <Route path="/server/manage" exact component={ServerManagement} />
                        <br /><br /><br />
                        <svg className="endbanneroutput">
                            <rect className="endbanneroutput"></rect>
                        </svg>
                    </div>
                </div>
            </Router>
        </Provider>
    );
}

export default App;


<|start_filename|>tornjak-frontend/src/components/agent-list.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import IsManager from './is_manager';
import Table from "tables/agents-list-table";
import { selectors, workloadSelectors, clusterType } from '../data/data';
import TornjakApi from './tornjak-api-helpers';
import {
  serverSelectedFunc,
  agentsListUpdateFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc,
  selectorInfoFunc,
  tornjakMessageFunc,
  workloadSelectorInfoFunc,
  agentworkloadSelectorInfoFunc,
  clusterTypeInfoFunc,
} from 'redux/actions';

const Agent = props => (
  <tr>
    <td>{props.agent.id.trust_domain}</td>
    <td>{"spiffe://" + props.agent.id.trust_domain + props.agent.id.path}</td>
    <td><div style={{ overflowX: 'auto', width: "400px" }}>
      <pre>{JSON.stringify(props.agent, null, ' ')}</pre>
    </div></td>

    <td>
      {/*
        // <Link to={"/agentView/"+props.agent._id}>view</Link> |
      */}
      <a href="/#" onClick={() => { props.banAgent(props.agent.id) }}>ban</a>
      <br />
      <a href="/#" onClick={() => { props.deleteAgent(props.agent.id) }}>delete</a>
    </td>
  </tr>
)

class AgentList extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.state = {
      message: "",
    };
  }

  componentDidMount() {
    this.props.clusterTypeInfoFunc(clusterType); //set cluster type info
    this.props.selectorInfoFunc(selectors); //set selector info
    this.props.workloadSelectorInfoFunc(workloadSelectors); //set workload selector info
    if (IsManager) {
      if (this.props.globalServerSelected !== "") {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc)
        this.TornjakApi.refreshSelectorsState(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc);
        this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
      }
    } else {
      this.TornjakApi.refreshLocalSelectorsState(this.props.agentworkloadSelectorInfoFunc);
      this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
      if(this.props.globalTornjakServerInfo !== "") {
        this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
      }
    }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.refreshSelectorsState(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc);
      }
    } else {
        if(prevProps.globalTornjakServerInfo !== this.props.globalTornjakServerInfo)
        {
          this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
          this.TornjakApi.refreshLocalSelectorsState(this.props.agentworkloadSelectorInfoFunc);
        }
    }
  }

  agentList() {
    if (typeof this.props.globalAgentsList !== 'undefined') {
      return this.props.globalAgentsList.map(currentAgent => {
        return <Agent key={currentAgent.id.path}
          agent={currentAgent}
          banAgent={this.banAgent}
          deleteAgent={this.deleteAgent} />;
      })
    } else {
      return ""
    }
  }

  render() {
    return (
      <div>
        <h3>Agents List</h3>
        {this.props.globalErrorMessage !== "OK" &&
          <div className="alert-primary" role="alert">
            <pre>
              {this.props.globalErrorMessage}
            </pre>
          </div>
        }
        <br /><br />
        <div className="indvidual-list-table">
          <Table data={this.agentList()} id="table-1" />
        </div>
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
  globalAgentsList: state.agents.globalAgentsList,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
  mapStateToProps,
  { serverSelectedFunc, agentsListUpdateFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, selectorInfoFunc, tornjakMessageFunc, workloadSelectorInfoFunc, agentworkloadSelectorInfoFunc, clusterTypeInfoFunc }
)(AgentList)


<|start_filename|>tornjak-frontend/src/components/entry-list.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import IsManager from './is_manager';
import Table from "tables/entries-list-table";
import TornjakApi from "./tornjak-api-helpers"
import {
  serverSelectedFunc,
  entriesListUpdateFunc,
  tornjakMessageFunc,
} from 'redux/actions';

const Entry = props => (
  <tr>
    <td>{props.entry.id}</td>
    <td>{ "spiffe://" + props.entry.spiffe_id.trust_domain + props.entry.spiffe_id.path}</td>
    <td>{ "spiffe://" + props.entry.parent_id.trust_domain + props.entry.parent_id.path}</td>
    <td>{ props.entry.selectors.map(s => s.type + ":" + s.value).join(', ')}</td>
    
    <td>
      <br/>
      <a href="/#" onClick={() => { props.deleteEntry (props.entry.id) }}>delete</a>
    </td>

    <td><div style={{overflowX: 'auto', width: "400px"}}>
    <pre>{JSON.stringify(props.entry, null, ' ')}</pre>
    </div></td>

  </tr>
)

class EntryList extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.state = { 
        servers: [],
        selectedServer: "",
    };
  }

  componentDidMount() {
    if (IsManager) {
      if(this.props.globalServerSelected !== ""){
        this.TornjakApi.populateEntriesUpdate(this.props.globalServerSelected, this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
      }
    } else {
        this.TornjakApi.populateLocalEntriesUpdate(this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
    }
  }

  componentDidUpdate(prevProps) {
    if (IsManager) {
      if(prevProps.globalServerSelected !== this.props.globalServerSelected){
        this.TornjakApi.populateEntriesUpdate(this.props.globalServerSelected, this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
      }
    } else {
      this.TornjakApi.populateLocalEntriesUpdate(this.props.entriesListUpdateFunc, this.props.tornjakMessageFunc)
    }
  }

  entryList() {
  if (typeof this.props.globalEntriesList !== 'undefined') {
      return this.props.globalEntriesList.map(currentEntry => {
        return <Entry key={currentEntry.id} 
                  entry={currentEntry} 
                  deleteEntry={this.deleteEntry}/>;
      })
  } else {
      return ""
  }
}

  render() {

    return (
      <div>
        <h3>Entries List</h3>
        {this.props.globalErrorMessage !== "OK" &&
          <div className="alert-primary" role="alert">
            <pre>
              {this.props.globalErrorMessage}
            </pre>
          </div>
        }
        <br/><br/>
        <div className="indvidual-list-table">
          <Table data={this.entryList()} id="table-1" />
        </div>
      </div>
    )
  }
}


const mapStateToProps = (state) => ({
  globalServerSelected: state.servers.globalServerSelected,
  globalEntriesList: state.entries.globalEntriesList,
  globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
  mapStateToProps,
  { serverSelectedFunc, entriesListUpdateFunc, tornjakMessageFunc }
)(EntryList)

<|start_filename|>tornjak-frontend/src/redux/reducers/index.js<|end_filename|>
import serversReducer from './serversReducer';
import clustersReducer from './clustersReducer';
import agentsReducer from './agentsReducer';
import entriesReducer from './entriesReducer';
import tornjakReducer from './tornjakReducer';
import {combineReducers} from 'redux';

const allReducers = combineReducers({
    servers : serversReducer,
    clusters : clustersReducer,
    agents : agentsReducer,
    entries : entriesReducer,
    tornjak: tornjakReducer,
});

export default allReducers;

<|start_filename|>tornjak-frontend/src/tables/list-table.js<|end_filename|>
import React from "react";
import { DataTable } from "carbon-components-react";
import ToolBar from './table-toolbar';
import Head from './table-head';
import Body from './table-body';
const {
    TableContainer,
    Table,
} = DataTable;

// DataTableRender takes in 
// entityType: type of entity, 
// listTableData: data to be rendered on table, 
// headerData: headerData of table, 
// deleteEntity: delete row function for specified entity
// banEntity: ban row function for specified entity if applicable
// returns data inside a carbon component table with specified functions for entity
class DataTableRender extends React.Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <DataTable
                isSortable
                rows={this.props.listTableData}
                headers={this.props.headerData}
                render={({
                    rows,
                    headers,
                    getHeaderProps,
                    getSelectionProps,
                    onInputChange,
                    getPaginationProps,
                    getBatchActionProps,
                    getTableContainerProps,
                    selectedRows,
                }) => (
                    <TableContainer
                        {...getTableContainerProps()}
                    >
                        <ToolBar
                            onInputChange={onInputChange}
                            getBatchActionProps={getBatchActionProps}
                            deleteEntity={this.props.deleteEntity}
                            banEntity={this.props.banEntity}
                            selectedRows={selectedRows}
                        />
                        <Table size="short" useZebraStyles>
                            <Head
                                getHeaderProps={getHeaderProps}
                                getSelectionProps={getSelectionProps}
                                headers={headers}
                            />
                            <Body
                                entityType={this.props.entityType}
                                rows={rows}
                                getSelectionProps={getSelectionProps}
                            />
                        </Table>
                    </TableContainer>
                )}
            />
        );
    }
}

export default (DataTableRender)


<|start_filename|>tornjak-backend/pkg/agent/db/db.go<|end_filename|>
package db

import (
	"github.com/spiffe/tornjak/tornjak-backend/pkg/agent/types"
)

type AgentDB interface {
	// AGENT - SELECTOR/PLUGIN interface
	CreateAgentEntry(sinfo types.AgentInfo) error
	GetAgentSelectors() (types.AgentInfoList, error)
	GetAgentPluginInfo(name string) (types.AgentInfo, error)

	// CLUSTER interface
	GetClusters() (types.ClusterInfoList, error)
	CreateClusterEntry(cinfo types.ClusterInfo) error
	EditClusterEntry(cinfo types.ClusterInfo) error
	DeleteClusterEntry(name string) error

	// AGENT - CLUSTER Get interface (for testing)e
	GetAgentClusterName(spiffeid string) (string, error)
	GetClusterAgents(name string) ([]string, error)
	GetAgentsMetadata(req types.AgentMetadataRequest) (types.AgentInfoList, error)
}


<|start_filename|>tornjak-frontend/src/components/work-load-attestor-modal.js<|end_filename|>
import React from "react";
import { ModalWrapper, Dropdown, TextArea } from "carbon-components-react";
import { connect } from 'react-redux';
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';
import {
    agentsListUpdateFunc,
    agentworkloadSelectorInfoFunc,
} from 'redux/actions';

class WorkLoadAttestor extends React.Component {
    constructor(props) {
        super(props);
        this.TornjakApi = new TornjakApi();
        this.state = {
            workloadPlugin: "",
            selectorsList: "",
            selectors: "",
            wLoadAttdata: [{}],
            agentId: "",
            agentSpiffeId: "",
        };
        this.onChangeWorkloadPlugin = this.onChangeWorkloadPlugin.bind(this);
        this.prepareAgentData = this.prepareAgentData.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    componentDidMount() {
        this.prepareAgentData();
        // if (IsManager) {
        //     this.TornjakApi.refreshSelectorsState(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc);
        // }
        // else {
        //     this.TornjakApi.refreshLocalSelectorsState(this.props.agentworkloadSelectorInfoFunc, this.props.globalServerSelected);
        // }
    }
    componentDidUpdate(prevProps, prevState) {
        if(prevProps !== this.props) {
            this.prepareAgentData();
        }
    }

    prepareAgentData() {
        const { spiffeid } = this.props;
        this.setState({
            agentSpiffeId: spiffeid,
        })
    }

    handleSubmit = () => {
        var wLoadAttdata = {
            "spiffeid": this.state.agentSpiffeId,
            "plugin": this.state.workloadPlugin,
        };
        if (IsManager) {
            this.TornjakApi.registerSelectors(this.props.globalServerSelected, wLoadAttdata, this.TornjakApi.refreshSelectorsState, this.props.agentworkloadSelectorInfoFunc);
            this.TornjakApi.refreshSelectorsState(this.props.globalServerSelected, this.props.agentworkloadSelectorInfoFunc);
        } else {
            this.TornjakApi.registerLocalSelectors(wLoadAttdata, this.TornjakApi.refreshLocalSelectorsState, this.props.agentworkloadSelectorInfoFunc);
            this.TornjakApi.refreshLocalSelectorsState(this.props.agentworkloadSelectorInfoFunc, this.props.globalServerSelected);
        }
        return true;
    };

    onChangeWorkloadPlugin = selected => {
        var selectors = "";
        var sid = selected.selectedItem.label;
        var selectorsObject = this.props.globalWorkloadSelectorInfo[sid];
        for (let i = 0; i < selectorsObject.length; i++) {
            if (i !== sid.length - 1) {
                selectors = selectors + selectorsObject[i].label + ":\n";
            }
            else {
                selectors = selectors + selectorsObject[i].label + ":"
            }
        }
        this.setState({
            workloadPlugin: sid,
            selectorsList: selectors,
        })
    }

    render() {
        //TODO: dynamically populated pluginlist
        const PluginList =
            [
                {
                    id: "1",
                    label: "Docker",
                },
                {
                    id: "2",
                    label: "Kubernetes",
                },
                {
                    id: "3",
                    label: "Unix",
                },
            ];
        return (
            <div>
                <ModalWrapper
                    triggerButtonKind="ghost"
                    buttonTriggerText="Add/ Edit WorkLoad Attestor"
                    primaryButtonText="Save & Add"
                    handleSubmit={this.handleSubmit}
                    shouldCloseAfterSubmit={true}
                >
                    <p> Define WorkLoad Attestor Information: </p>
                    <br />
                    <div className="parentId-drop-down">
                        <Dropdown
                            ariaLabel="workload-attestor-kind-drop-down"
                            id="workload-attestor-kind-drop-down"
                            items={PluginList}
                            label="Select WorkLoad Attestor Plugin"
                            titleText="WorkLoad Attestor Plugin"
                            onChange={this.onChangeWorkloadPlugin}
                        />
                    </div>
                    <div className="selectors-textArea">
                        <TextArea
                            cols={50}
                            helperText="i.e. docker:label:,..."
                            id="selectors-textArea"
                            invalidText="A valid value is required"
                            labelText="Workload Selectors"
                            placeholder="Select Workload Attestor Plugin from above and selectors will be populated here - Refer to Workload Attestor Plugin"
                            defaultValue={this.state.selectorsList}
                            rows={8}
                            disabled
                        />
                    </div>
                </ModalWrapper>
            </div>
        );
    }
}

const mapStateToProps = (state) => ({
    globalServerSelected: state.servers.globalServerSelected,
    globalAgentsList: state.agents.globalAgentsList,
    globalWorkloadSelectorInfo: state.servers.globalWorkloadSelectorInfo,
})

export default connect(
    mapStateToProps,
    { agentsListUpdateFunc, agentworkloadSelectorInfoFunc }
)(WorkLoadAttestor)


<|start_filename|>tornjak-frontend/src/components/cluster-management.js<|end_filename|>
import React, { Component } from 'react';
import { Tabs, Tab } from 'carbon-components-react';
import ClusterCreate from './cluster-create';
import ClusterEdit from './cluster-edit';
import { connect } from 'react-redux';
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';
import './style.css';
import {
  clusterTypeInfoFunc,
  serverSelectedFunc,
  selectorInfoFunc,
  agentsListUpdateFunc,
  tornjakMessageFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc
} from 'redux/actions';

class ClusterManagement extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.prepareClusterTypeList = this.prepareClusterTypeList.bind(this);
    this.prepareAgentsList = this.prepareAgentsList.bind(this);
    this.state = {
      clusterTypeList: [],
      agentsList: [],
      agentsListDisplay: "Select Agents",
      clusterTypeManualEntryOption: "----Select this option and Enter Custom Cluster Type Below----",
    }
  }

  componentDidMount() {
    if (IsManager) {
      if (this.props.globalServerSelected !== "" && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
        this.setState({ selectedServer: this.props.globalServerSelected });
        this.prepareClusterTypeList();
        this.prepareAgentsList();
      }
    } else {
      this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
      this.prepareClusterTypeList();
      this.prepareAgentsList();
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.setState({ selectedServer: this.props.globalServerSelected });
      }
      if (prevProps.globalServerInfo !== this.props.globalServerInfo) {
        this.prepareAgentsList();
      }
    } else {
      if (prevProps.globalServerInfo !== this.props.globalServerInfo) {
        this.prepareAgentsList();
      }
    }
  }

  prepareClusterTypeList() {
    let localClusterTypeList = [];
    // user prefered option
    localClusterTypeList[0] = this.state.clusterTypeManualEntryOption;
    // cluster type list
    console.log("this.props.globalClusterTypeInfo", this.props.globalClusterTypeInfo)
    for (let i = 0; i < this.props.globalClusterTypeInfo.length; i++) {
      localClusterTypeList[i + 1] = this.props.globalClusterTypeInfo[i];
    }
    this.setState({
      clusterTypeList: localClusterTypeList
    });
  }

  prepareAgentsList() {
    var prefix = "spiffe://";
    let localAgentsIdList = [];
    //agents
    for (let i = 0; i < this.props.globalAgentsList.length; i++) {
      localAgentsIdList[i] = {}
      localAgentsIdList[i]["label"] = prefix + this.props.globalAgentsList[i].id.trust_domain + this.props.globalAgentsList[i].id.path;
    }
    this.setState({
      agentsList: localAgentsIdList,
    });
  }

  render() {
    return (
      <div className="cluster-management-tabs">
        <Tabs scrollIntoView={false} >
          <Tab className="cluster-management-tab1"
            id="tab-1"
            label="Create Cluster"
          >
            <ClusterCreate
              clusterTypeList={this.state.clusterTypeList}
              agentsList={this.state.agentsList}
            />
          </Tab>
          <Tab
            id="tab-2"
            label="Edit Cluster"
          >
            <ClusterEdit
              clusterTypeList={this.state.clusterTypeList}
              agentsList={this.state.agentsList}
            />
          </Tab>
        </Tabs>
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalClusterTypeInfo: state.clusters.globalClusterTypeInfo,
  globalServerSelected: state.servers.globalServerSelected,
  globalAgentsList: state.agents.globalAgentsList,
  globalServerInfo: state.servers.globalServerInfo,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
})

export default connect(
  mapStateToProps,
  { clusterTypeInfoFunc, serverSelectedFunc, selectorInfoFunc, agentsListUpdateFunc, tornjakMessageFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc }
)(ClusterManagement)


<|start_filename|>tornjak-frontend/src/data/data.js<|end_filename|>
const clusterType = 
[
  "Kubernetes",
  "VMs",
];

const selectors =
{
  "aws_iid": [
    {
      "label": "aws_iid:tag:name"
    },
    {
      "label": "aws_iid:sg:id"
    },
    {
      "label": "aws_iid:sg:name"
    },
    {
      "label": "aws_iid:iamrole:arn:aws:iam"
    }
  ],
  "gcp_iit": [
    {
      "label": "gcp_iit:project-id"
    },
    {
      "label": "gcp_iit:zone"
    },
    {
      "label": "gcp_iit:instance-name"
    },
    {
      "label": "gcp_iit:tag"
    },
    {
      "label": "gcp_iit:sa"
    },
    {
      "label": "gcp_iit:label"
    },
    {
      "label": "gcp_iit:metadata"
    }
  ],
  "k8s_sat": [
    {
      "label": "k8s_sat:cluster"
    },
    {
      "label": "k8s_sat:agent_ns"
    },
    {
      "label": "k8s_sat:agent_sa"
    }
  ],
  "k8s_psat": [
    {
      "label": "k8s_psat:cluster"
    },
    {
      "label": "k8s_psat:agent_ns"
    },
    {
      "label": "k8s_psat:agent_sa"
    },
    {
      "label": "k8s_psat:agent_pod_name"
    },
    {
      "label": "k8s_psat:agent_pod_uid"
    },
    {
      "label": "k8s_psat:agent_pod_label"
    },
    {
      "label": "k8s_psat:agent_node_ip"
    },
    {
      "label": "k8s_psat:agent_node_name"
    },
    {
      "label": "k8s_psat:agent_node_uid"
    },
    {
      "label": "k8s_psat:agent_node_label"
    }
  ],
};

const workloadSelectors =
{
  "Docker": [
    {
      "label": "docker:label"
    },
    {
      "label": "docker:env"
    },
    {
      "label": "docker:image_id"
    },
  ],
  "Kubernetes": [
    {
      "label": "k8s:ns"
    },
    {
      "label": "k8s:sa"
    },
    {
      "label": "k8s:container-image"
    },
    {
      "label": "k8s:container-name"
    },
    {
      "label": "k8s:node-name"
    },
    {
      "label": "k8s:pod-label"
    },
    {
      "label": "k8s:pod-owner"
    },
    {
      "label": "k8s:pod-owner-uid"
    },
    {
      "label": "k8s:pod-uid"
    },
    {
      "label": "k8s:pod-name"
    },
    {
      "label": "k8s:pod-image"
    },
    {
      "label": "k8s:pod-image-count"
    },
    {
      "label": "k8s:pod-init-image"
    },
    {
      "label": "k8s:pod-init-image-count"
    },
  ],
  "Unix": [
    {
      "label": "unix:uid"
    },
    {
      "label": "unix:user"
    },
    {
      "label": "unix:gid"
    },
    {
      "label": "unix:group"
    },
    {
      "label": "unix:supplementary_gid"
    },
    {
      "label": "unix:supplementary_group"
    },
    {
      "label": "unix:path"
    },
    {
      "label": "unix:sha256"
    },
  ],
};
export { clusterType, selectors, workloadSelectors };

<|start_filename|>tornjak-frontend/src/components/server-management.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios'
import GetApiServerUri from './helpers';
import IsManager from './is_manager';
import {
  serversListUpdateFunc
} from 'redux/actions';

const Server = props => (
  <tr>
    <td>{props.server.name}</td>
    <td>{props.server.address}</td>
    <td>{(props.server.mtls && "mTLS") || (props.server.tls && "TLS") || "None"}</td>
  </tr>
)

class ServerManagement extends Component {
  constructor(props) {
    super(props);
    this.state = {
        formServerName: "",
        formServerAddress: "",
        formTLS: false,
        formMTLS: false,
        formCAData: null,
        formCertData: null,
        formKeyData: null,
        CAFileText: "",
        certFileText: "",
        keyFileText: "",
    };
    this.onCertFileChange = this.onCertFileChange.bind(this);
    this.onCAFileChange = this.onCAFileChange.bind(this);
    this.onKeyFileChange = this.onKeyFileChange.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
  }

  componentDidMount() {
      this.refreshServerState()
  }

  refreshServerState () {
    axios.get(GetApiServerUri("/manager-api/server/list"), { crossdomain: true })
      .then(response => {
          console.log(response.data);
          this.props.serversListUpdateFunc(response.data["servers"]);
      })
      .catch((error) => {
        console.log(error);
      })
  }

  serverList() {
    if (typeof this.props.globalServersList !== 'undefined') {
        return this.props.globalServersList.map(s => {
          return <Server key={s.name} 
                    server={s} />;
        })
    } else {
        return ""
    }
  }

  handleInputChange(e) {
    const target = e.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value
    });
  }


  onCAFileChange = event => {
    // Update the state
    const reader = new FileReader();
    reader.onload = e => {
      this.setState({ 
          formCAData: (new Buffer(e.target.result)).toString("base64"),
          CAFileText: "CA file load success",
      });
    }
    reader.readAsText(event.target.files[0])
  };
  

  onCertFileChange = event => {
    // Update the state
    const reader = new FileReader();
    reader.onload = e => {
      this.setState({ 
          formCertData: (new Buffer(e.target.result)).toString("base64"),
          certFileText: "cert file load success",
      });
    }
    reader.readAsText(event.target.files[0])
  };

  onKeyFileChange = event => {
    // Update the state
    const reader = new FileReader();
    reader.onload = e => {
      this.setState({ 
          formKeyData: (new Buffer(e.target.result)).toString("base64"),
          keyFileText: "key file load success",
      });
    }
    reader.readAsText(event.target.files[0])
  };


  onSubmit(e) {
    e.preventDefault();

    console.log("onSubmit");
    var cjtData = {
        "name": this.state.formServerName,
        "address": this.state.formServerAddress,
        "tls": this.state.formTLS,
        "mtls": this.state.formMTLS,
        "ca": this.state.formCAData,
        "cert": this.state.formCertData,
        "key": this.state.formKeyData,
    };
    axios.post(GetApiServerUri('/manager-api/server/register'), cjtData)
      .then(res => {
          this.setState({ message: "Requst:" + JSON.stringify(cjtData,null,  ' ')+ "\n\nSuccess:" + JSON.stringify(res.data, null, ' ')});
          this.refreshServerState();
      }
      )
      .catch(err => this.setState({ message: "ERROR:" + err + (typeof (err.response) !== "undefined" ? err.response.data : "")}))

  }


  render() {
    if (!IsManager) {
        return <h1>Only manager deployments have use of this page</h1>
    }

    const tlsFormOptions = (
          <div> CA File (for (m)TLS):
          <input name="CAfile" type="file" onChange={this.onCAFileChange} />
            {this.state.CAFileText}
          </div>
    )

    const mtlsFormOptions = (
        <div>
          <div> 
           Cert File (for mTLS):
          <input name="certfile" type="file" onChange={this.onCertFileChange} />
            {this.state.certFileText}
          </div>
          <div> Key File (for mTLS):
          <input name="keyfile" type="file" onChange={this.onKeyFileChange} />
            {this.state.keyFileText}
          </div>
        </div>
    )

    return (
      <div>
        <h3> Register New Server </h3>
        <form onSubmit={this.onSubmit}>
          <div className="form-group">
            <label>Server Name (Unique)</label>
            <input type="text"
              name="formServerName"
              required
              className="form-control"
              value={this.state.formServerName}
              onChange={this.handleInputChange}
            />
          </div>


          <div className="form-group">
            <label>Address (i.e. http://localhost:5000/) </label>
            <input type="text"
              name="formServerAddress"
              required
              className="form-control"
              value={this.state.formServerAddress}
              onChange={this.handleInputChange}
            />
          </div>

          <div name="tls-checkbox-input">
          <input
            name="formTLS"
            type="checkbox"
            checked={this.state.formTLS}
            onChange={this.handleInputChange}
            />
              TLS Enabled
          </div>
          {this.state.formTLS && tlsFormOptions}

          <div name="mtls-checkbox-input">
          <input
            name="formMTLS"
            type="checkbox"
            checked={this.state.formMTLS}
            onChange={this.handleInputChange}
            />
            mTLS Enabled
          </div>
          {this.state.formMTLS && mtlsFormOptions}

          <div className="form-group">
            <br></br>
          <input type="submit" value="Register Server" className="btn btn-primary" />
          </div>
        </form>

        <div className="alert-primary" role="alert">
        <pre>
           {this.state.message}
        </pre>
        </div>


        <h3>Server List</h3>
        <table className="table" style={{width : "100%"}}>
          <thead className="thead-light">
            <tr>
              <th>Server Name</th>
              <th>Address</th>
              <th>TLS?</th>
            </tr>
          </thead>
          <tbody>
            {this.serverList()}
          </tbody>
        </table>

      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  globalServersList: state.servers.globalServersList
})

export default connect(
  mapStateToProps,
  { serversListUpdateFunc }
)(ServerManagement)


<|start_filename|>tornjak-frontend/src/components/cluster-create.js<|end_filename|>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import { Dropdown, TextInput, MultiSelect, TextArea } from 'carbon-components-react';
import GetApiServerUri from './helpers';
import IsManager from './is_manager';
import TornjakApi from './tornjak-api-helpers';
import { clusterType } from '../data/data';
import './style.css';
import {
  clusterTypeInfoFunc,
  serverSelectedFunc,
  selectorInfoFunc,
  agentsListUpdateFunc,
  tornjakMessageFunc,
  tornjakServerInfoUpdateFunc,
  serverInfoUpdateFunc
} from 'redux/actions';

class ClusterCreate extends Component {
  constructor(props) {
    super(props);
    this.TornjakApi = new TornjakApi();
    this.onChangeClusterName = this.onChangeClusterName.bind(this);
    this.onChangeClusterType = this.onChangeClusterType.bind(this);
    this.onChangeManualClusterType = this.onChangeManualClusterType.bind(this);
    this.onChangeClusterDomainName = this.onChangeClusterDomainName.bind(this);
    this.onChangeClusterManagedBy = this.onChangeClusterManagedBy.bind(this);
    this.onChangeAgentsList = this.onChangeAgentsList.bind(this);
    this.onSubmit = this.onSubmit.bind(this);

    this.state = {
      clusterName: "",
      clusterType: "",
      clusterDomainName: "",
      clusterManagedBy: "",
      clusterAgentsList: "",
      clusterTypeList: this.props.clusterTypeList,
      clusterTypeManualEntryOption: "----Select this option and Enter Custom Cluster Type Below----",
      clusterTypeManualEntry: false,
      message: "",
      statusOK: "",
      selectedServer: "",
      agentsListDisplay: "Select Agents",
      assignedAgentsListDisplay: "",
    }
  }

  componentDidMount() {
    this.props.clusterTypeInfoFunc(clusterType); //set cluster type info
    if (IsManager) {
      if (this.props.globalServerSelected !== "" && (this.props.globalErrorMessage === "OK" || this.props.globalErrorMessage === "")) {
        this.TornjakApi.populateAgentsUpdate(this.props.globalServerSelected, this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
        this.TornjakApi.populateTornjakServerInfo(this.props.globalServerSelected, this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
        this.setState({ selectedServer: this.props.globalServerSelected });
      }
    } else {
      this.TornjakApi.populateLocalAgentsUpdate(this.props.agentsListUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc);
      this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc);
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (IsManager) {
      if (prevProps.globalServerSelected !== this.props.globalServerSelected) {
        this.setState({ selectedServer: this.props.globalServerSelected });
      }
    }
  }

  onChangeAgentsList = selected => {
    var sid = selected.selectedItems, agents = "", agentsDisplay = "", assignedAgentsDisplay = "";
    let localAgentsIdList = [];
    for (let i = 0; i < sid.length; i++) {
      localAgentsIdList[i] = sid[i].label;
    }
    agents = localAgentsIdList;
    agentsDisplay = localAgentsIdList.toString();
    assignedAgentsDisplay = localAgentsIdList.join("\n");
    if (agentsDisplay.length === 0) {
      agentsDisplay = "Select Agents"
    }
    this.setState({
      clusterAgentsList: agents,
      agentsListDisplay: agentsDisplay,
      assignedAgentsListDisplay: assignedAgentsDisplay,
    });
  }

  onChangeClusterName(e) {
    var sid = e.target.value;
    this.setState({
      clusterName: sid
    });
    return
  }

  onChangeClusterType = selected => {
    var sid = selected.selectedItem;
    if (sid === this.state.clusterTypeManualEntryOption) {
      this.setState({
        clusterTypeManualEntry: true,
        clusterType: sid,
      });
    } else {
      this.setState({
        clusterType: sid,
        clusterTypeManualEntry: false
      });
    }
    return
  }

  onChangeManualClusterType(e) {
    var sid = e.target.value;
    this.setState({
      clusterType: sid
    });
    return
  }

  onChangeClusterDomainName(e) {
    var sid = e.target.value;
    this.setState({
      clusterDomainName: sid
    });
    return
  }

  onChangeClusterManagedBy(e) {
    var sid = e.target.value;
    this.setState({
      clusterManagedBy: sid
    });
    return
  }

  getApiEntryCreateEndpoint() {
    if (!IsManager) {
      return GetApiServerUri('/api/tornjak/clusters/create')
    } else if (IsManager && this.state.selectedServer !== "") {
      return GetApiServerUri('/manager-api/tornjak/clusters/create') + "/" + this.state.selectedServer
    } else {
      this.setState({ message: "Error: No server selected" })
      return ""
    }
  }

  onSubmit(e) {
    var agentsList = [];
    e.preventDefault();

    if (this.state.clusterName.length === 0) {
      this.setState({ message: "ERROR: Cluster Name Can Not Be Empty - Enter Cluster Name" });
      return
    }

    if (this.state.clusterType.length === 0) {
      this.setState({ message: "ERROR: Cluster Type Can Not Be Empty - Choose/ Enter Cluster Type" });
      return
    }

    if (this.state.clusterAgentsList !== "") {
      agentsList = this.state.clusterAgentsList;
    }

    var cjtData = {
      "cluster": {
        "name": this.state.clusterName,
        "platformType": this.state.clusterType,
        "domainName": this.state.clusterDomainName,
        "managedBy": this.state.clusterManagedBy,
        "agentsList": agentsList
      }
    }

    let endpoint = this.getApiEntryCreateEndpoint();
    if (endpoint === "") {
      return
    }
    axios.post(endpoint, cjtData)
      .then(
        res => this.setState({
          message: "Request:" + JSON.stringify(cjtData, null, ' ') + "\n\nSuccess:" + JSON.stringify(res.data, null, ' '),
          statusOK: "OK",
        })
      )
      .catch(
        err => this.setState({
          message: "ERROR:" + err.response.data,
          statusOK: "ERROR"
        })
      )
  }
  render() {
    const ClusterType = this.props.clusterTypeList;
    return (
      <div>
        <div className="cluster-create">
          <div className="create-create-title">
            <h3>Create Cluster</h3>
          </div>
          <form onSubmit={this.onSubmit}>
            <br /><br />
            <div className="entry-form">
              <div className="clustername-input-field">
                <TextInput
                  aria-required="true"
                  helperText="i.e. exampleabc"
                  id="clusterNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Cluster Name [*required]"
                  placeholder="Enter CLUSTER NAME"
                  onChange={this.onChangeClusterName}
                  required />
              </div>
              <div className="clustertype-drop-down">
                <Dropdown
                  aria-required="true"
                  ariaLabel="clustertype-drop-down"
                  id="clustertype-drop-down"
                  items={ClusterType}
                  label="Select Cluster Type"
                  titleText="Cluster Type [*required]"
                  onChange={this.onChangeClusterType}
                  required />
                <p className="cluster-helper">i.e. Kubernetes, VMs...</p>
              </div>
              {this.state.clusterTypeManualEntry === true &&
                <div className="clustertype-manual-input-field">
                  <TextInput
                    helperText="i.e. Kubernetes, VMs..."
                    id="clusterTypeManualInputField"
                    invalidText="A valid value is required - refer to helper text below"
                    labelText="Cluster Type - Manual Entry"
                    placeholder="Enter Cluster Type"
                    onChange={(e) => {
                      this.onChangeManualClusterType(e);
                    }}
                  />
                </div>}
              <div className="cluster-domain-name-input-field">
                <TextInput
                  helperText="i.e. example.org"
                  id="clusterDomainNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Cluster Domain Name/ URL"
                  placeholder="Enter CLUSTER DOMAIN NAME/ URL"
                  onChange={this.onChangeClusterDomainName}
                />
              </div>
              <div className="cluster-managed-by-input-field">
                <TextInput
                  helperText="i.e. person-A"
                  id="clusterNameInputField"
                  invalidText="A valid value is required - refer to helper text below"
                  labelText="Cluster Managed By"
                  placeholder="Enter CLUSTER MANAGED BY"
                  onChange={this.onChangeClusterManagedBy}
                />
              </div>
              <div className="agents-multiselect">
                <MultiSelect.Filterable
                  titleText="Assign Agents To Cluster"
                  helperText="i.e. spiffe://example.org/agent/myagent1..."
                  placeholder={this.state.agentsListDisplay}
                  ariaLabel="selectors-multiselect"
                  id="selectors-multiselect"
                  items={this.props.agentsList}
                  label={this.state.agentsListDisplay}
                  onChange={this.onChangeAgentsList}
                />
              </div>
              <div className="selectors-textArea">
                <TextArea
                  cols={50}
                  helperText="i.e. spiffe://example.org/agent/myagent1..."
                  id="selectors-textArea"
                  invalidText="A valid value is required"
                  labelText="Assigned Agents"
                  placeholder="Assigned agents will be populated here - Refer to Assign Agents To Cluster"
                  defaultValue={this.state.assignedAgentsListDisplay}
                  rows={8}
                  disabled
                />
              </div>
              <div className="form-group">
                <input type="submit" value="Create Cluster" className="btn btn-primary" />
              </div>
              <div>
                {this.state.statusOK === "OK" &&
                  <p className="success-message">--CLUSTER SUCCESSFULLY CREATED--</p>
                }
                {(this.state.statusOK === "ERROR") &&
                  <p className="failed-message">--CLUSTER CREATION FAILED--</p>
                }
              </div>
              <div className="alert-primary" role="alert">
                <pre>
                  {this.state.message}
                </pre>
              </div>
            </div>
          </form>
        </div>
      </div>
    )
  }
}


const mapStateToProps = (state) => ({
  globalClusterTypeInfo: state.clusters.globalClusterTypeInfo,
  globalServerSelected: state.servers.globalServerSelected,
  globalSelectorInfo: state.servers.globalSelectorInfo,
  globalAgentsList: state.agents.globalAgentsList,
  globalServerInfo: state.servers.globalServerInfo,
  globalTornjakServerInfo: state.servers.globalTornjakServerInfo,
  globalErrorMessage: state.tornjak.globalErrorMessage,
  globalWorkloadSelectorInfo: state.servers.globalWorkloadSelectorInfo,
  globalAgentsWorkLoadAttestorInfo: state.agents.globalAgentsWorkLoadAttestorInfo,
})

export default connect(
  mapStateToProps,
  { clusterTypeInfoFunc, serverSelectedFunc, selectorInfoFunc, agentsListUpdateFunc, tornjakMessageFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc }
)(ClusterCreate)


<|start_filename|>tornjak-frontend/src/tables/agents-list-table.js<|end_filename|>
import React from "react";
import { connect } from 'react-redux';
import GetApiServerUri from 'components/helpers';
import IsManager from 'components/is_manager';
import axios from 'axios';
import {
    agentsListUpdateFunc
} from 'redux/actions';
import Table from './list-table';

// AgentListTable takes in 
// listTableData: agents data to be rendered on table
// returns agents data inside a carbon component table with specified functions
class AgentsListTable extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            listData: props.data,
            listTableData: [{ "id": "0" }],
        };
        this.prepareTableData = this.prepareTableData.bind(this);
        this.deleteAgent = this.deleteAgent.bind(this);
        this.banAgent = this.banAgent.bind(this);
    }

    componentDidMount() {
        this.prepareTableData();
    }
    componentDidUpdate(prevProps, prevState) {
        if (prevProps !== this.props) {
            this.setState({
                listData: this.props.globalAgentsList
            })
            this.prepareTableData();
        }
    }

    prepareTableData() {
        const { data } = this.props;
        let listData = [...data];
        let listtabledata = [];
        for (let i = 0; i < listData.length; i++) {
            listtabledata[i] = {};
            listtabledata[i]["id"] = (i + 1).toString();
            listtabledata[i]["trustdomain"] = listData[i].props.agent.id.trust_domain;
            listtabledata[i]["spiffeid"] = "spiffe://" + listData[i].props.agent.id.trust_domain + listData[i].props.agent.id.path;
            listtabledata[i]["info"] = JSON.stringify(listData[i].props.agent, null, ' ');
            if (this.props.globalAgentsWorkLoadAttestorInfo !== undefined) {
                var check_id = this.props.globalAgentsWorkLoadAttestorInfo.filter(agent => (agent.spiffeid) === listtabledata[i].spiffeid);
                if (check_id.length !== 0) {
                    listtabledata[i]["plugin"] = check_id[0].plugin;
                }
                else {
                    listtabledata[i]["plugin"] = "No Plugin Configured For Agent";
                }
            } else {
                listtabledata[i]["plugin"] = "No Plugin Configured For Agent";
            }
        }
        this.setState({
            listTableData: listtabledata
        })
    }

    deleteAgent(selectedRows) {
        var id = [], endpoint = "", prefix = "spiffe://";
        let promises = [];
        if (IsManager) {
            endpoint = GetApiServerUri('/manager-api/agent/delete') + "/" + this.props.globalServerSelected;
        } else {
            endpoint = GetApiServerUri('/api/agent/delete');
        }
        if (selectedRows !== undefined && selectedRows.length !== 0) {
            for (let i = 0; i < selectedRows.length; i++) {
                id[i] = {}
                id[i]["trust_domain"] = selectedRows[i].cells[1].value;
                id[i]["path"] = selectedRows[i].cells[2].value.substr(selectedRows[i].cells[1].value.concat(prefix).length);
                promises.push(axios.post(endpoint, {
                    "id": {
                        "trust_domain": id[i].trust_domain,
                        "path": id[i].path,
                    }
                }))
            }
        } else {
            return ""
        }
        Promise.all(promises)
            .then(responses => {
                for (let i = 0; i < responses.length; i++) {
                    this.props.agentsListUpdateFunc(this.props.globalAgentsList.filter(el =>
                        el.id.trust_domain !== id[i].trust_domain ||
                        el.id.path !== id[i].path));
                }
            })
            .catch((error) => {
                console.log(error);
            })
    }

    banAgent(selectedRows) {
        var id = [], i = 0, endpoint = "", prefix = "spiffe://";
        if (IsManager) {
            endpoint = GetApiServerUri('/manager-api/agent/ban') + "/" + this.props.globalServerSelected
        } else {
            endpoint = GetApiServerUri('/api/agent/ban')
        }
        if (selectedRows !== undefined && selectedRows.length !== 0) {
            for (i = 0; i < selectedRows.length; i++) {
                id[i] = {}
                id[i]["trust_domain"] = selectedRows[i].cells[1].value;
                id[i]["path"] = selectedRows[i].cells[2].value.substr(selectedRows[i].cells[1].value.concat(prefix).length);
                axios.post(endpoint, {
                    "id": {
                        "trust_domain": id[i].trust_domain,
                        "path": id[i].path,
                    }
                })
                    .then(res => console.log(res.data), alert("Ban SUCCESS"), this.componentDidMount())
                    .catch((error) => {
                        console.log(error);
                    })
            }
        } else {
            return ""
        }
    }
    render() {
        const { listTableData } = this.state;
        const headerData = [
            {
                header: '#No',
                key: 'id',
            },
            {
                header: 'Trust Domain',
                key: 'trustdomain',
            },
            {
                header: 'SPIFFE ID',
                key: 'spiffeid',
            },
            {
                header: 'Info',
                key: 'info',
            },
            {
                header: 'Workload Attestor Plugin',
                key: 'plugin',
            }
        ];
        return (
            <div>
                <Table
                    entityType={"Agent"}
                    listTableData={listTableData}
                    headerData={headerData}
                    deleteEntity={this.deleteAgent}
                    banEntity={this.banAgent}
                />
            </div>
        );
    }
}

const mapStateToProps = (state) => ({
    globalServerSelected: state.servers.globalServerSelected,
    globalAgentsList: state.agents.globalAgentsList,
    globalAgentsWorkLoadAttestorInfo: state.agents.globalAgentsWorkLoadAttestorInfo,
})

export default connect(
    mapStateToProps,
    { agentsListUpdateFunc }
)(AgentsListTable)


<|start_filename|>tornjak-frontend/src/redux/reducers/agentsReducer.js<|end_filename|>
import {
    GLOBAL_AGENTS_LIST,
    GLOBAL_AGENTS_WORKLOADATTESTOR_INFO,
} from '../actions/types';

const initialState = {
    globalAgentsList: [],
    globalAgentsWorkLoadAttestorInfo: [],
};

export default function agentsReducer(state = initialState, action) {
    switch (action.type) {
        case GLOBAL_AGENTS_LIST:
            return {
                ...state,
                globalAgentsList: action.payload
            };
        case GLOBAL_AGENTS_WORKLOADATTESTOR_INFO:
            return {
                ...state,
                globalAgentsWorkLoadAttestorInfo: action.payload
            };
        default:
            return state;
    }
}

<|start_filename|>tornjak-frontend/src/tables/table-head.js<|end_filename|>
import React from "react";
import { DataTable } from "carbon-components-react";
const {
    TableHead,
    TableRow,
    TableSelectAll,
    TableHeader,
} = DataTable;

// Head takes in 
// getSelectionProps: getSelectionProps function for selecting all rows from DataTable
// headers: headerData of table
// getHeaderProps: getHeaderProps function from DataTable
// returns header of the table for the specified entity
class Head extends React.Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <TableHead>
                <TableRow>
                    <TableSelectAll {...this.props.getSelectionProps()} />
                    {this.props.headers.map((header) => (
                        <TableHeader key={header.header} {...this.props.getHeaderProps({ header })}>
                            {header.header}
                        </TableHeader>
                    ))}
                </TableRow>
            </TableHead>
        )
    }
};

export default (Head)


<|start_filename|>tornjak-backend/pkg/manager/types/serverinfo.go<|end_filename|>
package types

import (
	"crypto/tls"
	"crypto/x509"
	"github.com/pkg/errors"
	"net/http"
)

func (s ServerInfo) HttpClient() (*http.Client, error) {
	if s.TLS || s.MTLS {
		// Create a CA certificate pool and add cert.pem to it
		caCert := s.CA

		// TODO: allow use of generic cert authorities
		if len(s.CA) == 0 {
			return nil, errors.New("Cannot configure TLS if CA is not provided")
		}
		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCert)

		var mTLSCerts []tls.Certificate = nil
		if s.MTLS {
			// TODO: Add ability to support different cert for mTLS
			if len(s.Cert) == 0 || len(s.Key) == 0 {
				return nil, errors.New("Cannot configure MTLS if not key or cert is provided")
			}

			cert, err := tls.X509KeyPair(s.Cert, s.Key)
			if err != nil {
				return nil, err
			}
			mTLSCerts = []tls.Certificate{cert}
		}

		// Create a HTTPS client and supply the created CA pool
		client := &http.Client{
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{
					RootCAs:      caCertPool,
					Certificates: mTLSCerts,
				},
			},
		}

		return client, nil
	}

	// default to no TLS
	return &http.Client{}, nil
}
