added multi agent execution
This commit is contained in:
parent
2056479224
commit
1ce6d2e676
12
main.go
12
main.go
|
@ -6,7 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"slices"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
@ -118,19 +118,13 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
agents, err := api.GetAgents(db)
|
agents, err := api.GetAgents(db)
|
||||||
currentAgents := getAgentsStatus()
|
currentAgents := getAgentsStatus()
|
||||||
|
|
||||||
for _, currAgent := range currentAgents {
|
for i := range agents {
|
||||||
for i, agent := range agents {
|
if slices.Contains(currentAgents, agents[i].AgentName) {
|
||||||
if currAgent == agent.AgentName {
|
|
||||||
// log.Printf("%s online", agent.AgentName)
|
|
||||||
// logger.InsertLog(logger.Debug, fmt.Sprintf("%s online after page refresh", agent.AgentName))
|
|
||||||
// agents[i].Status = fmt.Sprint("<span class=\"badge bg-success\">Connected</span>")
|
|
||||||
agents[i].Status = "Connected"
|
agents[i].Status = "Connected"
|
||||||
} else {
|
} else {
|
||||||
// agent.Status = fmt.Sprintf("<span class=\"badge bg-danger\">Disconnected</span>")
|
|
||||||
agents[i].Status = "Disconnected"
|
agents[i].Status = "Disconnected"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to fetch agents", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch agents", http.StatusInternalServerError)
|
||||||
|
|
|
@ -235,6 +235,67 @@ type Message struct {
|
||||||
Payload string `json:"payload"`
|
Payload string `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
|
||||||
|
// err := r.ParseForm()
|
||||||
|
// if err != nil {
|
||||||
|
// http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||||
|
// logger.InsertLog(logger.Info, "Invalid form data")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// agentName := r.FormValue("agentName")
|
||||||
|
// command := r.FormValue("command")
|
||||||
|
|
||||||
|
// agentSocketsMutex.Lock()
|
||||||
|
// conn, ok := agentSockets[agentName]
|
||||||
|
// agentSocketsMutex.Unlock()
|
||||||
|
|
||||||
|
// if !ok {
|
||||||
|
// http.Error(w, "Agent not connected", http.StatusNotFound)
|
||||||
|
// logger.InsertLog(logger.Info, "Agent not connected")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// responseChan := make(chan string, 1)
|
||||||
|
// responseChannels.Store(agentName, responseChan)
|
||||||
|
// defer responseChannels.Delete(agentName)
|
||||||
|
|
||||||
|
// message := Message {
|
||||||
|
// Type: "command",
|
||||||
|
// Payload: command,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// messageBytes, _ := json.Marshal(message)
|
||||||
|
|
||||||
|
// err = conn.WriteMessage(websocket.TextMessage, messageBytes)
|
||||||
|
// if err != nil {
|
||||||
|
// http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
|
||||||
|
// logger.InsertLog(logger.Error, "Failed to send command to the agent")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// select {
|
||||||
|
// case response := <-responseChan:
|
||||||
|
// var parsedResponse map[string]string
|
||||||
|
// if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
|
||||||
|
// http.Error(w, "Failed to parse response", http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// payload, ok := parsedResponse["payload"]
|
||||||
|
// if !ok {
|
||||||
|
// http.Error(w, "Invalid response structure", http.StatusInternalServerError)
|
||||||
|
// logger.InsertLog(logger.Error, "Invalid response structure")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// w.WriteHeader(http.StatusOK)
|
||||||
|
// w.Header().Set("Content-Type", "text/plain")
|
||||||
|
// w.Write([]byte(payload))
|
||||||
|
// case <- time.After(10 * time.Second):
|
||||||
|
// http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
|
||||||
|
// logger.InsertLog(logger.Info, "Agent response timed out")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
|
var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -243,59 +304,103 @@ var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
agentNameStr := r.FormValue("agentNames")
|
||||||
|
var agentNames []string
|
||||||
|
|
||||||
|
if agentNameStr != "" {
|
||||||
|
agentNames = strings.Split(agentNameStr, ",")
|
||||||
|
} else {
|
||||||
agentName := r.FormValue("agentName")
|
agentName := r.FormValue("agentName")
|
||||||
|
if agentName != "" {
|
||||||
|
agentNames = []string{agentName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
command := r.FormValue("command")
|
command := r.FormValue("command")
|
||||||
|
|
||||||
|
if len(agentNames) == 0 || command == "" {
|
||||||
|
http.Error(w, "Missing agent or command", http.StatusBadRequest)
|
||||||
|
logger.InsertLog(logger.Error, "Missing agent or command")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
AgentName string
|
||||||
|
Type string
|
||||||
|
Payload string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsChan := make(chan result, len(agentNames))
|
||||||
|
|
||||||
|
for _, agentName := range agentNames {
|
||||||
|
agentName := strings.TrimSpace(agentName)
|
||||||
|
|
||||||
|
go func(agent string) {
|
||||||
agentSocketsMutex.Lock()
|
agentSocketsMutex.Lock()
|
||||||
conn, ok := agentSockets[agentName]
|
conn, ok := agentSockets[agentName]
|
||||||
agentSocketsMutex.Unlock()
|
agentSocketsMutex.Unlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Agent not connected", http.StatusNotFound)
|
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Agent not connected")}
|
||||||
logger.InsertLog(logger.Info, "Agent not connected")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
responseChan := make(chan string, 1)
|
responseChan := make(chan string, 1)
|
||||||
responseChannels.Store(agentName, responseChan)
|
responseChannels.Store(agent, responseChan)
|
||||||
defer responseChannels.Delete(agentName)
|
defer responseChannels.Delete(agent)
|
||||||
|
|
||||||
message := Message {
|
msg := Message {
|
||||||
Type: "command",
|
Type: "command",
|
||||||
Payload: command,
|
Payload: command,
|
||||||
}
|
}
|
||||||
|
|
||||||
messageBytes, _ := json.Marshal(message)
|
msgBytes, _ := json.Marshal(msg)
|
||||||
|
err := conn.WriteMessage(websocket.TextMessage, msgBytes)
|
||||||
err = conn.WriteMessage(websocket.TextMessage, messageBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
|
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Send failed")}
|
||||||
logger.InsertLog(logger.Error, "Failed to send command to the agent")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case response := <-responseChan:
|
case resp := <- responseChan:
|
||||||
var parsedResponse map[string]string
|
var parsed map[string]string
|
||||||
if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
|
if err:= json.Unmarshal([]byte(resp), &parsed); err != nil {
|
||||||
http.Error(w, "Failed to parse response", http.StatusInternalServerError)
|
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Invalid response")}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload, ok := parsedResponse["payload"]
|
|
||||||
|
payload, ok := parsed["payload"]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Invalid response structure", http.StatusInternalServerError)
|
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("No payload")}
|
||||||
logger.InsertLog(logger.Error, "Invalid response structure")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resultsChan <- result{AgentName: agent, Payload: payload}
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Timeout")}
|
||||||
|
}
|
||||||
|
} (agentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var combined strings.Builder
|
||||||
|
for i := 0; i < len(agentNames); i++ {
|
||||||
|
res := <- resultsChan
|
||||||
|
if res.Err != nil {
|
||||||
|
combined.WriteString(fmt.Sprintf("[%s] ERROR: %s\n", res.AgentName, res.Err.Error()))
|
||||||
|
} else {
|
||||||
|
combined.WriteString(fmt.Sprintf("[%s] %s\n", res.AgentName, res.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
w.Write([]byte(payload))
|
w.Write([]byte(combined.String()))
|
||||||
case <- time.After(10 * time.Second):
|
|
||||||
http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
|
|
||||||
logger.InsertLog(logger.Info, "Agent response timed out")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func Server() (*http.Server) {
|
func Server() (*http.Server) {
|
||||||
webSocketHandler := webSocketHandler {
|
webSocketHandler := webSocketHandler {
|
||||||
upgrader: websocket.Upgrader{
|
upgrader: websocket.Upgrader{
|
||||||
|
|
|
@ -10,70 +10,54 @@
|
||||||
<!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> -->
|
<!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> -->
|
||||||
<title>g2: gommand & gontrol</title>
|
<title>g2: gommand & gontrol</title>
|
||||||
<script>
|
<script>
|
||||||
// Query Agents for the Dropdown Menu
|
const checkboxState = new Map();
|
||||||
<!-- document.addEventListener('DOMContentLoaded', () => { -->
|
|
||||||
<!-- fetch('/agentNames') -->
|
|
||||||
<!-- .then(response => response.json()) -->
|
|
||||||
<!-- .then(agentNames => { -->
|
|
||||||
<!-- const dropdown = document.getElementById('agentName'); -->
|
|
||||||
<!-- agentNames.forEach(name => { -->
|
|
||||||
<!-- const option = document.createElement('option'); -->
|
|
||||||
<!-- option.value = name; -->
|
|
||||||
<!-- option.textContent = name; -->
|
|
||||||
<!-- dropdown.appendChild(option); -->
|
|
||||||
<!-- }); -->
|
|
||||||
<!-- }) -->
|
|
||||||
<!-- .catch(error => console.error('Error fetching agent names:', error)); -->
|
|
||||||
<!-- }); -->
|
|
||||||
<!-- // Query agents currently connected to the websocket and put status into the table -->
|
|
||||||
<!-- const updateAgentStatuses = () => { -->
|
|
||||||
<!-- fetch('http://localhost:5555/agentNames') -->
|
|
||||||
<!-- .then(response => response.json()) -->
|
|
||||||
<!-- .then(agentNames => { -->
|
|
||||||
<!-- console.log("Agent names fetched:", agentNames); -->
|
|
||||||
<!-- const tableRows = document.querySelectorAll('#agentList table tbody tr'); -->
|
|
||||||
<!-- tableRows.forEach(row => { -->
|
|
||||||
<!-- const nameCell = row.querySelector('td:nth-child(2)'); -->
|
|
||||||
<!-- const statusCell = row.querySelector('td:nth-child(5)'); -->
|
|
||||||
<!-- if (nameCell && statusCell) { -->
|
|
||||||
<!-- const agentName = nameCell.textContent.trim(); -->
|
|
||||||
<!-- if (agentNames.includes(agentName)) { -->
|
|
||||||
<!-- statusCell.innerHTML = '<span class="badge bg-success">Connected</span>'; -->
|
|
||||||
<!-- } else { -->
|
|
||||||
<!-- statusCell.innerHTML = '<span class="badge bg-danger">Disconnected</span>'; -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- }); -->
|
|
||||||
<!-- }) -->
|
|
||||||
<!-- .catch(error => console.error('Error fetching agent names:', error)); -->
|
|
||||||
<!-- }; -->
|
|
||||||
<!-- updateAgentStatuses(); -->
|
|
||||||
<!-- setInterval(updateAgentStatuses, 5000); -->
|
|
||||||
|
|
||||||
<!-- document.body.addEventListener('htmx:afterSwap', function(evt) { -->
|
|
||||||
<!-- if (evt.detail.xhr.status === 200) { -->
|
|
||||||
<!-- const tableRows = document.querySelectorAll('#agentList table tbody tr'); -->
|
|
||||||
<!-- tableRows.forEach(row => { -->
|
|
||||||
<!-- const nameCell = row.querySelector('td:nth-child(2)'); -->
|
|
||||||
<!-- const statusCell = row.querySelector('td:nth-child(5)'); -->
|
|
||||||
<!-- if (nameCell && statusCell) { -->
|
|
||||||
<!-- const agentName = nameCell.textContent.trim(); -->
|
|
||||||
<!-- if ("Connected" === statusCell.innerHTML) { -->
|
|
||||||
<!-- statusCell.innerHTML = '<span class="badge bg-success">Connected</span>'; -->
|
|
||||||
<!-- } else { -->
|
|
||||||
<!-- statusCell.innerHTML = '<span class="badge bg-danger">Disconnected</span>'; -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- } -->
|
|
||||||
<!-- }); -->
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
document.body.addEventListener('htmx:beforeSwap', function(event) {
|
||||||
|
if (event.target.id === "agentList") {
|
||||||
|
saveCheckboxState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||||
if (event.target.id === "agentList") {
|
if (event.target.id === "agentList") {
|
||||||
|
restoreCheckboxState();
|
||||||
updateAgentDropdown();
|
updateAgentDropdown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function prepareAgentNames(event) {
|
||||||
|
const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => cb.dataset.agentName);
|
||||||
|
|
||||||
|
const hiddenInput = document.getElementById('agentNamesInput');
|
||||||
|
|
||||||
|
if (selected.length > 0) {
|
||||||
|
document.getElementById('agentName').removeAttribute('name');
|
||||||
|
hiddenInput.value = selected.join(',');
|
||||||
|
} else {
|
||||||
|
document.getElementById('agentName').setAttribute('name', 'agentName');
|
||||||
|
hiddenInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCheckboxState() {
|
||||||
|
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
|
||||||
|
checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreCheckboxState() {
|
||||||
|
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
|
||||||
|
const state = checkboxState.get(checkbox.dataset.agentName);
|
||||||
|
if (state !== undefined) {
|
||||||
|
checkbox.checked = state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAgentDropdown() {
|
function updateAgentDropdown() {
|
||||||
const select = document.getElementById("agentName");
|
const select = document.getElementById("agentName");
|
||||||
|
@ -103,7 +87,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
@ -137,11 +120,11 @@
|
||||||
<!-- Agent Commands -->
|
<!-- Agent Commands -->
|
||||||
<div id="agentCommands">
|
<div id="agentCommands">
|
||||||
<h3>Command Execution</h3>
|
<h3>Command Execution</h3>
|
||||||
<form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded" hx-swap="innerHTML">
|
<form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded" hx-swap="innerHTML" onsubmit="prepareAgentNames(event)">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="agentName" class="form-label">Agent Name</label>
|
<label for="agentName" class="form-label">Agent Name</label>
|
||||||
<!-- <select class="form-select" id="agentName" name="agentName" required> -->
|
<!-- <select class="form-select" id="agentName" name="agentName" required> -->
|
||||||
<select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown" required>
|
<select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown">
|
||||||
<option value="" disabled selected>Select an Agent</option>
|
<option value="" disabled selected>Select an Agent</option>
|
||||||
<!-- Dynamically populated with agent names -->
|
<!-- Dynamically populated with agent names -->
|
||||||
</select>
|
</select>
|
||||||
|
@ -151,39 +134,12 @@
|
||||||
<input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
|
<input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Execute</button>
|
<button type="submit" class="btn btn-primary">Execute</button>
|
||||||
|
<!-- Hidden checkbox form !-->
|
||||||
|
<input type="hidden" name="agentNames" id="agentNamesInput">
|
||||||
</form>
|
</form>
|
||||||
<pre id="commandOutput"></pre>
|
<pre id="commandOutput"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Agent Form -->
|
|
||||||
<!-- <button class="btn btn-primary mt-3" data-bs-toggle="collapse" data-bs-target="#addAgentForm">Add Agent</button> -->
|
|
||||||
<!-- <div id="addAgentForm" class="collapse mt-2"> -->
|
|
||||||
<!-- <form hx-post="/agents" hx-target="#agentList" hx-swap="innerHTML"> -->
|
|
||||||
<!-- <div class="mb-3"> -->
|
|
||||||
<!-- <label for="agentId" class="form-label">Agent Id</label> -->
|
|
||||||
<!-- <input type="text" class="form-control" id="agentId" name="agentId" required> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- <div class="mb-3"> -->
|
|
||||||
<!-- <label for="agentName" class="form-label">Agent Name</label> -->
|
|
||||||
<!-- <input type="text" class="form-control" id="agentName" name="agentName" required> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- <div class="mb-3"> -->
|
|
||||||
<!-- <label for="IPv4Address" class="form-label">IPv4 Address</label> -->
|
|
||||||
<!-- <input type="text" class="form-control" id="IPv4Address" name="IPv4Address" required> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- <div class="mb-3"> -->
|
|
||||||
<!-- <label for="initialContact" class="form-label">Initial Contact</label> -->
|
|
||||||
<!-- <input type="datetime-local" class="form-control" id="initialContact" name="initialContact" required> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- <div class="mb-3"> -->
|
|
||||||
<!-- <label for="lastContact" class="form-label">Last Contact</label> -->
|
|
||||||
<!-- <input type="datetime-local" class="form-control" id="lastContact" name="lastContact" required> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- <button type="submit" class="btn btn-success">Add Agent</button> -->
|
|
||||||
<!-- </form> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
|
|
||||||
<!-- Logs Section -->
|
<!-- Logs Section -->
|
||||||
<h3>Logs</h3>
|
<h3>Logs</h3>
|
||||||
<div id="logs-container" hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 3s">
|
<div id="logs-container" hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 3s">
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
|
<!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
|
||||||
<td>{{.Status}}</td>
|
<td>{{.Status}}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}">
|
||||||
<button class="btn btn-warning" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button>
|
<button class="btn btn-warning" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button>
|
||||||
|
|
||||||
<button class="btn btn-danger" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
|
<button class="btn btn-danger" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
|
||||||
|
|
Loading…
Reference in New Issue