function update
This commit is contained in:
+66
-1
@@ -4,6 +4,7 @@
|
||||
var el = {};
|
||||
var _resolve = null;
|
||||
var _busy = false;
|
||||
var _mode = 'confirm'; // 'confirm' | 'prompt'
|
||||
|
||||
function build() {
|
||||
el.overlay = document.createElement('div');
|
||||
@@ -25,6 +26,20 @@
|
||||
el.msg = document.createElement('p');
|
||||
el.msg.style.cssText = 'margin:0 0 20px;color:#c9d1d9;font-size:14px;line-height:1.6;white-space:pre-wrap';
|
||||
|
||||
// Prompt input (hidden in confirm mode)
|
||||
el.inputWrap = document.createElement('div');
|
||||
el.inputWrap.style.cssText = 'margin-bottom:12px;display:none';
|
||||
|
||||
el.input = document.createElement('input');
|
||||
el.input.type = 'text';
|
||||
el.input.className = 'field-input';
|
||||
el.input.style.cssText = 'width:100%;box-sizing:border-box;font-family:"JetBrains Mono",monospace;font-size:13px';
|
||||
el.inputWrap.appendChild(el.input);
|
||||
|
||||
el.inputErr = document.createElement('div');
|
||||
el.inputErr.style.cssText = 'margin-top:6px;font-size:12px;color:#ff3b3b;display:none';
|
||||
el.inputWrap.appendChild(el.inputErr);
|
||||
|
||||
el.row = document.createElement('div');
|
||||
el.row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end';
|
||||
|
||||
@@ -38,12 +53,14 @@
|
||||
el.row.appendChild(el.cancelBtn);
|
||||
el.row.appendChild(el.confirmBtn);
|
||||
el.box.appendChild(el.msg);
|
||||
el.box.appendChild(el.inputWrap);
|
||||
el.box.appendChild(el.row);
|
||||
el.overlay.appendChild(el.box);
|
||||
document.body.appendChild(el.overlay);
|
||||
|
||||
el.cancelBtn.addEventListener('click', function () { close(false); });
|
||||
el.confirmBtn.addEventListener('click', function () { close(true); });
|
||||
el.confirmBtn.addEventListener('click', function () { submitModal(); });
|
||||
el.input.addEventListener('keydown', function (e) { if (e.key === 'Enter') submitModal(); });
|
||||
el.overlay.addEventListener('click', function (e) {
|
||||
if (e.target === el.overlay) close(false);
|
||||
});
|
||||
@@ -52,17 +69,61 @@
|
||||
});
|
||||
}
|
||||
|
||||
var _promptPattern = null;
|
||||
|
||||
function openModal(msg, confirmLabel, isDanger, showCancel) {
|
||||
if (!el.overlay) build();
|
||||
_mode = 'confirm';
|
||||
_promptPattern = null;
|
||||
el.msg.textContent = msg;
|
||||
el.confirmBtn.textContent = confirmLabel || 'OK';
|
||||
el.confirmBtn.className = isDanger !== false ? 'btn-danger' : 'btn-primary';
|
||||
el.cancelBtn.style.display = showCancel === false ? 'none' : '';
|
||||
el.inputWrap.style.display = 'none';
|
||||
el.overlay.style.display = 'flex';
|
||||
el.confirmBtn.focus();
|
||||
return new Promise(function (res) { _resolve = res; });
|
||||
}
|
||||
|
||||
function openPrompt(msg, placeholder, pattern, confirmLabel) {
|
||||
if (!el.overlay) build();
|
||||
_mode = 'prompt';
|
||||
_promptPattern = pattern || null;
|
||||
el.msg.textContent = msg;
|
||||
el.confirmBtn.textContent = confirmLabel || 'Install';
|
||||
el.confirmBtn.className = 'btn-primary';
|
||||
el.cancelBtn.style.display = '';
|
||||
el.input.value = '';
|
||||
el.input.placeholder = placeholder || '';
|
||||
el.inputErr.style.display = 'none';
|
||||
el.inputErr.textContent = '';
|
||||
el.inputWrap.style.display = 'block';
|
||||
el.overlay.style.display = 'flex';
|
||||
setTimeout(function () { el.input.focus(); }, 50);
|
||||
return new Promise(function (res) { _resolve = res; });
|
||||
}
|
||||
|
||||
function submitModal() {
|
||||
if (_mode === 'prompt') {
|
||||
var val = el.input.value.trim();
|
||||
if (!val) {
|
||||
el.inputErr.textContent = 'Please enter a value.';
|
||||
el.inputErr.style.display = 'block';
|
||||
el.input.focus();
|
||||
return;
|
||||
}
|
||||
if (_promptPattern && !_promptPattern.test(val)) {
|
||||
el.inputErr.textContent = 'Invalid format. Use: author/item-name (e.g. crowdsecurity/ssh-bf)';
|
||||
el.inputErr.style.display = 'block';
|
||||
el.input.focus();
|
||||
return;
|
||||
}
|
||||
close(val);
|
||||
} else {
|
||||
close(true);
|
||||
}
|
||||
}
|
||||
|
||||
function close(result) {
|
||||
if (el.overlay) el.overlay.style.display = 'none';
|
||||
if (_resolve) { _resolve(result); _resolve = null; }
|
||||
@@ -76,6 +137,10 @@
|
||||
// Info/alert dialog — returns Promise<void>
|
||||
info: function (msg) {
|
||||
return openModal(msg, 'OK', false, false);
|
||||
},
|
||||
// Text input dialog — returns Promise<string|null>
|
||||
prompt: function (msg, placeholder, pattern, confirmLabel) {
|
||||
return openPrompt(msg, placeholder, pattern, confirmLabel);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,28 +16,61 @@
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
<div class="panel" style="margin-bottom:16px">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Add Entry</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="/allowlist/add">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:16px">
|
||||
<div>
|
||||
<label class="field-label">Allowlist Name</label>
|
||||
<input class="field-input" type="text" name="list" placeholder="default" required>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px">
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header"><span class="panel-title">Add IPs to Allowlist</span></div>
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="/allowlist/add">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<div style="display:grid;gap:12px;margin-bottom:16px">
|
||||
<div>
|
||||
<label class="field-label">Allowlist Name</label>
|
||||
<input class="field-input" type="text" name="list" placeholder="default" required
|
||||
list="list-names">
|
||||
{{if .Lists}}
|
||||
<datalist id="list-names">
|
||||
{{range .Lists}}<option value="{{.Name}}">{{end}}
|
||||
</datalist>
|
||||
{{end}}
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
||||
If the list does not exist it will be created automatically.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Comment (optional)</label>
|
||||
<input class="field-input" type="text" name="comment" placeholder="e.g. trusted office IP">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">IP Addresses / CIDR Ranges</label>
|
||||
<textarea class="field-input" name="value" rows="5"
|
||||
placeholder="One per line, or comma-separated: 1.2.3.4 192.168.0.0/24 8.8.8.8, 8.8.4.4"
|
||||
required style="resize:vertical;font-family:'JetBrains Mono',monospace;font-size:12px"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Value (IP or CIDR)</label>
|
||||
<input class="field-input" type="text" name="value" placeholder="1.2.3.4 or 1.2.3.0/24" required>
|
||||
<div style="display:flex;justify-content:flex-end">
|
||||
<button type="submit" class="btn-primary">Add to Allowlist</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end">
|
||||
<button type="submit" class="btn-primary">Add to Allowlist</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header"><span class="panel-title">Create New List</span></div>
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="/allowlist/create">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<div style="margin-bottom:16px">
|
||||
<label class="field-label">List Name</label>
|
||||
<input class="field-input" type="text" name="name" placeholder="mylist" required>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end">
|
||||
<button type="submit" class="btn-secondary">Create List</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{if .Lists}}
|
||||
@@ -46,6 +79,12 @@
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">{{.Name}}</span>
|
||||
{{if .Description}}<span style="font-size:12px;color:var(--muted)">{{.Description}}</span>{{end}}
|
||||
<form method="POST" action="/allowlist/delete-list" style="margin-left:auto">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<button type="submit" class="btn-danger-sm"
|
||||
data-confirm="Delete entire allowlist '{{.Name}}'? This cannot be undone.">Delete List</button>
|
||||
</form>
|
||||
</div>
|
||||
{{if .Items}}
|
||||
<table class="data-table">
|
||||
@@ -88,7 +127,7 @@
|
||||
<div class="panel">
|
||||
<div class="empty-state">
|
||||
<div class="empty-text">No allowlists found</div>
|
||||
<div class="empty-sub">Create an allowlist with cscli allowlists create, then add entries above</div>
|
||||
<div class="empty-sub">Use "Create New List" above, or type a list name in the Add form — it will be created automatically.</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -78,67 +78,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<form method="POST" action="/decisions/delete" id="bulk-form">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">
|
||||
Page {{.Page}}{{if .HasNext}} (more available){{end}}
|
||||
</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button type="button" class="btn-ghost-sm" onclick="toggleAll()">Select all</button>
|
||||
<button type="submit" class="btn-danger-sm" data-confirm-fn="confirmBulkDelete">Delete selected</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/* Bulk-delete form lives outside the table so row-level forms are never nested inside it.
|
||||
Checkboxes reference it via form="bulk-form". */}}
|
||||
<form id="bulk-form" method="POST" action="/decisions/delete" style="display:none">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
</form>
|
||||
|
||||
{{if .Decisions}}
|
||||
<div style="overflow-x:auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px"><input type="checkbox" id="chk-all" onchange="selectAll(this)"></th>
|
||||
<th>Value</th>
|
||||
<th>Type</th>
|
||||
<th>Scope</th>
|
||||
<th>Origin</th>
|
||||
<th>Scenario</th>
|
||||
<th>Duration</th>
|
||||
<th>Expires</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Decisions}}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="id" value="{{.ID}}" class="row-chk"></td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">
|
||||
{{if eq .Scope "Country"}}{{countryFlag .Value}} {{end}}{{.Value}}
|
||||
</td>
|
||||
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
|
||||
<td><span class="badge badge-gray">{{.Scope}}</span></td>
|
||||
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{truncate .Scenario 24}}</td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Duration}}</td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{if .Until}}{{truncate .Until 16}}{{else}}{{.Duration}}{{end}}</td>
|
||||
<td>
|
||||
<form method="POST" action="/decisions/delete" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button type="submit" class="btn-danger-sm" data-confirm="Delete this decision?">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">
|
||||
Page {{.Page}}{{if .HasNext}} (more available){{end}}
|
||||
</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button type="button" class="btn-ghost-sm" onclick="toggleAll()">Select all</button>
|
||||
<button type="submit" form="bulk-form" class="btn-danger-sm"
|
||||
data-confirm-fn="confirmBulkDelete">Delete selected</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<div class="empty-text">No decisions found</div>
|
||||
<div class="empty-sub">No active decisions match the current filters</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{if .Decisions}}
|
||||
<div style="overflow-x:auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px"><input type="checkbox" id="chk-all" onchange="selectAll(this)"></th>
|
||||
<th>Value</th>
|
||||
<th>Type</th>
|
||||
<th>Scope</th>
|
||||
<th>Origin</th>
|
||||
<th>Scenario</th>
|
||||
<th>Duration</th>
|
||||
<th>Expires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Decisions}}
|
||||
<tr>
|
||||
{{/* form="bulk-form" associates this checkbox with the external bulk form */}}
|
||||
<td><input type="checkbox" name="id" value="{{.ID}}" class="row-chk" form="bulk-form"></td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">
|
||||
{{if eq .Scope "Country"}}{{countryFlag .Value}} {{end}}{{.Value}}
|
||||
</td>
|
||||
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
|
||||
<td><span class="badge badge-gray">{{.Scope}}</span></td>
|
||||
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{truncate .Scenario 24}}</td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Duration}}</td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{if .Until}}{{truncate .Until 16}}{{else}}{{.Duration}}{{end}}</td>
|
||||
<td style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">
|
||||
|
||||
{{/* Permanent — standalone form, not inside bulk-form */}}
|
||||
<form method="POST" action="/decisions/add">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="value" value="{{.Value}}">
|
||||
<input type="hidden" name="scope" value="{{.Scope}}">
|
||||
<input type="hidden" name="type" value="{{.Type}}">
|
||||
<input type="hidden" name="duration" value="999999h">
|
||||
<input type="hidden" name="scenario" value="manual">
|
||||
<button type="submit" class="btn-ghost-sm"
|
||||
data-confirm="Make {{.Value}} permanent (999999h)?">Permanent</button>
|
||||
</form>
|
||||
|
||||
{{/* Extend — standalone hidden form, submitted by JS after modal */}}
|
||||
<form id="ext-{{.ID}}" method="POST" action="/decisions/add">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="value" value="{{.Value}}">
|
||||
<input type="hidden" name="scope" value="{{.Scope}}">
|
||||
<input type="hidden" name="type" value="{{.Type}}">
|
||||
<input type="hidden" name="duration" id="ext-dur-{{.ID}}" value="">
|
||||
<input type="hidden" name="scenario" value="manual">
|
||||
</form>
|
||||
<button type="button" class="btn-ghost-sm"
|
||||
onclick="extendDecision({{.ID}})">Extend</button>
|
||||
|
||||
{{/* Delete — standalone form, not inside bulk-form */}}
|
||||
<form method="POST" action="/decisions/delete">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button type="submit" class="btn-danger-sm"
|
||||
data-confirm="Delete decision for {{.Value}}?">Delete</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<div class="empty-text">No decisions found</div>
|
||||
<div class="empty-sub">No active decisions match the current filters</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or (gt .Page 1) .HasNext}}
|
||||
<div class="pagination">
|
||||
@@ -174,5 +207,18 @@ function confirmBulkDelete() {
|
||||
if (n === 0) { appModal.info('Select at least one decision.'); return null; }
|
||||
return 'Delete ' + n + ' decision(s)? This cannot be undone.';
|
||||
}
|
||||
var _durPattern = /^\d+[smhdw]$/;
|
||||
function extendDecision(id) {
|
||||
appModal.prompt(
|
||||
'Extend decision\n\nEnter new duration:',
|
||||
'48h',
|
||||
_durPattern,
|
||||
'Extend'
|
||||
).then(function(dur) {
|
||||
if (!dur) return;
|
||||
document.getElementById('ext-dur-' + id).value = dur;
|
||||
document.getElementById('ext-' + id).submit();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -8,19 +8,30 @@
|
||||
<div class="page-sub">Collections, parsers, scenarios, and postoverflows</div>
|
||||
</div>
|
||||
{{if .CLIAvailable}}
|
||||
<form method="POST" action="/hub/update">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn-secondary"
|
||||
data-confirm="Run hub update + upgrade? This may take a minute." data-confirm-label="Update">Update All</button>
|
||||
</form>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button type="button" class="btn-secondary" onclick="promptInstallNew()">Install New...</button>
|
||||
<form method="POST" action="/hub/update">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn-secondary"
|
||||
data-confirm="Run hub update + upgrade? This may take a minute." data-confirm-label="Update">Update All</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* Hidden form used by Install New modal */}}
|
||||
<form id="install-new-form" method="POST" action="/hub/install" style="display:none">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<input type="hidden" name="tab" value="{{.Tab}}">
|
||||
<input type="hidden" name="kind" value="{{.Tab}}">
|
||||
<input type="hidden" name="name" id="install-new-name" value="">
|
||||
</form>
|
||||
|
||||
{{if not .CLIAvailable}}
|
||||
<div class="cli-unavail-banner" style="margin-bottom:16px">
|
||||
<div>
|
||||
<strong style="display:block;margin-bottom:4px">cscli unavailable</strong>
|
||||
Hub management requires the cscli binary. Mount it at the CSCLI_PATH configured in your environment.
|
||||
Hub management requires the cscli binary.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -40,50 +51,57 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Version</th>
|
||||
<th>State</th>
|
||||
<th>Action</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{$tab := .Tab}}
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
{{$active := or .Installed .Downloaded (ne .Status "")}}
|
||||
{{$needsUpdate := eq .Status "update-available"}}
|
||||
<tr{{if .RemovedByUI}} style="opacity:0.7"{{end}}>
|
||||
<td>
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:600">{{.Name}}</div>
|
||||
{{if .Description}}<div style="font-size:11px;color:var(--muted);margin-top:2px">{{truncate .Description 64}}</div>{{end}}
|
||||
</td>
|
||||
<td><span class="badge {{hubStatusClass .Status}}">{{.Status}}</span></td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)">{{.Version}}</td>
|
||||
<td style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)">
|
||||
{{if .LocalVersion}}{{.LocalVersion}}{{else if and $active .Version}}{{.Version}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Tainted}}
|
||||
<span class="badge badge-amber">tainted</span>
|
||||
{{else if .UpToDate}}
|
||||
<span class="badge badge-green">up to date</span>
|
||||
{{else if .Installed}}
|
||||
<span class="badge badge-amber">update avail</span>
|
||||
{{else if $needsUpdate}}
|
||||
<span class="badge badge-amber">update available</span>
|
||||
{{else if $active}}
|
||||
<span class="badge badge-green">installed</span>
|
||||
{{else if .RemovedByUI}}
|
||||
<span class="badge badge-purple">removed</span>
|
||||
{{else}}
|
||||
<span class="badge badge-gray">not installed</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Installed}}
|
||||
<form method="POST" action="/hub/remove" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="kind" value="{{$tab}}">
|
||||
<input type="hidden" name="tab" value="{{$tab}}">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<button type="submit" class="btn-danger-sm" data-confirm="Remove {{.Name}}?">Remove</button>
|
||||
</form>
|
||||
<td style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
{{if $active}}
|
||||
{{if .ConfigEditorURL}}
|
||||
<a href="{{.ConfigEditorURL}}" class="btn-ghost-sm">Edit Config</a>
|
||||
{{end}}
|
||||
<form method="POST" action="/hub/remove" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="kind" value="{{$tab}}">
|
||||
<input type="hidden" name="tab" value="{{$tab}}">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<button type="submit" class="btn-danger-sm" data-confirm="Remove {{.Name}}? Files will be deleted.">Remove</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form method="POST" action="/hub/install" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="kind" value="{{$tab}}">
|
||||
<input type="hidden" name="tab" value="{{$tab}}">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<button type="submit" class="btn-safe-sm">Install</button>
|
||||
</form>
|
||||
<form method="POST" action="/hub/install" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="kind" value="{{$tab}}">
|
||||
<input type="hidden" name="tab" value="{{$tab}}">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<button type="submit" class="btn-safe-sm">Install</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -94,10 +112,37 @@
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<div class="empty-text">No {{.Tab}} found</div>
|
||||
<div class="empty-sub">Run "Update All" to refresh the hub index</div>
|
||||
<div class="empty-sub">Run "Update All" to refresh the hub index, then use "Install New..." to add items</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
var _hubTab = '{{.Tab}}';
|
||||
var _tabSingular = {
|
||||
collections: 'collection',
|
||||
parsers: 'parser',
|
||||
scenarios: 'scenario',
|
||||
postoverflows: 'postoverflow'
|
||||
};
|
||||
var _hubItemPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
||||
|
||||
function promptInstallNew() {
|
||||
var kind = _tabSingular[_hubTab] || _hubTab;
|
||||
appModal.prompt(
|
||||
'Install ' + kind + '\n\nEnter the hub item name (author/item-name):',
|
||||
'e.g. crowdsecurity/ssh-bf',
|
||||
_hubItemPattern,
|
||||
'Install'
|
||||
).then(function (name) {
|
||||
if (!name) return;
|
||||
document.getElementById('install-new-name').value = name;
|
||||
document.getElementById('install-new-form').submit();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user